
AI 可能會寫的 Vue Composable 地雷,你踩過了嗎?
大家有沒有請 AI 幫忙寫過 Vue composable?
甚麼?你問現在誰還在手寫程式?抱歉我多問了。(´;ω;`)
請 AI 寫一個「多個元件共用狀態」的 composable,雖然不是每次,但是他有可能會給你這樣的程式碼(尤其是降智的時候 ( ◔ ௰◔))。
import { ref } from 'vue'
const count = ref(0)
export function useCounter() {
function increment() {
count.value++
}
function decrement() {
count.value--
}
return { count, increment, decrement }
}把 ref 放在函式外面,這樣不管哪個元件呼叫 useCounter(),拿到的都是同一份 count,完美實現共用狀態。
AI 還會貼心地解釋:「把變數宣告在模組層級,所有呼叫者就能共用同一份響應式狀態。」...(́⊙◞౪◟⊙‵)
為什麼放外面就能共用?
ES Module 只會被執行一次,之後不管多少地方 import,拿到的都是同一個模組實例。
所以宣告在模組頂層的變數,自然就變成全域共用的單例(Singleton)。
看起來很合理對吧?但其實這段程式碼可能在無意間埋下地雷。ԅ( ˘ω˘ԅ)
這段程式碼有什麼問題?
既然知道這是利用 ES Module 的單例特性,讓我們想想這代表什麼。
count 這個 ref 從程式啟動到結束,永遠只有一份。
如果你的專案用了 Nuxt 這類有 SSR 的框架,要特別注意。
在 SSR 環境中,server 端的模組被所有請求共用。
SPA 就不用擔心了吧?
純 SPA 的確不會有跨請求污染的問題,不過難保未來專案不會加上 SSR,到時候就變成埋在深處的地雷了。
既然有更安全的寫法,不如一開始就避開這顆潛在的地雷。
甚麼?你說你就是想埋?我不知道,我甚麼都沒聽到(。-`ω´-)
也就是說,Request A 的使用者修改了 count,Request B 的使用者進來時,拿到的 count 就是 A 改過的值。
// 模組層級的變數,server 端所有請求共用
const count = ref(0)
export function useCounter() {
// Request A 呼叫 increment(),count 變成 1
// Request B 進來時,count 已經是 1 了
function increment() {
count.value++
}
return { count, increment }
}這就是 Vue 官方文件中特別警告的 Cross-Request State Pollution(跨請求狀態污染)。
使用者 A 的資料洩漏到使用者 B 的頁面上,輕則顯示錯誤,重則變成資安事件。
正確的替代方案
經典用法是請出我們可愛的鳳梨 Pinia。( ´ ▽ ` )ノ🍍
Pinia 的 store 綁定在 Vue 的 app instance 上,SSR 時每個請求都會拿到獨立的 store 實例,從根本上解決跨請求污染問題。
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment }
})如果你用的是 Nuxt,官方提供了 useState 這個 composable,專門用來建立 SSR 安全的共用狀態。
以剛才的 useCounter 為例,可以改寫成:
export function useCounter() {
const count = useState('count', () => 0)
function increment() {
count.value++
}
function decrement() {
count.value--
}
return { count, increment, decrement }
}useState 底層會將狀態綁定在請求的 context 上,server 端不同請求之間不會互相污染,同時還會自動將 server 端的狀態序列化傳遞給 client 端,避免 hydration mismatch。
一定要用 Pinia 嗎?
有些時候你只是想讓幾個元件共用一個 composable 的狀態,又覺得為了這點小事特地開一顆鳳梨(Pinia store)感覺有點太重了。
這時候可以試試 VueUse 提供的 createSharedComposable。
import { createSharedComposable } from '@vueuse/core'
import { ref } from 'vue'
function _useCounter() {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment }
}
export const useCounter = createSharedComposable(_useCounter)createSharedComposable 會確保不管多少個元件呼叫 useCounter(),底層的 _useCounter 只會執行一次,所有元件拿到的都是同一份狀態。
而且在 SSR 環境下會自動退回非共用模式,每個請求都拿到獨立的實例,不會有跨請求污染問題。( •̀ ω •́ )✧
簡單整理一下選擇方式:
| 需求 | 方案 |
|---|---|
| 各元件獨立狀態 | 變數放函式內部 |
| 輕量共用,不想開 store | createSharedComposable |
| 複雜共用狀態、需要 devtools | Pinia |
總結 🐟
- 把
ref放在 composable 函式外面,利用 ES Module 單例特性共用狀態,在 SSR 環境下會造成跨請求狀態污染 - 即使目前是純 SPA,也建議避開這種寫法,為未來可能加入 SSR 預留彈性
- 需要跨元件共用狀態時,優先考慮 Pinia,Nuxt 專案可用
useState - 覺得開 store 太重,可以試試 VueUse 的
createSharedComposable,輕量且 SSR 安全
感謝您讀到這裡,如果您覺得有收穫,歡迎分享出去。
有錯誤或任何想法還請多多指教。( ´∀`)~♥