Appearance
Pinia 状态管理
本文介绍Pinia的核心概念、使用方法和最佳实践。Pinia是Vue官方推荐的新一代状态管理库,替代了Vuex,提供了更简洁的API、更好的TypeScript支持和更灵活的使用方式。
什么是Pinia?
Pinia是Vue.js的轻量级状态管理库,由Vue核心团队成员开发,是Vuex的继任者。它最初是为了探索Vuex的下一个版本而创建的,最终被官方推荐为Vue 3项目的状态管理解决方案。
Pinia的优势
- 简洁的API:没有Mutation,没有模块嵌套,使用更直观
- 完整的TypeScript支持:类型推断更加完善,开发体验更好
- 轻量级:体积约1KB,性能优秀
- 灵活的API:支持组合式API和选项式API
- 开发工具支持:与Vue Devtools集成良好
- 服务器端渲染支持:兼容SSR场景
与Vuex的对比
| 特性 | Pinia | Vuex 3 | Vuex 4 |
|---|---|---|---|
| 支持Vue 2 | ❌ | ✅ | ❌ |
| 支持Vue 3 | ✅ | ❌ | ✅ |
| TypeScript支持 | ✅ | ❌ | ⚠️ 有限支持 |
| Mutation | ❌ | ✅ | ✅ |
| Action | ✅ | ✅ | ✅ |
| Getter | ✅ | ✅ | ✅ |
| 模块 | ✅ | ✅ | ✅ |
| 模块嵌套 | ❌ | ✅ | ✅ |
| 命名空间 | ✅ (默认) | ❌ (需手动开启) | ❌ (需手动开启) |
安装与配置
安装Pinia
bash
# 使用npm
npm install pinia --save
# 使用yarn
yarn add pinia
# 使用pnpm
pnpm add 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)
app.mount('#app')核心概念
Pinia的核心概念比Vuex更简洁,主要包括:
- Store:存储状态和业务逻辑的容器
- State:存储应用状态的响应式数据
- Getter:从state中派生出的计算属性
- Action:修改state的方法,可以包含同步和异步操作
创建Store
Store是Pinia的核心,使用defineStore函数创建,需要一个唯一的ID和一个配置对象。
基本用法
javascript
// stores/counter.js
import { defineStore } from 'pinia'
// 定义并导出store
export const useCounterStore = defineStore('counter', {
// 状态
state: () => ({
count: 0,
name: 'Pinia'
}),
// 计算属性
getters: {
doubleCount: (state) => state.count * 2,
// 使用this访问其他getter
doubleCountPlusOne() {
return this.doubleCount + 1
}
},
// 方法
actions: {
increment() {
this.count++
},
decrement() {
this.count--
},
// 异步action
async incrementAsync() {
await new Promise(resolve => setTimeout(resolve, 1000))
this.increment()
},
// 带参数的action
incrementBy(amount) {
this.count += amount
}
}
})使用组合式API风格
Pinia也支持使用组合式API风格定义store:
javascript
// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// 状态
const name = ref('John')
const age = ref(30)
// getters
const isAdult = computed(() => age.value >= 18)
const userInfo = computed(() => `${name.value}, ${age.value} years old`)
// actions
function setName(newName) {
name.value = newName
}
function incrementAge() {
age.value++
}
async function fetchUser() {
const response = await fetch('/api/user')
const data = await response.json()
name.value = data.name
age.value = data.age
}
// 返回需要暴露的状态、getter和action
return {
name,
age,
isAdult,
userInfo,
setName,
incrementAge,
fetchUser
}
})使用Store
在组件中使用store非常简单,只需导入并调用定义的store函数。
基本用法
javascript
// Counter.vue
import { useCounterStore } from '@/stores/counter'
export default {
setup() {
// 获取store实例
const counterStore = useCounterStore()
return {
// 暴露状态、getter和action给模板
counterStore
}
}
}html
<template>
<div>
<h1>{{ counterStore.name }}</h1>
<p>Count: {{ counterStore.count }}</p>
<p>Double Count: {{ counterStore.doubleCount }}</p>
<button @click="counterStore.increment">Increment</button>
<button @click="counterStore.decrement">Decrement</button>
<button @click="counterStore.incrementAsync">Increment Async</button>
<button @click="counterStore.incrementBy(5)">Increment by 5</button>
</div>
</template>在选项式API中使用
Pinia提供了mapStores、mapState、mapActions等辅助函数,方便在选项式API中使用:
javascript
import { mapStores, mapState, mapActions } from 'pinia'
import { useCounterStore } from '@/stores/counter'
export default {
computed: {
// 映射整个store
...mapStores(useCounterStore),
// 映射state
...mapState(useCounterStore, ['count', 'name', 'doubleCount']),
// 重命名映射
...mapState(useCounterStore, {
myCount: 'count',
myDoubleCount: 'doubleCount'
})
},
methods: {
// 映射actions
...mapActions(useCounterStore, ['increment', 'decrement', 'incrementAsync']),
// 重命名映射
...mapActions(useCounterStore, {
add: 'increment'
})
}
}State操作
访问State
直接通过store实例访问state:
javascript
const counterStore = useCounterStore()
console.log(counterStore.count) // 0
console.log(counterStore.name) // 'Pinia'修改State
可以直接修改state,也可以使用$patch方法批量修改:
javascript
// 直接修改
counterStore.count++
counterStore.name = 'New Name'
// 使用$patch修改多个属性
counterStore.$patch({
count: counterStore.count + 1,
name: 'Updated Name'
})
// 使用函数式$patch(适用于复杂修改)
counterStore.$patch((state) => {
state.count++
state.name = 'Updated Name'
})重置State
使用$reset方法将state重置为初始值:
javascript
counterStore.$reset()替换State
使用$state属性替换整个state对象:
javascript
counterStore.$state = {
count: 100,
name: 'Replaced'
}Getter
Getter是从state中派生出的计算属性,类似于组件中的computed。
定义Getter
javascript
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
getters: {
// 基础用法
doubleCount: (state) => state.count * 2,
// 使用this访问其他getter
doubleCountPlusOne() {
return this.doubleCount + 1
},
// 带参数的getter
getCountWithFactor: (state) => (factor) => {
return state.count * factor
},
// 使用其他store
userCount() {
const userStore = useUserStore()
return userStore.users.length
}
}
})使用Getter
javascript
const counterStore = useCounterStore()
console.log(counterStore.doubleCount) // 0
console.log(counterStore.doubleCountPlusOne) // 1
console.log(counterStore.getCountWithFactor(3)) // 0Action
Action是修改state的方法,可以包含同步和异步操作。与Vuex不同,Pinia没有Mutation,所有状态修改都在Action中完成。
定义Action
javascript
export const useUserStore = defineStore('user', {
state: () => ({
users: [],
loading: false,
error: null
}),
actions: {
// 同步action
addUser(user) {
this.users.push(user)
},
// 异步action
async fetchUsers() {
this.loading = true
this.error = null
try {
const response = await fetch('/api/users')
if (!response.ok) throw new Error('Failed to fetch users')
this.users = await response.json()
} catch (err) {
this.error = err.message
} finally {
this.loading = false
}
},
// 使用其他store
async fetchUserAndPosts(userId) {
const userStore = useUserStore()
const postStore = usePostStore()
await Promise.all([
userStore.fetchUser(userId),
postStore.fetchPostsByUser(userId)
])
}
}
})调用Action
javascript
const userStore = useUserStore()
userStore.addUser({ id: 1, name: 'John' })
userStore.fetchUsers().then(() => {
console.log('Users fetched')
})模块(Modules)
Pinia没有Vuex中的模块嵌套概念,而是通过创建多个store来实现模块化。每个store都是独立的,可以相互引用。
创建多个Store
stores/
├── counter.js
├── user.js
├── post.js
└── cart.js在Store中引用其他Store
javascript
// stores/post.js
import { defineStore } from 'pinia'
import { useUserStore } from './user'
export const usePostStore = defineStore('post', {
state: () => ({
posts: []
}),
actions: {
async fetchPostsByUser(userId) {
const userStore = useUserStore()
if (!userStore.currentUser) {
await userStore.fetchUser(userId)
}
// ... fetch posts
}
}
})开发工具集成
Pinia与Vue Devtools集成良好,支持:
- 时间旅行调试:可以回溯和重放状态变化
- 状态快照:查看任意时刻的状态
- Action调用日志:记录所有Action调用
- Store结构可视化:清晰展示所有Store
持久化存储
Pinia本身不提供持久化存储功能,但可以与第三方库结合使用,如pinia-plugin-persistedstate。
安装插件
bash
npm install pinia-plugin-persistedstate --save使用插件
javascript
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
app.mount('#app')配置持久化
javascript
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Pinia'
}),
// 配置持久化
persist: true,
// 或自定义配置
persist: {
key: 'counter-store',
storage: localStorage, // 默认是localStorage
paths: ['count'] // 只持久化count字段
}
// ...
})最佳实践
Store设计
- 按功能划分Store:每个Store负责一个功能模块
- 保持Store精简:只存储全局共享状态,局部状态应放在组件内
- 单一职责原则:一个Store只处理一个领域的逻辑
- 避免深层嵌套:状态结构应扁平化,便于访问和修改
项目结构
stores/
├── index.js # 导出所有store
├── counter.js # 计数器相关状态
├── user.js # 用户相关状态
├── post.js # 文章相关状态
└── cart.js # 购物车相关状态命名规范
- Store文件名:使用小写字母,多个单词用连字符连接,如
user.js、shopping-cart.js - Store函数名:使用
useXxxStore格式,如useUserStore、useCartStore - Store ID:与Store函数名对应,使用小写字母,多个单词用连字符连接,如
user、shopping-cart
性能优化
- 避免不必要的全局状态:局部状态应放在组件内
- 使用解构时注意响应性:直接解构store会失去响应性,应使用
storeToRefs
javascript
import { storeToRefs } from 'pinia'
const counterStore = useCounterStore()
// 保持响应性
const { count, name } = storeToRefs(counterStore)
// actions不需要解构,直接使用store调用
const { increment, decrement } = counterStore- 批量更新状态:使用
$patch方法减少响应式更新次数
测试
Pinia的测试非常简单,可以像测试普通函数一样测试Store:
javascript
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '@/stores/counter'
describe('Counter Store', () => {
beforeEach(() => {
// 创建一个新的pinia实例
setActivePinia(createPinia())
})
it('should increment count', () => {
const counterStore = useCounterStore()
expect(counterStore.count).toBe(0)
counterStore.increment()
expect(counterStore.count).toBe(1)
})
it('should double count', () => {
const counterStore = useCounterStore()
counterStore.count = 2
expect(counterStore.doubleCount).toBe(4)
})
})从Vuex迁移到Pinia
从Vuex迁移到Pinia非常简单,主要步骤包括:
- 安装Pinia并在应用中配置
- 将Vuex的Store逐个转换为Pinia的Store
- 将state转换为函数返回对象
- 将getters转换为对象或函数
- 将mutations和actions合并为actions
- 将modules拆分为多个独立的Store
- 更新组件中使用Store的方式
- 移除Vuex相关依赖
迁移示例
Vuex Store:
javascript
// store/modules/counter.js
const state = {
count: 0
}
const getters = {
doubleCount: state => state.count * 2
}
const mutations = {
increment(state) {
state.count++
}
}
const actions = {
incrementAsync({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
export default {
namespaced: true,
state,
getters,
mutations,
actions
}Pinia Store:
javascript
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
getters: {
doubleCount: state => state.count * 2
},
actions: {
increment() {
this.count++
},
incrementAsync() {
setTimeout(() => {
this.increment()
}, 1000)
}
}
})总结
Pinia是Vue官方推荐的状态管理库,提供了简洁的API、优秀的TypeScript支持和灵活的使用方式。通过Store、State、Getter和Action等核心概念,Pinia帮助我们更好地管理应用状态。相比Vuex,Pinia去除了Mutation和模块嵌套,使状态管理更加直观和简单。对于新项目,推荐使用Pinia;对于现有Vuex项目,可以逐步迁移到Pinia。合理使用Pinia可以使应用状态变化更加可预测、可调试,同时提高代码的可维护性和复用性。