目标:把 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 胜在“灵活与复用”。
选型的关键不是“谁更先进”,而是“当前问题需要哪种力量”。理解两种写法的边界与优势,团队协作会更顺手、代码也更可持续。