Pinia 状态管理详解
Store 定义、状态读写、异步操作与插件机制。
前置知识
- TypeScript 集成:建议先完成前一篇的学习
学习目标
- 掌握「1. Pinia 概述」的核心机制、典型用法与常见陷阱
- 掌握「2. 环境搭建」的核心机制、典型用法与常见陷阱
- 掌握「3. 基础用法」的核心机制、典型用法与常见陷阱
- 掌握「4. 状态管理」的核心机制、典型用法与常见陷阱
- 掌握「5. Getters」的核心机制、典型用法与常见陷阱
1. Pinia 概述
Pinia 是 Vue 3 官方推荐的状态管理库,它是 Vuex 的替代品,提供了更简洁的 API 和更好的 TypeScript 支持。
1.1 主要特性
- 简洁的 API:使用组合式 API 风格
- 更好的 TypeScript 支持:无需手动类型声明
- 模块化设计:支持多个 Store
- 支持插件:可以扩展 Pinia 功能
- 支持持久化:可以轻松实现状态持久化
- 支持热更新:开发时可以热更新状态
- 支持 SSR:服务端渲染友好
2. 环境搭建
2.1 安装 Pinia
# 使用 npm
npm install pinia
# 使用 yarn
yarn add pinia
2.2 基本配置
// main.ts
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');
3. 基础用法
3.1 创建 Store
// store/counter.ts
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
// 状态
state: () => ({
count: 0,
name: '计数器'
}),
// 计算属性
getters: {
doubleCount: (state) => state.count * 2,
// 可以访问其他 getter
doubleCountPlusOne: (state, getters) => getters.doubleCount + 1
},
// 方法
actions: {
increment() {
this.count++
},
incrementBy(amount: number) {
this.count += amount
},
// 异步操作
async incrementAsync() {
await new Promise(resolve => setTimeout(resolve, 1000))
this.count++
}
}
}
3.2 使用 Store
<template>
<div>
<h1>{{ counterStore.name }}</h1>
<p>Count: {{ counterStore.count }}</p>
<p>Double Count: {{ counterStore.doubleCount }}</p>
<p>Double Count Plus One: {{ counterStore.doubleCountPlusOne }}</p>
<button @click="counterStore.increment">Increment</button>
<button @click="counterStore.incrementBy(5)">Increment by 5</button>
<button @click="counterStore.incrementAsync">Increment Async</button>
</div>
</template>
<script setup lang="ts">
import { useCounterStore } from '../store/counter';
const counterStore = useCounterStore();
</script>
4. 状态管理
4.1 直接修改状态
<script setup lang="ts">
import { useCounterStore } from '../store/counter';
const counterStore = useCounterStore();
// 直接修改状态
counterStore.count = 10;
</script>
4.2 使用 $patch 批量修改
<script setup lang="ts">
import { useCounterStore } from '../store/counter'
const counterStore = useCounterStore()
// 批量修改状态
counterStore.$patch({
count: 20,
name: '新计数器'
}
// 使用函数形式批量修改
counterStore.$patch((state) => {
state.count += 10
state.name = '更新后的计数器'
}
</script>
4.3 重置状态
<script setup lang="ts">
import { useCounterStore } from '../store/counter';
const counterStore = useCounterStore();
// 重置状态到初始值
function resetStore() {
counterStore.$reset();
}
</script>
5. Getters
5.1 基础 Getters
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
getters: {
// 基础 getter
doubleCount: (state) => state.count * 2,
// 带参数的 getter
getCountBy: (state) => (multiplier: number) => state.count * multiplier
}
}
vue
<template>
<div>
<p>Double Count: {{ counterStore.doubleCount }}</p>
<p>Count * 3: {{ counterStore.getCountBy(3) }}</p>
</div>
</template>
5.2 访问其他 Store 的 Getters
// store/user.ts
import { defineStore } from 'pinia'
import { useCounterStore } from './counter'
export const useUserStore = defineStore('user', {
state: () => ({
name: '张三'
}),
getters: {
// 访问其他 store 的 getter
userWithCount: (state) => {
const counterStore = useCounterStore()
return `${state.name} 的计数器值为 ${counterStore.count}`
}
}
}
6. Actions
6.1 基础 Actions
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
actions: {
increment() {
this.count++
},
incrementBy(amount: number) {
this.count += amount
}
}
}
6.2 异步 Actions
export const useUserStore = defineStore('user', {
state: () => ({
userList: [],
loading: false
}),
actions: {
async fetchUsers() {
this.loading =
try {
const response = await fetch('https://api.example.com/users')
this.userList = await response.json()
} catch (error) {
console.error('获取用户列表失败:', error)
} finally {
this.loading = false
}
}
}
}
6.3 访问其他 Store 的 Actions
// store/cart.ts
import { defineStore } from 'pinia'
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', {
state: () => ({
items: []
}),
actions: {
addItem(item: any) {
this.items.push(item)
// 访问其他 store 的 action
const userStore = useUserStore()
userStore.updateLastActivity()
}
}
}
7. 模块化
7.1 基本模块化
// store/modules/user.ts
export const useUserStore = defineStore('user', {
// ...
,
});
// store/modules/cart.ts
export const useCartStore = defineStore('cart', {
// ...
,
});
// store/index.ts
export * from './modules/user';
export * from './modules/cart';
7.2 组合式 Store
// store/user.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useUserStore = defineStore('user', () => {
// 状态
const name = ref('张三');
const age = ref(20);
// 计算属性
const isAdult = computed(() => age.value >= 18);
// 方法
function updateName(newName: string) {
name.value = newName;
}
function incrementAge() {
age.value++;
}
return {
name,
age,
isAdult,
updateName,
incrementAge,
};
});
8. 持久化
8.1 使用 pinia-plugin-persistedstate
安装:
npm install pinia-plugin-persistedstate
配置:
// main.ts
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');
使用:
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
// 启用持久化
persist: True,
});
8.2 自定义持久化配置
export const useUserStore = defineStore('user', {
state: () => ({
name: '张三',
age: 20,
token: ''
}),
persist: {
// 存储到 localStorage
storage: localStorage,
// 只持久化特定字段
paths: ['name', 'token'],
// 自定义键名
key: 'user-storage'
}
}
9. 插件
9.1 自定义插件
// pinia plugins
import { PiniaPluginContext } from 'pinia';
export function myPiniaPlugin(context: PiniaPluginContext) {
const { store } = context;
// 在 store 初始化时执行
console.log('Store initialized:', store.$id);
// 添加自定义方法
store.$resetState = () => {
store.$reset();
console.log('Store reset:', store.$id);
};
// 监听状态变化
store.$subscribe((mutation, state) => {
console.log('State changed:', mutation.type, state);
});
}
// main.ts
import { createPinia } from 'pinia';
import { myPiniaPlugin } from './plugins/pinia';
const pinia = createPinia();
pinia.use(myPiniaPlugin);
9.2 使用官方插件
- pinia-plugin-persistedstate:状态持久化
- pinia-plugin-debug:调试工具
- pinia-plugin-logger:日志记录
10. 类型安全
10.1 TypeScript 支持
// store/user.ts
import { defineStore } from 'pinia'
interface User {
id: number
name: string
email: string
}
export const useUserStore = defineStore('user', {
state: (): {
users: User[]
loading: boolean
} => ({
users: [],
loading: false
}),
getters: {
activeUsers: (state): User[] => {
return state.users.filter(user => user.name.length > 0)
}
},
actions: {
addUser(user: User) {
this.users.push(user)
}
}
}
10.2 组合式 Store 的类型
// store/user.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
interface User {
id: number;
name: string;
;
}
export const useUserStore = defineStore('user', () => {
const users = ref<User[]>([]);
const loading = ref(false);
const activeUsers = computed(() => {
return users.value.filter((user) => user.name.length > 0);
});
function addUser(user: User) {
users.value.push(user);
}
return {
users,
loading,
activeUsers,
addUser,
};
;
});
11. 最佳实践
- 使用模块化:将不同功能的状态分离到不同的 Store 中
- 使用组合式 API:对于复杂的 Store,使用组合式 API 风格
- 使用 TypeScript:提供类型安全,减少运行时错误
- 合理使用持久化:只持久化必要的状态
- 使用 actions 处理复杂逻辑:将业务逻辑封装在 actions 中
- 使用 getters 处理派生状态:避免在组件中重复计算
- 监听状态变化:使用 $subscribe 监听状态变化,执行副作用
- 测试 Store:确保 Store 的逻辑正确
12. 常见问题与解决方案
12.1 状态更新后组件不更新
问题:修改状态后组件没有重新渲染 解决方案:确保使用正确的方式修改状态,对于对象和数组,使用 $patch 或直接替换整个对象/数组
12.2 持久化不生效
问题:状态持久化后刷新页面状态丢失 解决方案:检查持久化配置是否正确,确保存储介质(localStorage/sessionStorage)可用
12.3 多个 Store 之间的依赖
问题:多个 Store 之间存在循环依赖 解决方案:在 actions 中按需导入其他 Store,避免在模块顶部直接导入
12.4 异步操作的错误处理
问题:异步 actions 中的错误没有被正确处理 解决方案:使用 try/catch 捕获错误,并在组件中处理错误状态
13. 总结
Pinia 是 Vue3 生态系统中推荐的状态管理库,它提供了简洁的 API、更好的 TypeScript 支持和强大的功能。通过本教程的学习,你应该已经掌握了 Pinia 的核心概念和使用方法,可以在实际项目中灵活运用。
创建 Pinia
createPinia 创建实例
import { createApp } from 'vue';
import { createPinia } from 'pinia';
const app = createApp(App);
app.use(createPinia());
app.mount('#app');
pinia 插件
const pinia = createPinia();
pinia.use(({ store }) => {
store.$subscribe((mutation, state) => {
localStorage.setItem(store.$id, JSON.stringify(state));
});
});
app.use(pinia);
defineStore 定义 store
Options 选项式
const <useStore> = defineStore(<id>, <options>);
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
name: 'Tom'
}),
getters: {
double: (state) => state.count * 2,
doublePlusOne(): number {
return this.double + 1;
}
},
actions: {
increment() {
this.count++;
},
async fetchCount() {
const res = await fetch('/api/count');
this.count = await res.json();
}
}
});
Setup 组合式
const <useStore> = defineStore(<id>, <setup>);
import { defineStore, ref, computed } from 'pinia';
export const useCounterStore = defineStore('counter', () => {
const count = ref(0);
const name = ref('Tom');
const double = computed(() => count.value * 2);
const doublePlusOne = computed(() => double.value + 1);
function increment() {
count.value++;
}
async function fetchCount() {
const res = await fetch('/api/count');
count.value = await res.json();
}
return { count, name, double, doublePlusOne, increment, fetchCount };
});
TS 类型推断
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
user: null as { id: number; name: string } | null,
token: '' as string
}),
getters: {
isLoggedIn: (state) => !!state.user,
userName(): string {
return this.user?.name ?? '';
}
},
actions: {
setUser(user: { id: number; name: string }) {
this.user = user;
}
}
});
使用 store
获取 store
const <store> = useXxxStore();
import { useCounterStore } from '@/stores/counter';
const counterStore = useCounterStore();
// state
console.log(counterStore.count);
counterStore.count++; // 直接修改(可行)
// getters
console.log(counterStore.double);
// actions
counterStore.increment();
await counterStore.fetchCount();
store 解构(响应性丢失)
const store = useCounterStore();
const { count } = store; // 响应性丢失
storeToRefs 解构响应式
const { <key>, ... } = storeToRefs(<store>);
import { storeToRefs } from 'pinia';
const store = useCounterStore();
const { count, name, double } = storeToRefs(store); // 保持响应性
// actions 可以直接解构(函数无需响应性)
const { increment } = store;
State 操作
直接修改
const store = useCounterStore();
store.count++;
store.user = { id: 1, name: 'Tom' };
**patch(<partial | function>);`
store.$patch({ count: 100, name: 'New' });
store.$patch((state) => {
state.count = 100;
state.list.push('new item');
state.user.name = 'Tom';
});
$reset 重置状态
store.$reset();
**subscribe((mutation, state) => {}, [options]);`
const unsubscribe = store.$subscribe((mutation, state) => {
console.log('mutation.type:', mutation.type); // 'direct' | 'patch object' | 'patch function'
console.log('mutation.storeId:', mutation.storeId);
console.log('state:', state);
}, { detached: true });
// 取消订阅
unsubscribe();
Getters
基础 getter
getters: {
double: (state) => state.count * 2,
quadruple: (state) => state.count * 4
}
getter 互相调用
getters: {
double: (state) => state.count * 2,
quadruple(): number {
return this.double * 2;
}
}
getter 接收参数
getters: {
getUserById: (state) => (id: number) => {
return state.users.find(u => u.id === id);
}
}
// 使用
const user = store.getUserById(1);
跨 store 调用
import { useUserStore } from './user';
export const useCartStore = defineStore('cart', {
getters: {
userNameWithItems(): string {
const userStore = useUserStore();
return `${userStore.name} (${this.items.length})`;
}
}
});
Actions
同步 action
actions: {
increment() {
this.count++;
},
reset() {
this.count = 0;
this.user = null;
}
}
异步 action
actions: {
async fetchUser(id: number) {
try {
const res = await fetch(`/api/users/${id}`);
this.user = await res.json();
} catch (e) {
console.error(e);
}
}
}
action 调用其他 action
actions: {
async login(credentials) {
const user = await api.login(credentials);
this.setUser(user);
this.loadProfile();
},
setUser(user) {
this.user = user;
},
async loadProfile() {
this.profile = await api.getProfile(this.user.id);
}
}
**onAction(
const unsubscribe = store.$onAction({
name: 'fetchUser',
after: (result) => console.log('action done', result),
onError: (error) => console.error('action error', error)
});
// 或函数式
const unsubscribe = store.$onAction((context) => {
console.log('action:', context.name, context.args);
context.after((result) => console.log('done'));
context.onError((error) => console.error(error));
});
unsubscribe();
多 store 组合
store 互相调用
// stores/user.ts
export const useUserStore = defineStore('user', () => {
const user = ref(null);
return { user };
});
// stores/cart.ts
import { useUserStore } from './user';
export const useCartStore = defineStore('cart', () => {
const userStore = useUserStore();
const items = ref([]);
const canCheckout = computed(() =>
!!userStore.user && items.value.length > 0
);
return { items, canCheckout };
});
持久化
手动持久化
import { useUserStore } from './user';
const store = useUserStore();
store.$subscribe((mutation, state) => {
localStorage.setItem('user', JSON.stringify(state));
});
// 初始化时恢复
const saved = localStorage.getItem('user');
if (saved) {
store.$patch(JSON.parse(saved));
}
插件方式
// main.ts
const pinia = createPinia();
pinia.use(({ store }) => {
// 恢复
const saved = localStorage.getItem(store.$id);
if (saved) {
store.$patch(JSON.parse(saved));
}
// 订阅变化
store.$subscribe((mutation, state) => {
localStorage.setItem(store.$id, JSON.stringify(state));
});
});
app.use(pinia);
组件中使用
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter';
import { storeToRefs } from 'pinia';
const store = useCounterStore();
const { count, double } = storeToRefs(store);
const { increment } = store;
function handleReset() {
store.$reset();
}
function handleBatchUpdate() {
store.$patch({ count: 100 });
}
</script>
<template>
<div>
<p>count: {{ count }}</p>
<p>double: {{ double }}</p>
<button @click="increment">+1</button>
<button @click="handleReset">重置</button>
</div>
</template>