学完这篇你能得到什么:你会在一个 Vue 3 项目里从零跑通 Pinia,写出用户登录和购物车两个 store,并且清楚从 Vuex 迁移时哪些写法必须改。
第一步:先搞清楚两者差在哪
Pinia 是 Vue 官方现在推荐的 store 方案,Vuex 在 Vue 3 时代基本进入维护状态。核心区别只有四条:
- 没有 `mutations`,只有 `state`、`getters`、`actions`,改 state 直接赋值或用 action。
- 没有 `modules` 嵌套和 `namespaced`,一个 store 就是一个文件,天然扁平。
- TypeScript 类型推断几乎不用手写,`state` 里的字段自动推导。
- 体积极小,API 少,学习成本比 Vuex 低一档。
搞清楚这四条,后面的代码你基本能猜到怎么写。
第二步:安装并注册
npm install pinia
然后在入口文件注册:
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia()) // 先装 Pinia
app.use(router) // 再装路由
app.mount('#app')
注意:`app.use(createPinia())` 必须早于任何 `useXxxStore()` 的调用。如果你在路由守卫里用了 store,务必把 Pinia 写在 `use(router)` 之前,否则会报 `getActivePinia was called with no active Pinia`。
注意:还在 Vue 2 项目上请装 Pinia 2.x;Pinia 3.x 只支持 Vue 3。
第三步:定义第一个 Store
在 `src/stores/` 下建文件,推荐用选项式写法,结构最清晰:
// src/stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
token: '',
profile: null,
cart: []
}),
getters: {
isLogin: (state) => !!state.token,
cartCount: (state) => state.cart.reduce((n, i) => n + i.qty, 0),
// 需要用到其他 getter 时用普通函数 + this,不要用箭头函数
cartTotal() {
return this.cart.reduce((s, i) => s + i.price * i.qty, 0)
}
},
actions: {
async login(username, password) {
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
})
const data = await res.json()
this.token = data.token
this.profile = data.user
},
logout() {
this.$reset() // 选项式 store 自带的复位方法
}
}
})
喜欢组合式 API 的话可以这样写,效果等价:
export const useUserStore = defineStore('user', () => {
const token = ref('')
const isLogin = computed(() => !!token.value)
async function login(username, password) { /* 同上 */ }
return { token, isLogin, login }
})
注意:组合式写法没有 `$reset`,需要自己写一个 `reset()` 函数手动还原,并在 `return` 里导出。
第四步:在组件里读和改
<script setup>
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const { isLogin, cartCount } = storeToRefs(userStore) // state / getter 用这个拆
const { login, logout } = userStore // action 直接解构没问题
// 改 state:直接赋值
userStore.token = 'abc123'
// 批量改:$patch
userStore.$patch({ token: 'abc', profile: { name: '阿乐' } })
</script>
<template>
<p v-if="isLogin">购物车共 {{ cartCount }} 件</p>
</template>
注意:`const { token } = userStore` 会丢掉响应性,模板不会更新。拆 state 和 getter 一律用 `storeToRefs`;只有 action 可以直接解构,因为它是绑定过的函数。
第五步:跨 store 调用与持久化
在 action 里可以随意调用别的 store,不需要像 Vuex 那样 `dispatch('模块名/方法')`:
import { useCartStore } from './cart'
actions: {
async checkout() {
const cart = useCartStore()
await cart.submit(this.token)
}
}
想刷新页面不丢登录态,装个持久化插件:
npm i pinia-plugin-persistedstate
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
pinia.use(piniaPluginPersistedstate)
// store 里加一行
persist: true
组合式写法把 `{ persist: true }` 放在 `defineStore` 的第三个参数。
注意:`persist: true` 会把整个 store 存进 localStorage,token 会明文可见。真上生产建议写 `persist: { pick: ['token'] }` 只挑需要的字段。
第六步:从 Vuex 迁移对照
| Vuex | Pinia |
|---|
| `mutations` | 删除,直接赋值或写进 action |
| `commit('xx')` | 直接调用 store 上的方法 |
| `dispatch('xx')` | 直接调用 action |
| `modules` + `namespaced` | 一个文件一个 `defineStore` |
| `mapState` / `mapGetters` | `storeToRefs(store)` |
| `mapActions` | 直接解构 action |
迁移时最容易漏的是 `commit`:原来的 commit 全部要删掉,同步逻辑直接写在原 mutation 里就成了 action 的一部分。
注意:Pinia 的 store id 必须全局唯一(`defineStore('user', ...)` 的 `'user'`),两个文件写同一个 id 会互相覆盖,调试时很难发现。
小结
- Pinia 是 Vue 官方推荐的 store 方案,没有 mutations、没有 modules,一个文件一个 store。
- 注册顺序不能错:`app.use(createPinia())` 要在路由和任何 `useStore()` 之前。
- 拆 state/getter 用 `storeToRefs`,拆 action 直接解构即可。
- `$reset` 只有选项式 store 自带,组合式写法要手写 reset。
- 持久化用 `pinia-plugin-persistedstate`,但别整包存,token 之类按需 `pick`。