目標:把 Pinia 的兩種寫法講透,寫明“怎么寫、怎么用、怎么選、各自優缺點與典型場景”。全文配完整代碼與注意事項,可直接當團隊規范參考。
一、背景與準備
- 適用版本:Vue 3 + Pinia 2.x
- 安裝與初始化:
# 安裝
npm i pinia# main.ts/main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'const app = createApp(App)
app.use(createPinia())
app.mount('#app')
Pinia 提供兩種定義 Store 的方式:
- Options Store(配置式):寫法類似 Vuex,結構清晰,學習成本低。
- Setup Store(組合式):寫法與 Composition API 一致,靈活可復用,能直接使用
ref
、computed
、watch
、自定義 composable。
下面分別實現 同一個業務:計數器 + 異步拉取用戶信息,用兩種寫法各做一遍,再對比差異與使用場景。
二、Options Store(配置式)
2.1 定義
// stores/counter.ts
import { defineStore } from 'pinia'export const useCounterStore = defineStore('counter', {// 1) state:必須是函數,返回對象;可被 DevTools 追蹤、可序列化state: () => ({count: 0,user: null as null | { id: number; name: string }}),// 2) getters:類似計算屬性,支持緩存與依賴追蹤getters: {double: (state) => state.count * 2,welcome(state) {return state.user ? `Hi, ${state.user.name}` : 'Guest'}},// 3) actions:業務方法,支持異步;這里的 this 指向 store 實例actions: {increment() {this.count++},reset() {this.$reset()},async fetchUser() {// 模擬請求await new Promise((r) => setTimeout(r, 400))this.user = { id: 1, name: 'Tom' }}}
})
注意:在 actions 里不要用箭頭函數,否則
this
不指向 store;如果必須用箭頭函數,改為顯式引用useCounterStore()
返回的實例。
2.2 組件中使用
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'const counter = useCounterStore()
// 解構 state/getters 請用 storeToRefs,保持解構后的值仍具備響應性
const { count, double, welcome, user } = storeToRefs(counter)function add() {counter.increment()
}function load() {counter.fetchUser()
}
</script><template><div class="card"><p>count: {{ count }}</p><p>double: {{ double }}</p><p>{{ welcome }}</p><button @click="add">+1</button><button @click="load">拉取用戶</button></div>
</template>
2.3 進階用法
const counter = useCounterStore()// 批量更新(避免多次觸發)
counter.$patch({ count: counter.count + 2, user: { id: 2, name: 'Jerry' } })// 監聽狀態變化(持久化/日志)
const unsubscribe = counter.$subscribe((mutation, state) => {// mutation.type: 'direct' | 'patch object' | 'patch function'// 可在這里做本地存儲localStorage.setItem('counter', JSON.stringify(state))
})// 監聽 action 調用鏈
counter.$onAction(({ name, args, onAfter, onError }) => {console.time(name)onAfter(() => console.timeEnd(name))onError((e) => console.error('[action error]', name, e))
})
優點小結(Options Store)
- 結構化:
state/getters/actions
職責清晰、易讀易管控。 - 遷移友好:從 Vuex 遷移幾乎零心智負擔。
- 可序列化:
state
天生適合被 DevTools/SSR 序列化與持久化插件處理。 this
能直達 getters/state,寫法直觀(注意不要箭頭函數)。
注意點
- 需要通過
this
訪問 store(對 TS “this” 的類型提示依賴更強)。 - 在
getters
中不要產生副作用;復雜邏輯建議放到actions
。
三、Setup Store(組合式)
3.1 定義
// stores/counter-setup.ts
import { defineStore } from 'pinia'
import { ref, computed, watch } from 'vue'export const useCounterSetup = defineStore('counter-setup', () => {// 1) 直接用 Composition APIconst count = ref(0)const user = ref<null | { id: number; name: string }>(null)const double = computed(() => count.value * 2)const welcome = computed(() => (user.value ? `Hi, ${user.value.name}` : 'Guest'))// 2) 方法就寫普通函數(無 this,更易測試/復用)function increment() {count.value++}function reset() {count.value = 0user.value = null}async function fetchUser() {await new Promise((r) => setTimeout(r, 400))user.value = { id: 1, name: 'Tom' }}// 3) 可直接使用 watch 等組合式能力watch(count, (v) => {if (v > 10) console.log('count 很大了')})// 4) 返回對外可用的成員return { count, user, double, welcome, increment, reset, fetchUser }
})
3.2 組件中使用
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useCounterSetup } from '@/stores/counter-setup'const store = useCounterSetup()
const { count, double, welcome, user } = storeToRefs(store)function add() {store.increment()
}
</script><template><div class="card"><p>count: {{ count }}</p><p>double: {{ double }}</p><p>{{ welcome }}</p><button @click="add">+1</button><button @click="store.fetchUser()">拉取用戶</button><button @click="store.reset()">重置</button></div>
</template>
3.3 進階用法(復用邏輯 & 外部 composable)
// composables/usePersist.ts(示例)
import { watch } from 'vue'
export function usePersist<T extends object>(key: string, state: T) {watch(() => state,(val) => localStorage.setItem(key, JSON.stringify(val)),{ deep: true })
}// stores/profile.ts - 在 Setup Store 里直接用組合函數
import { defineStore } from 'pinia'
import { reactive, computed } from 'vue'
import { usePersist } from '@/composables/usePersist'export const useProfile = defineStore('profile', () => {const form = reactive({ name: '', age: 0 })const valid = computed(() => form.name.length > 0 && form.age > 0)usePersist('profile', form)return { form, valid }
})
優點小結(Setup Store)
- 靈活:原生 Composition API 能力全開(
ref/computed/watch/async
/自定義 composable)。 - 可復用:把復雜業務拆到多個 composable,再組合進 store。
- 更易測試:普通函數、無
this
語義,單元測試與類型推斷更直觀。
注意點
- 返回的成員必須顯式
return
,未返回的屬性對外不可見。 - 非可序列化的值(如函數、Map、類實例)放入 state 時需考慮 SSR/持久化的影響。
四、兩種寫法如何選?(場景對比)
維度 | Options Store(配置式) | Setup Store(組合式) |
---|---|---|
上手成本 | 低,結構固定,接近 Vuex | 中等,需要熟悉 Composition API |
代碼組織 | 三段式清晰:state/getters/actions | 任意組織,更靈活,也更考驗規范 |
邏輯復用 | 依賴抽出到獨立函數/插件 | 直接用 composable,自然拼裝 |
this 使用 | actions 里有 this,直達 state/getters | 無 this,純函數,易測試 |
TypeScript | 對 this 的類型推斷要注意 | 類型自然跟隨 ref/reactive |
DevTools/序列化 | 天然友好(state 可序列化) | 取決于返回的成員是否可序列化 |
典型場景 | 業務中小、邏輯清晰、團隊從 Vuex 遷移 | 中大型、復合邏輯、強復用/抽象需求 |
選擇建議
- 團隊以簡單業務/快速落地/從 Vuex 遷移為主 → 優先 Options Store。
- 團隊重組合式、強調復用與抽象,或需要在 store 內使用
watch
/ 自定義 composable → 選擇 Setup Store。 - 實際項目中可以混用:簡單模塊用 Options,復雜域(如表單域、編輯器域)用 Setup。
五、最佳實踐清單
- 永遠用
storeToRefs
解構:保持解構后仍具備響應性。 - 批量更新用
$patch
:一次性修改多個字段,減少觸發次數。 - 持久化:使用插件
@pinia/plugin-persistedstate
或自寫$subscribe
落地。 - SSR:每次請求都要創建新的
pinia
實例;避免向 state 放入不可序列化的“大對象”。 - 跨 Store 調用:在 action 內部調用另一個 store,按需引入,避免循環依賴。
- 命名規范:
stores/模塊名.ts
,導出useXxxStore
/useXxxSetup
等有語義的命名。
六、從 Vuex 遷移到 Pinia(速查)
Vuex | Pinia(Options) |
---|---|
state | state() { return { … } } |
getters | getters: { double: (s)=>s.count*2 } |
mutations | actions(同步/異步都放 actions) |
actions | 仍然是 actions |
mapState/mapGetters | 直接 storeToRefs(useStore()) 解構 |
遷移時最常見問題:丟失響應性。記得使用 storeToRefs
,或在模板中直接用 store.count
不解構。
七、常見坑位與排查
- 解構丟響應性:
const { count } = useStore()
? →const { count } = storeToRefs(useStore())
? - actions 用了箭頭函數導致 this 丟失(Options):改普通函數或顯式引用 store。
- 在 getters 里寫副作用:應移到 action 或 watch。
- 循環依賴:跨 store 調用時注意 import 時機,可在 action 內部按需調用另一個 store。
- SSR 水合失敗:state 內含不可序列化值;或客戶端初始 state 與服務端不一致。
八、結語
Options Store 強在“結構化與可維護”,Setup Store 勝在“靈活與復用”。
選型的關鍵不是“誰更先進”,而是“當前問題需要哪種力量”。理解兩種寫法的邊界與優勢,團隊協作會更順手、代碼也更可持續。