《Pinia 从入门到精通》Vue 3 官方状态管理 – 基础入门篇
《Pinia 从入门到精通》Vue 3 官方状态管理 – 进阶使用篇
《Pinia 从入门到精通》Vue 3 官方状态管理 – 插件扩展篇
Store 的模块化设计
随着项目规模扩大,单一 Store 会迅速膨胀,导致维护困难。
我们将从实际项目结构出发,讲解如何构建多模块 Store,支持清晰组织、职责分离和可维护性。
Pinia 提供了天然模块化的设计,每一个 Store 就是一个模块,天生支持按需加载、组合调用。
4.1 多模块结构设计
✅ 推荐目录结构(中大型项目)
src/
├── stores/
│ ├── index.ts # 导出所有 store(可选)
│ ├── user.ts # 用户模块
│ ├── auth.ts # 登录认证模块
│ ├── cart.ts # 购物车模块
│ └── product.ts # 商品模块
每个模块都用 defineStore
定义自己的状态、逻辑、计算属性,保持内聚。
4.2 定义独立模块 Store
// stores/user.ts
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
name: '',
email: ''
}),
actions: {
setUser(name: string, email: string) {
this.name = name
this.email = email
}
}
})
// stores/cart.ts
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({
items: [] as { id: number; name: string; qty: number }[]
}),
getters: {
totalItems: (state) => state.items.reduce((sum, item) => sum + item.qty, 0)
},
actions: {
addItem(item: { id: number; name: string; qty: number }) {
this.items.push(item)
}
}
})
4.3 在组件中组合多个 Store
<script setup lang="ts">
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'
const userStore = useUserStore()
const cartStore = useCartStore()
function checkout() {
console.log(`${userStore.name} 正在购买 ${cartStore.totalItems} 件商品`)
}
</script>
4.4 跨模块调用(解耦调用)
Pinia 中不同 Store 可相互独立调用,而无需手动注入依赖:
// stores/order.ts
import { defineStore } from 'pinia'
import { useUserStore } from './user'
export const useOrderStore = defineStore('order', {
actions: {
submitOrder() {
const userStore = useUserStore()
console.log(`下单用户:${userStore.name}`)
// ...业务逻辑
}
}
})
注意:调用其他 Store 必须在 action 中动态获取,而不能在 Store 顶层直接引入(避免依赖环问题)。
4.5 模块化 Store 的命名约定
内容 | 命名规则 | 示例 |
---|---|---|
Store 函数 | useXxxStore |
useUserStore |
文件名 | 模块名小写 | user.ts |
Store ID | 与函数名一致的小写形式 | 'user' |
命名空间 | 使用 id 区分,无需嵌套定义 |
所有 Store 自动隔离 |
4.6 所有 Store 统一导出(可选)
你可以在 stores/index.ts
中统一导出,方便使用:
// stores/index.ts
export * from './user'
export * from './cart'
export * from './order'
// 使用
import { useUserStore, useCartStore } from '@/stores'
✅ 小结
模块化是构建大型 Vue 应用的核心策略,Pinia 以其函数式 Store 和天然隔离的 Store ID 设计,使模块化变得:
- 更加清晰(每个模块职责单一)
- 更易维护(按需加载,互不干扰)
- 更易测试(每个 Store 独立可测试)
类型系统集成(TypeScript 支持)
Pinia 天生支持 TypeScript,基于其函数式 API 和明确的声明结构,使得类型推导更加自然、精准。
5.1 Store 的类型推导基础
最基础的 defineStore
形式在 TS 中已经具备类型提示能力:
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Counter Module'
}),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
}
}
})
此时使用该 Store 时会自动获得类型提示:
const store = useCounterStore()
store.count // number ✅
store.increment() // 自动补全 ✅
5.2 自定义类型(推荐)
虽然大多数场景下可自动推导,但我们仍推荐使用接口显式定义 state
、getters
、actions
类型以获得更强可维护性。
✅ 定义接口
// types/store.d.ts
export interface CounterState {
count: number
name: string
}
export interface CounterGetters {
doubleCount(state: CounterState): number
}
export interface CounterActions {
increment(): void
}
✅ 使用泛型定义 Store
import { defineStore, StoreDefinition } from 'pinia'
import type { CounterState, CounterActions, CounterGetters } from '@/types/store'
export const useCounterStore: StoreDefinition<'counter', CounterState, CounterGetters, CounterActions> = defineStore('counter', {
state: (): CounterState => ({
count: 0,
name: 'Counter Store'
}),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
}
}
})
5.3 Setup Store(组合式写法)中的类型支持
对于复杂逻辑,我们可以使用组合式写法,即 setup
形式的 Store,TS 类型控制更灵活。
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
const name = ref('Alice')
const age = ref(25)
const summary = computed(() => `${name.value}(${age.value}岁)`)
function setName(newName: string) {
name.value = newName
}
return {
name,
age,
summary,
setName
}
})
此种写法中,Pinia 能自动推导返回值类型,无需额外泛型,只要你在返回时显式写出 ref
/ computed
。
5.4 类型提示与 IDE 自动补全效果
- State 属性:可识别为
ref<number>
/ref<string>
等 - Getter 属性:识别为
ComputedRef
- Action 方法:自动推导参数和返回值类型
举个例子:
const userStore = useUserStore()
userStore.age.value // number ✅
userStore.summary.value // string ✅
userStore.setName('Bob') // ✅
5.5 类型约束下的好处
特性 | 说明 |
---|---|
自动补全 | 所有属性、方法可自动提示,无需手动查找 |
静态校验 | 错误属性/参数在编译期即可发现,避免运行时异常 |
支持重构 | 改名、移动属性时 IDE 可自动跟踪更新引用 |
接口复用 | 同一份接口可复用在组件、接口、后端通讯中 |
✅ 小结
Pinia 在 TypeScript 项目中具备一流的类型体验:
- 推荐用接口明确 State / Action / Getter 结构
- 可选使用组合式写法提升灵活性与组合能力
- 强类型推导贯穿编写、使用、调试各个环节
持久化存储与插件机制
本节将深入讲解如何使用 Pinia 插件 实现 Store 状态的持久化存储,以及自定义扩展 Store 功能。
在实际项目中,我们常常需要:
- 保持用户登录信息,即使刷新页面也不丢失
- 记住用户的主题偏好、语言设置
- 在多个 Tab 页面之间共享状态
Pinia 原生支持插件机制,非常适合用于这些扩展场景。
6.1 什么是 Pinia 插件?
插件是一个函数,在每个 Store 实例创建时执行。你可以通过插件:
- 添加全局状态
- 劫持/监听 Store 生命周期
- 自动同步状态到 localStorage / sessionStorage
- 注入外部依赖(如 Axios)
6.2 状态持久化插件:pinia-plugin-persistedstate
最常用的第三方插件是 pinia-plugin-persistedstate
,可自动将状态持久化到本地存储。
✅ 安装
npm install pinia-plugin-persistedstate
✅ 注册插件
// main.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
6.3 使用持久化配置
只需在 defineStore
中增加 persist
配置:
// stores/user.ts
export const useUserStore = defineStore('user', {
state: () => ({
token: '',
theme: 'light'
}),
persist: true // 默认使用 localStorage
})
刷新后依然保留 token 和主题偏好。
6.4 自定义持久化策略
persist: {
enabled: true,
strategies: [
{
key: 'user-token',
storage: sessionStorage,
paths: ['token'] // 只存 token
}
]
}
key
:本地存储的键名storage
:可选localStorage
或sessionStorage
paths
:只持久化指定字段
6.5 多模块下持久化组合
每个模块 Store 都可独立配置 persist
,互不影响:
// user.ts => 存 token 到 sessionStorage
persist: {
storage: sessionStorage,
paths: ['token']
}
// settings.ts => 存 theme 到 localStorage
persist: {
storage: localStorage,
paths: ['theme']
}
6.6 自定义插件机制(进阶)
你可以自定义自己的插件来扩展 Store 功能:
// plugins/logger.ts
import type { PiniaPluginContext } from 'pinia'
export function loggerPlugin({ store }: PiniaPluginContext) {
store.$subscribe((mutation, state) => {
console.log(`[${mutation.storeId}]`, mutation.type, mutation.events)
})
}
注册插件:
pinia.use(loggerPlugin)
这样每次状态改变都会打印日志。
6.7 生命周期钩子:$subscribe
& $onAction
Pinia 提供两种原生生命周期钩子:
1. $subscribe
—— 监听状态变化
const userStore = useUserStore()
userStore.$subscribe((mutation, state) => {
console.log('状态发生变化:', mutation, state)
})
2. $onAction
—— 监听 Action 执行
userStore.$onAction(({ name, args, after, onError }) => {
console.log(`Action 被调用: ${name}`, args)
after(() => console.log(`${name} 执行成功`))
onError((err) => console.error(`${name} 报错`, err))
})
非常适合做埋点、错误日志等逻辑。
✅ 小结
Pinia 插件机制极为强大和灵活:
功能类型 | 工具 | 用途描述 |
---|---|---|
本地持久化 | pinia-plugin-persistedstate | 自动同步状态到 Storage |
状态监听 | $subscribe |
跟踪 State 的变化 |
行为监听 | $onAction |
跟踪 Action 的执行情况 |
自定义扩展 | 插件函数 | 注入工具、处理副作用、封装逻辑等 |
最佳实践总结与项目结构规范化设计
将基于前面内容,系统梳理一套企业级 Pinia 状态管理的最佳实践,从模块设计、命名规范、状态解耦、持久化、类型安全等多个维度,构建一个清晰、稳定、可维护、易扩展的 Store 架构体系。
7.1 推荐项目结构
src/
├── stores/ # 所有 Pinia Store 模块
│ ├── user.ts # 用户相关状态
│ ├── auth.ts # 权限与登录认证
│ ├── ui.ts # UI 状态(如 sidebar)
│ ├── settings.ts # 全局设置项
│ └── index.ts # 自动导出所有 Store
├── types/ # Store 类型定义
│ └── store.d.ts
├── plugins/ # 自定义插件(如 router 注入、日志等)
├── composables/ # 组合逻辑封装,可配合 Store 使用
7.2 Store 命名规范
类型 | 命名建议 | 示例 |
---|---|---|
Store 名称 | useXxxStore | useUserStore |
ID(storeId) | 模块名小写 | user , auth , ui |
文件名 | 模块名小写 | user.ts , auth.ts |
命名统一,利于团队协作和自动化生成。
7.3 状态设计原则
- 一个模块职责单一,避免巨型 Store
- 状态最小化:只存 UI 需要的状态,不要存派生数据(放 getters)
- 与组件无关的数据放 Store,临时数据放组件
- 使用
ref
,computed
,watch
配合使用 Store 提高响应性控制
7.4 Store 类型系统统一
统一定义 Store 的 state
, getters
, actions
类型接口:
// types/store.d.ts
export interface UserState { name: string; age: number }
export interface UserActions { login(): void; logout(): void }
export interface UserGetters { isAdult: boolean }
结合 StoreDefinition
:
export const useUserStore: StoreDefinition<'user', UserState, UserGetters, UserActions> =
defineStore('user', {
state: (): UserState => ({ ... }),
...
})
优点:
- 强类型保障
- 便于重构
- 编辑器智能提示清晰
7.5 持久化策略标准化
- token、用户信息 → 存到
sessionStorage
(浏览器关闭清空) - 用户偏好、主题设置 → 存到
localStorage
(长期保存) - 配置统一封装成策略常量:
const localPersist = {
storage: localStorage,
paths: ['theme', 'language']
}
7.6 自动化导出 Store
// stores/index.ts
export * from './user'
export * from './auth'
export * from './settings'
支持模块自动导入:
import { useUserStore, useAuthStore } from '@/stores'
7.7 组合逻辑封装:useXXXLogic
业务逻辑不要堆在组件里,应该封装成组合逻辑:
// composables/useLogin.ts
export function useLogin() {
const userStore = useUserStore()
const doLogin = async (formData) => {
await userStore.login(formData)
router.push('/dashboard')
}
return { doLogin }
}
- 提高复用性
- 分离 UI 与业务逻辑
- 组件更轻盈可测试
✅ 实战应用:多页面应用的 Store 模型设计范式
模块名称 | 典型状态字段 | 持久化 | 常驻内存 | 是否解耦 UI |
---|---|---|---|---|
user | token , info |
✅ | ✅ | ✅ |
ui | sidebar , theme |
✅ | ✅ | ✅ |
auth | roles , routes |
❌ | ✅ | ✅ |
search | keyword , filters |
❌ | ❌ | ❌(页面级) |
✅ 统一 Store 模板(可直接复制)
// stores/xxx.ts
import { defineStore } from 'pinia'
import type { XxxState } from '@/types/store'
export const useXxxStore = defineStore('xxx', {
state: (): XxxState => ({ ... }),
getters: {
...
},
actions: {
...
},
persist: {
enabled: true,
strategies: [...]
}
})
✅ 结语:Pinia 最佳实践总览图
+-------------------------------+
| 模块划分清晰 |
| ↓ |
| 单一职责 |
| ↓ |
| 类型定义接口统一 |
| ↓ |
| Plugin 封装 & 逻辑抽离 |
| ↓ |
| 状态最小化 & 可持久化 |
| ↓ |
| 与 Router / API / UI 解耦 |
+-------------------------------+
Pinia 的设计理念是简单、透明、类型友好。配合组合式 API,它不仅可以替代 Vuex,还能帮助我们构建更现代、更可控、更高效的前端架构。