Vue 3 中 Pinia 完全指南
本文档从零开始讲解 Vue 3 的状态管理库 Pinia:什么是 Pinia、为什么需要它、如何安装与注册、如何定义 store(选项式与组合式两种写法)、如何在组件里使用、state/getters/actions 的用法,以及常见场景示例,适合新手系统学习。
一、什么是 Pinia?为什么需要它?
1.1 状态管理是什么?
页面上很多数据会被多个组件共用,例如:当前登录用户、购物车列表、主题(亮/暗)。
若只用 props 和 emit,就要一层层往下传、一层层往上发,非常麻烦。
状态管理就是:把这类“全局/共享”的数据放在一个统一的地方(store),谁需要谁就去读/改,不用层层传递。
1.2 Pinia 是什么?
Pinia 是 Vue 官方推荐的状态管理库(Vue 3 时代用来替代 Vuex)。
你可以把“共享状态”放在 store 里,在任意组件里引入 store,直接读写 state、调用 actions,非常直观。
1.3 Pinia 的特点(为什么选它)
- API 简单:没有 mutations,直接改 state 或通过 actions 改。
- 对 TypeScript 友好。
- 模块化:每个 store 独立定义,按需引入。
- 与 Vue 3 组合式 API 契合:可以用 ref、computed 的写法定义 store。
下面从安装开始,到定义 store、在组件里使用,一步步说明。
二、安装与注册
2.1 安装
在项目根目录执行:
npm install pinia
或:
yarn add pinia
pnpm add pinia
2.2 在 main.js 里注册
创建 Pinia 实例,并用 app.use(pinia) 挂到 Vue 应用上,这样整个应用都能用 store。
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')
之后就可以在项目里 defineStore 定义 store,并在组件里 useXxxStore() 使用。
三、定义第一个 Store
3.1 defineStore 与 store 的“名字”
用 defineStore 定义一个 store,需要传一个唯一 id(字符串)和配置(选项式或组合式)。
习惯上 id 和文件名一致,如 stores/counter.js 里定义 ‘counter’。
3.2 选项式写法(state + getters + actions)
和 Vue 的选项式 API 类似:state 是数据,getters 是派生数据(像计算属性),actions 是方法(可异步)。
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
getters: {
double: (state) => state.count * 2
},
actions: {
increment() {
this.count++
},
add(n) {
this.count += n
}
}
})
- state 必须是函数,返回一个对象(和组件的 data 一样)。
- getters 里用 state 做只读计算;若需要传参,可返回一个函数(见后文)。
- actions 里用 this 访问 state、getters、其他 actions。
3.3 在组件里使用(选项式 store)
在组件里 import 并调用 useCounterStore(),得到 store 实例;然后可以:
- store.count 读 state;
- store.double 读 getter;
- store.increment() 调 action。
<template>
<div>
<p>{{ store.count }}</p>
<p>双倍:{{ store.double }}</p>
<button @click="store.increment()">+1</button>
<button @click="store.add(10)">+10</button>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
</script>
- useCounterStore() 是 Pinia 根据 defineStore 自动生成的;每次调用返回同一个 store 实例(单例)。
- 直接改 store.count 也可以触发更新(Pinia 不强制通过 action 改),但复杂逻辑建议放在 actions 里。
四、State(状态)
4.1 state 必须是函数
state 要写成函数,返回初始对象,这样每个 store 实例不会共享同一引用:
state: () => ({
count: 0,
list: []
})
4.2 直接改 state
在组件里可以直接改 store.xxx:
const store = useCounterStore()
store.count++
store.list.push(1)
4.3 一次性改多个:$patch
用 $patch 传一个对象,一次性改多个字段:
store.$patch({
count: 10,
name: '小明'
})
或传一个函数,在函数里改 state:
store.$patch((state) => {
state.count++
state.list.push(1)
})
4.4 恢复初始:$reset
$reset() 会把 state 恢复成 defineStore 里 state() 返回的初始值(仅选项式 store 支持):
store.$reset()
五、Getters(相当于计算属性)
5.1 基本写法
getters 接收 state 作为第一个参数,可以访问 this(即当前 store),返回派生数据;在组件里当只读用,不用括号(除非返回的是函数)。
getters: {
double: (state) => state.count * 2,
triple() {
return this.count * 3
}
}
5.2 使用其它 getter
在 getter 里可以用 this.其他Getter:
getters: {
double: (state) => state.count * 2,
quadruple() {
return this.double * 2
}
}
5.3 带参数的 getter(返回函数)
需要“传参”时,让 getter 返回一个函数,在组件里调用这个函数并传参:
getters: {
getItemById: (state) => (id) => {
return state.list.find((item) => item.id === id)
}
}
组件里:store.getItemById(1)。
六、Actions(方法,可异步)
6.1 基本写法
actions 里用 this 访问 state、getters、其他 actions;可以是 async,适合请求接口。
actions: {
async fetchUser() {
this.loading = true
try {
const res = await fetch('/api/user')
this.user = await res.json()
} finally {
this.loading = false
}
}
}
6.2 调用其它 action
actions: {
async fetchAndReset() {
await this.fetchUser()
this.someFlag = false
}
}
七、组合式写法(setup 风格)的 Store
defineStore 的第二种写法:第二个参数传一个 setup 函数,函数里用 ref、computed、function 定义 state、getters、actions,最后 return 出去。
和组件的 风格一致。
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() {
count.value++
}
function add(n) {
count.value += n
}
return { count, double, increment, add }
})
- ref 相当于 state;computed 相当于 getters;function 相当于 actions。
- 在组件里用法一样:useCounterStore() 后 store.count、store.double、store.increment()。
- setup 风格的 store 没有 $reset(没有“选项式 state 的初始值”概念),需要自己写一个 action 把各 ref 设回初值。
八、在组件里保持响应式:storeToRefs
若在组件里解构 store:
const store = useCounterStore()
const { count, double } = store
count、double 会失去响应式(变成普通值/引用)。
要用 storeToRefs 解构 state 和 getters,解构出来的才是 ref,保持响应式;actions 直接解构即可(函数不需要响应式)。
import { storeToRefs } from 'pinia'
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
const { count, double } = storeToRefs(store)
const { increment } = store
模板里用 count、double、increment 即可。
不解构、一直用 store.count、store.double 也可以,不会丢响应式。
九、多个 Store 与在 Store 里用另一个 Store
9.1 定义多个 store
按功能拆成多个文件,例如 stores/user.js、stores/cart.js,各自 defineStore 不同 id。
9.2 在一个 store 里用另一个 store
在 action(或 setup 风格里的函数)里调用 useXxxStore() 即可:
// stores/cart.js
import { defineStore } from 'pinia'
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', {
state: () => ({ items: [] }),
actions: {
checkout() {
const userStore = useUserStore()
if (!userStore.isLoggedIn) {
alert('请先登录')
return
}
// ...
}
}
})
注意:useUserStore() 要在 action 或 setup 函数执行时调用,不要在最顶层调用(否则可能 pinia 还未安装)。
十、完整示例
10.1 用户 Store(登录状态、用户信息)
// stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
user: null,
token: ''
}),
getters: {
isLoggedIn: (state) => !!state.token,
userName: (state) => state.user?.name ?? ''
},
actions: {
setToken(token) {
this.token = token
},
setUser(user) {
this.user = user
},
logout() {
this.user = null
this.token = ''
}
}
})
组件里:const userStore = useUserStore(),用 userStore.isLoggedIn、userStore.logout() 等。
10.2 购物车 Store(列表、总价、增删)
// stores/cart.js
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({
items: []
}),
getters: {
total: (state) => state.items.reduce((sum, item) => sum + item.price * item.qty, 0),
count: (state) => state.items.reduce((sum, item) => sum + item.qty, 0)
},
actions: {
add(item) {
const found = this.items.find((i) => i.id === item.id)
if (found) found.qty++
else this.items.push({ ...item, qty: 1 })
},
remove(id) {
this.items = this.items.filter((i) => i.id !== id)
}
}
})
十一、持久化(可选)
若希望刷新页面后 state 不丢,可以用 pinia-plugin-persistedstate 等插件,把指定 store 的 state 存到 localStorage。
安装后:
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
在 defineStore 里加 persist: true 或 persist: { key: ‘xxx’, storage: localStorage } 等配置(具体看插件文档)。
新手可先不用,先把 store 的读写在内存里练熟。
十二、常见问题与注意点
12.1 解构 store 要用 storeToRefs
直接 const { count } = store 会丢响应式;对 state/getters 用 storeToRefs(store) 再解构,actions 可直接解构。
12.2 在 store 里用其它 store 要在 action/setup 里调 useXxxStore
不要在 store 文件的最顶层调用 useUserStore()(可能 pinia 未挂载);在 action 或 setup 函数里调用就没问题。
12.3 state 必须是函数
state: () => ({ … }),不要写成 state: { … },否则多个地方用同一个 store 时可能共享同一引用。
十三、Pinia 速查表
| 内容 | 说明 |
|---|---|
| 定义 store | defineStore(‘id’, { state, getters, actions }) 或 defineStore(‘id’, () => { … }) |
| 使用 store | const store = useXxxStore() |
| 读 state | store.xxx |
| 改 state | 直接 store.xxx = 值 或 store.$patch({ … }) |
| getters | store.getterName,带参时 store.getterName(参数) |
| actions | store.actionName() |
| 解构保持响应式 | storeToRefs(store) 解构 state/getters,actions 直接解构 |
| 恢复初始 state | 选项式 store:store.$reset() |
十四、学习建议
- 先会选项式一个 store:state + getters + actions,在组件里 useXxxStore() 读和调。
- 需要解构时用 storeToRefs;多个 store 时在 action 里 useOtherStore()。
- 再试组合式写法(setup 风格),和 ref/computed/function 对应起来。
- 持久化、插件等用到再查文档即可。
把本文档里的 counter、user、cart 三个示例在项目里敲一遍,会掌握得更牢。祝你学习顺利。