Vue3 Pinia 从入门到精通
一、Pinia 简介与核心优势
Pinia 是 Vue 官方推荐的新一代状态管理库,由 Vuex 核心团队开发,旨在替代 Vuex 成为 Vue 3 生态的首选状态管理方案。Pinia 于 2021 年正式发布,目前已成为 Vue 3 项目的标配状态管理工具。
核心优势对比
| 特性 | Pinia | Vuex 3 (Vue 2) | Vuex 4 (Vue 3) |
|---|---|---|---|
| 包体积 | ~1KB (gzip) | ~10KB | ~5KB |
| TypeScript 支持 | 原生完美支持 | 需额外类型声明 | 部分支持 |
| 核心概念 | State, Getters, Actions | State, Getters, Mutations, Actions, Modules | 同左 |
| 模块化 | 天然支持多 Store,扁平化结构 | 需通过 Modules 嵌套,需命名空间 | 同左 |
| DevTools 支持 | 完整支持,含时间旅行 | 支持 | 支持 Vue 3,但体验一般 |
| 代码分割 | 自动支持,按需加载 | 需手动配置 | 需手动配置 |
与 Vuex 的核心差异
- 无 Mutations:Pinia 移除了 Vuex 中冗余的 Mutations,同步/异步操作统一由 Actions 处理,减少 40% 样板代码。
javascript
// Vuex
store.commit('increment', 1) // 同步
store.dispatch('fetchData') // 异步
// Pinia
store.increment(1) // 同步
store.fetchData() // 异步(无需额外分层)
-
更简洁的 API:Pinia 采用更直观的 API 设计,减少概念负担。
-
TypeScript 优先设计:所有 API 均类型安全,自动推导状态类型,无需手动声明。
-
扁平化模块结构:每个 Store 独立管理,避免命名空间冲突,模块间通信更直观。
二、Pinia 安装与基础配置
1. 安装 Pinia
bash
# 使用 npm
npm install pinia
# 使用 yarn
yarn add pinia
2. 初始化 Pinia
在入口文件(main.js 或 main.ts)中创建并挂载 Pinia 实例:
javascript
// 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) // 挂载 Pinia
app.mount('#app')
3. 创建第一个 Store
在 src/stores 目录下创建 Store 文件(推荐按功能模块划分):
javascript
// src/stores/counter.js
import { defineStore } from 'pinia'
// 定义 Store,第一个参数是唯一 ID(必填)
export const useCounterStore = defineStore('counter', {
// 状态(类似组件的 data)
state: () => ({
count: 0,
name: 'Eduardo'
}),
// 计算属性(类似组件的 computed)
getters: {
doubleCount: (state) => state.count * 2,
// 访问其他 getters
doubleCountPlusOne() {
return this.doubleCount + 1
}
},
// 方法(类似组件的 methods,支持同步/异步)
actions: {
increment() {
this.count++
},
async fetchData() {
// 模拟异步请求
const data = await new Promise(resolve =>
setTimeout(() => resolve(10), 1000)
)
this.count = data
}
}
})
4. 在组件中使用 Store
vue
<!-- CounterComponent.vue -->
<template>
<div>
<h2>Count: {{ counter.count }}</h2>
<p>Double Count: {{ counter.doubleCount }}</p>
<button @click="counter.increment">+1</button>
<button @click="counter.fetchData">Async Update</button>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter'
// 获取 Store 实例
const counter = useCounterStore()
</script>
三、核心概念详解
1. State(状态)
State 是 Store 的数据源,使用 state() 函数返回初始状态对象:
javascript
state: () => ({
user: null,
token: '',
permissions: []
})
修改状态:
-
直接修改(推荐):
store.count++ -
批量修改:
store.$patch({ count: 10, name: 'New Name' }) -
函数式修改:
javascriptstore.$patch(state => { state.items.push({ id: 1, name: 'New Item' }) state.total++ })
重置状态 :store.$reset()
2. Getters(计算属性)
Getters 用于派生出基于 State 的新状态,结果会自动缓存:
javascript
getters: {
// 基础用法
filteredPermissions: (state) => state.permissions.filter(p => p.enabled),
// 带参数的 getter
hasPermission: (state) => (permission) =>
state.permissions.includes(permission),
// 访问其他 getter
permissionCount(state, getters) {
return getters.filteredPermissions.length
}
}
3. Actions(方法)
Actions 用于封装业务逻辑,支持同步和异步操作:
javascript
actions: {
// 同步 action
setUser(user) {
this.user = user
this.token = user.token
},
// 异步 action
async login(credentials) {
this.loading = true
try {
const response = await api.login(credentials)
this.setUser(response.data) // 调用其他 action
return response.data
} catch (error) {
this.error = error.message
throw error // 允许组件捕获错误
} finally {
this.loading = false
}
}
}
四、高级用法
1. Setup 函数式写法(推荐)
Pinia 支持更灵活的组合式 API 风格定义 Store:
javascript
// src/stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// 状态(对应 state)
const user = ref(null)
const token = ref('')
// 计算属性(对应 getters)
const isLoggedIn = computed(() => !!token.value)
// 方法(对应 actions)
function setUser(userData) {
user.value = userData
token.value = userData.token
}
async function login(credentials) {
const response = await api.login(credentials)
setUser(response.data)
}
// 暴露状态和方法
return { user, token, isLoggedIn, setUser, login }
})
2. 模块化管理
Pinia 推荐按业务域拆分多个独立 Store:
src/
└── stores/
├── user.js # 用户模块
├── cart.js # 购物车模块
└── product.js # 产品模块
跨 Store 通信:
javascript
// stores/cart.js
import { defineStore } from 'pinia'
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', {
actions: {
checkout() {
const userStore = useUserStore()
if (!userStore.isLoggedIn) {
throw new Error('请先登录')
}
// 执行结账逻辑
}
}
})
3. 状态持久化
使用 pinia-plugin-persistedstate 插件实现状态持久化:
bash
npm install pinia-plugin-persistedstate
javascript
// main.js
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate) // 注册插件
基本用法:
javascript
// stores/user.js
export const useUserStore = defineStore('user', {
state: () => ({ token: '', userInfo: {} }),
persist: true // 开启持久化(默认存储到 localStorage)
})
高级配置:
javascript
persist: {
key: 'user-store', // 存储键名
storage: sessionStorage, // 使用 sessionStorage
paths: ['token'], // 只持久化 token 字段
// 自定义序列化(如加密)
serializer: {
serialize: (data) => encrypt(JSON.stringify(data)),
deserialize: (data) => JSON.parse(decrypt(data))
}
}
4. 响应式处理与解构
直接解构 Store 会丢失响应性,需使用 storeToRefs:
javascript
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
const counter = useCounterStore()
// 错误:解构后失去响应性
const { count, doubleCount } = counter // ❌
// 正确:使用 storeToRefs 保持响应性
const { count, doubleCount } = storeToRefs(counter) // ✅
// Actions 可直接解构(它们是绑定到 Store 的函数)
const { increment } = counter // ✅
</script>
五、TypeScript 深度集成
Pinia 为 TypeScript 提供一流支持,所有状态和方法自动推导类型:
1. 类型化 State
typescript
// stores/user.ts
import { defineStore } from 'pinia'
// 定义用户类型
interface User {
id: number
name: string
role: 'admin' | 'editor' | 'viewer'
}
export const useUserStore = defineStore('user', {
state: (): {
currentUser: User | null,
permissions: string[]
} => ({
currentUser: null,
permissions: []
}),
getters: {
isAdmin: (state): boolean =>
state.currentUser?.role === 'admin' || false
},
actions: {
setUser(user: User) { // 参数类型约束
this.currentUser = user
}
}
})
2. 组合式 Store 类型化
typescript
// stores/product.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
interface Product {
id: number
name: string
price: number
}
export const useProductStore = defineStore('product', () => {
const products = ref<Product[]>([])
const filteredProducts = computed(() =>
products.value.filter(p => p.price < 100)
)
function addProduct(product: Product) { // 类型约束
products.value.push(product)
}
return { products, filteredProducts, addProduct }
})
六、与 Vue Router 结合使用
1. 在 Store 中使用路由
javascript
// stores/auth.js
import { defineStore } from 'pinia'
import { useRouter } from 'vue-router'
export const useAuthStore = defineStore('auth', {
actions: {
login(credentials) {
const router = useRouter()
// 登录逻辑...
router.push('/dashboard') // 登录后跳转
},
logout() {
const router = useRouter()
// 登出逻辑...
router.push('/login') // 登出后跳转
}
}
})
2. 路由守卫中使用 Store
javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { useAuthStore } from '@/stores/auth'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/dashboard',
component: Dashboard,
meta: { requiresAuth: true }
}
]
})
router.beforeEach((to, from, next) => {
const authStore = useAuthStore()
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
next('/login') // 未登录则重定向到登录页
} else {
next()
}
})
七、最佳实践与性能优化
1. 状态设计原则
-
扁平化状态:避免深层嵌套,便于访问和持久化
javascript// 不推荐 state: () => ({ user: { info: { name: '', age: 0 } } }) // 推荐 state: () => ({ userName: '', userAge: 0 }) -
最小化全局状态:仅将跨组件共享的数据放入 Store,局部状态留在组件内。
2. 性能优化技巧
-
批量更新状态 :使用
$patch减少响应式触发次数javascript// 优化前(触发 2 次更新) store.name = 'New Name' store.count = 10 // 优化后(仅触发 1 次更新) store.$patch({ name: 'New Name', count: 10 }) -
缓存计算结果:复杂计算逻辑使用 Getters,利用其缓存特性
-
按需加载大型 Store:
javascript// 路由懒加载时加载 Store const ProductDetail = () => import('@/views/ProductDetail.vue') // 在组件中动态导入 Store const useLargeStore = await import('@/stores/largeStore').then(m => m.useLargeStore())
3. 项目结构推荐
src/
├── stores/ # Store 目录
│ ├── index.ts # 统一导出所有 Store
│ ├── user.ts # 用户模块
│ ├── cart.ts # 购物车模块
│ └── modules/ # 子模块(如需)
│ └── product.ts # 产品子模块
├── router/ # 路由配置
├── views/ # 页面组件
└── components/ # 通用组件
八、常见问题解决方案
1. "getActivePinia was called with no active Pinia" 错误
原因 :在 Pinia 实例创建前调用了 useStore()。
解决方案 :确保 app.use(pinia) 在任何 useStore() 调用前执行。
2. 持久化状态未生效
检查清单:
- 是否正确注册
pinia-plugin-persistedstate插件 - 确认浏览器隐私模式未禁用存储
- 复杂对象(如 Date)需自定义序列化
3. TypeScript 类型推断失败
解决方案:
- 为 State 显式声明接口
- 使用组合式 Store 时,确保所有状态都通过
ref/reactive定义
4. 开发环境热更新失效
解决方案:
javascript
// 在 Store 文件末尾添加
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useCounterStore, import.meta.hot))
}
九、总结与迁移指南
Pinia 作为 Vue 3 官方推荐的状态管理库,凭借其简洁的 API、出色的 TypeScript 支持和灵活的模块化设计,已成为现代 Vue 项目的首选方案。对于现有 Vuex 项目,可按以下步骤迁移:
- 安装 Pinia 并创建基础 Store
- 逐步迁移:按模块将 Vuex Store 转换为 Pinia Store
- 替换 Mutations :将
commit调用改为直接修改状态 - 优化模块化:将嵌套 Modules 拆分为独立 Pinia Store
- 启用持久化 :替换
vuex-persistedstate为 Pinia 持久化插件
通过本文的学习,你已掌握 Pinia 的核心概念、高级用法和最佳实践。Pinia 的设计哲学是"让状态管理像使用普通变量一样简单",这种简洁性和强大功能的平衡,使其成为 Vue 3 生态中不可或缺的一部分。