Pinia 完全指南
相关文档:Vue3入门(/vue-3-ru-men-zhi-nan/) TanStack Query 完全指南(/tanstack-query-wan-quan-zhi-nan/) Axios完全指南(/axios-wan-quan-zhi-nan/) Pinia 是 Vue 官方推荐的状态管理库,是 Vuex 的继任者。 Pinia 支持两种风格:选项式(类 Vuex)和 Setup(推荐)。 每个 Store 只管理一类数据,不要创建一个大而全的 Store: 用 TanStack Query 管理服务端数据(自动缓存、失效),Store 只管理真正
官方文档:https://pinia.vuejs.org/zh/
适用版本:Pinia 2.x(2026-05-07 核实)
相关文档:Vue3入门 TanStack Query 完全指南 Axios完全指南
1. 基础概念
Pinia 是什么
Pinia 是 Vue 官方推荐的状态管理库,是 Vuex 的继任者。
| 特性 | Pinia | Vuex 4 |
|---|---|---|
| TypeScript 支持 | 完整类型推断 | 需大量类型体操 |
| API 风格 | Composition API(选项式也支持) | mutations + actions 分离 |
| 模块化 | 每个 store 独立,无需 modules | 嵌套 modules |
| DevTools | 支持 | 支持 |
| 体积 | ~1KB | ~10KB |
| SSR | 支持 | 支持 |
安装
npm install pinia
// src/main.ts
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
const app = createApp(App);
app.use(createPinia());
app.mount("#app");
2. 定义 Store
Pinia 支持两种风格:选项式(类 Vuex)和 Setup(推荐)。
Setup 风格(推荐,更灵活)
// src/stores/user.ts
import { defineStore } from "pinia";
import { ref, computed } from "vue";
import type { User } from "@/types";
export const useUserStore = defineStore("user", () => {
// state:用 ref / reactive 定义
const currentUser = ref<User | null>(null);
const token = ref<string>("");
const users = ref<User[]>([]);
// getters:用 computed 定义
const isLoggedIn = computed(() => !!currentUser.value);
const userCount = computed(() => users.value.length);
// actions:普通函数(支持异步)
async function login(email: string, password: string) {
const { data } = await authApi.login(email, password);
token.value = data.token;
currentUser.value = data.user;
localStorage.setItem("token", data.token);
}
function logout() {
currentUser.value = null;
token.value = "";
localStorage.removeItem("token");
}
async function fetchUsers() {
users.value = await userApi.list();
}
return {
// 对外暴露 state、getters、actions
currentUser,
token,
users,
isLoggedIn,
userCount,
login,
logout,
fetchUsers,
};
});
选项式风格(类 Vuex,适合简单场景)
import { defineStore } from "pinia";
export const useCounterStore = defineStore("counter", {
state: () => ({
count: 0,
name: "counter",
}),
getters: {
doubled: (state) => state.count * 2,
// 访问其他 store
fullInfo(): string {
return `${this.name}: ${this.count}`;
},
},
actions: {
increment() {
this.count++;
},
async fetchCount() {
this.count = await api.getCount();
},
},
});
3. 在组件中使用
<script setup lang="ts">
import { useUserStore } from "@/stores/user";
import { storeToRefs } from "pinia";
const userStore = useUserStore();
// 直接解构会失去响应性!
// const { currentUser, isLoggedIn } = userStore; // 错误
// storeToRefs:保持响应性解构 state 和 getters
const { currentUser, isLoggedIn, userCount } = storeToRefs(userStore);
// actions 直接解构即可(函数不需要响应性)
const { login, logout, fetchUsers } = userStore;
// 调用 action
async function handleLogin() {
await login("[email protected]", "password");
}
</script>
<template>
<div>
<p v-if="isLoggedIn">欢迎,{{ currentUser?.name }}(共 {{ userCount }} 个用户)</p>
<button @click="handleLogin">登录</button>
<button @click="logout">退出</button>
</div>
</template>
4. 直接修改 State
const store = useUserStore();
// 方式 1:直接赋值(需要 setup store 暴露 ref)
store.token = "new-token";
// 方式 2:$patch(批量修改,性能更好,只触发一次更新)
store.$patch({
token: "new-token",
currentUser: { id: 1, name: "Alice" },
});
// 方式 3:$patch 函数形式(适合复杂修改)
store.$patch((state) => {
state.users.push({ id: 2, name: "Bob" });
state.users[0].name = "Alice Updated";
});
// 重置到初始 state(仅选项式 store 默认支持,setup store 需手动实现)
store.$reset();
5. 订阅 Store 变化
$subscribe — 监听 state 变化
const store = useCounterStore();
// 监听 state 变化(类似 watch store)
const unsubscribe = store.$subscribe((mutation, state) => {
console.log(mutation.type) // 'direct' | 'patch object' | 'patch function'
console.log(mutation.storeId) // 'counter'
console.log(state.count) // 新的 state
// 持久化到 localStorage
localStorage.setItem("counter", JSON.stringify(state));
}, {
detached: true, // 组件卸载后不自动停止监听
});
// 手动取消订阅
unsubscribe();
$onAction — 监听 action 调用
store.$onAction(({ name, args, after, onError }) => {
console.log(`action "${name}" 被调用,参数:`, args);
after((result) => {
console.log(`action "${name}" 执行成功,结果:`, result);
});
onError((error) => {
console.error(`action "${name}" 执行失败:`, error);
});
});
6. Store 间相互访问
// src/stores/cart.ts
import { defineStore } from "pinia";
import { useUserStore } from "./user";
export const useCartStore = defineStore("cart", () => {
const userStore = useUserStore(); // 在 store 内部直接使用其他 store
async function checkout() {
if (!userStore.isLoggedIn) {
throw new Error("请先登录");
}
await cartApi.checkout(userStore.currentUser!.id);
}
return { checkout };
});
7. 持久化插件(pinia-plugin-persistedstate)
npm install pinia-plugin-persistedstate
// src/main.ts
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
// src/stores/auth.ts
export const useAuthStore = defineStore("auth", () => {
const token = ref("");
const user = ref<User | null>(null);
return { token, user };
}, {
persist: {
// 默认使用 localStorage,key 为 store id("auth")
key: "auth_state",
storage: localStorage, // 或 sessionStorage
pick: ["token"], // 只持久化 token,不持久化 user
},
});
8. 常用代码段
通用加载状态封装
// src/stores/user.ts
export const useUserStore = defineStore("user", () => {
const users = ref<User[]>([]);
const loading = ref(false);
const error = ref<string | null>(null);
async function fetchUsers() {
loading.value = true;
error.value = null;
try {
users.value = await userApi.list();
} catch (e) {
error.value = e instanceof Error ? e.message : "加载失败";
} finally {
loading.value = false;
}
}
return { users, loading, error, fetchUsers };
});
认证 Store(完整示例)
// src/stores/auth.ts
import { defineStore } from "pinia";
import { ref, computed } from "vue";
export const useAuthStore = defineStore("auth", () => {
const token = ref(localStorage.getItem("token") ?? "");
const user = ref<User | null>(null);
const isLoggedIn = computed(() => !!token.value);
function setToken(newToken: string) {
token.value = newToken;
localStorage.setItem("token", newToken);
}
async function login(credentials: { email: string; password: string }) {
const { data } = await authApi.login(credentials);
setToken(data.token);
user.value = data.user;
}
function logout() {
token.value = "";
user.value = null;
localStorage.removeItem("token");
}
// 初始化:从 token 恢复用户信息
async function init() {
if (token.value) {
try {
user.value = await userApi.profile();
} catch {
logout();
}
}
}
return { token, user, isLoggedIn, login, logout, setToken, init };
});
9. 与 TanStack Query 的分工
| 数据类型 | 推荐方案 |
|---|---|
| 服务端数据(用户列表、文章等) | TanStack Query(自动缓存、失效、重试) |
| 认证状态(token、当前用户) | Pinia(全局共享,持久化) |
| UI 状态(弹窗开关、侧边栏折叠) | 组件内 ref(不需要跨组件共享时) |
| 跨组件 UI 状态(主题、语言) | Pinia |
| 表单状态 | 组件内 ref(或 VeeValidate / TanStack Form) |
10. 最佳实践
Store 职责单一
每个 Store 只管理一类数据,不要创建一个大而全的 Store:
stores/
auth.ts # 登录状态、token、当前用户
ui.ts # 主题、语言、侧边栏状态
cart.ts # 购物车
notification.ts # 通知
不要在 Store 中存服务端数据
用 TanStack Query 管理服务端数据(自动缓存、失效),Store 只管理真正的"全局客户端状态"(认证、UI 偏好设置等)。
11. 踩坑与注意事项
直接解构 store 会失去响应性
const store = useUserStore();
// 错误:解构后的值是普通变量,不是响应式
const { count } = store;
// 正确
const { count } = storeToRefs(store); // ref
// 或
const count = computed(() => store.count);
Setup Store 默认不支持 $reset()
选项式 store 的 $reset() 会重置到 state() 返回的初始值,但 setup store 没有这个默认行为。需手动实现:
const initialState = { count: 0, name: "" };
export const useStore = defineStore("demo", () => {
const state = reactive({ ...initialState });
function $reset() {
Object.assign(state, initialState);
}
return { ...toRefs(state), $reset };
});
在非 setup 函数中使用 Store
在路由守卫、axios 拦截器等非 Vue 组件上下文中使用 store,必须传入 pinia 实例(或在 app.use(pinia) 之后调用):
// 在 router/index.ts
import { useAuthStore } from "@/stores/auth";
router.beforeEach((to) => {
const auth = useAuthStore(); // 在路由守卫中直接调用是安全的(pinia 已初始化)
if (to.meta.requiresAuth && !auth.isLoggedIn) {
return "/login";
}
});
最佳实践
Store 按功能领域拆分,一个 store 管一件事:避免一个巨大的 globalStore,将用户信息、购物车、UI 状态等拆为独立 store,互相引用时直接导入另一个 store 函数调用即可。
// stores/cart.ts
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', () => {
const user = useUserStore()
const items = ref<CartItem[]>([])
const total = computed(() => items.value.reduce((s, i) => s + i.price, 0))
return { items, total }
})
用 storeToRefs 解构响应式数据,普通方法直接解构:直接解构 store 会丢失响应性;storeToRefs 只转换 state 和 getter,action 方法不需要 toRefs 包裹。
const store = useCounterStore()
const { count, doubleCount } = storeToRefs(store) // 保持响应性
const { increment, reset } = store // action 直接解构
用 $patch 批量更新 state 减少响应式触发次数:多个 state 字段同时修改时,用 $patch 只触发一次响应更新,比逐个赋值效率更高。
store.$patch({ count: 10, name: 'new' })
// 或函数式(处理数组等复杂更新)
store.$patch((state) => { state.items.push(newItem); state.count++ })
持久化 store 用 pinia-plugin-persistedstate:登录状态、用户偏好等需要跨会话保存的数据,使用官方推荐插件持久化到 localStorage。
defineStore('user', () => { ... }, {
persist: { key: 'user-store', storage: localStorage }
})
常见陷阱
陷阱:在 setup() 外调用 store 函数报错
现象: useUserStore() 在 main.ts 顶层调用时报 [🍍]: getActivePinia() was called with no active Pinia。
原因: Pinia store 的 useXxxStore() 函数必须在活跃的 Pinia 实例存在时调用,即 app.use(pinia) 之后。在 app.use() 之前调用会找不到 Pinia 实例。
解决: 确保 app.use(pinia) 在任何 store 调用之前执行,或将初始化逻辑移到组件或路由守卫中。
// 正确:先 use(pinia) 再使用 store
const app = createApp(App)
const pinia = createPinia()
app.use(pinia) // 必须在前
app.use(router)
const userStore = useUserStore() // 此时安全
陷阱:直接解构 store state 丢失响应性
现象: const { count } = store 后,模板中 count 不会随 store 更新而更新。
原因: 解构时 count 成为一个普通 JS 值,切断了与 reactive 对象的连接。
解决: 使用 storeToRefs(store) 解构 state 和 getter。
// 错误:丢失响应性
const { count } = useCounterStore()
// 正确
const { count } = storeToRefs(useCounterStore())
陷阱:在 SSR 中 store 跨请求共享状态
现象: 服务端渲染时,不同用户的请求看到了相同的 store 数据(用户 A 看到了用户 B 的购物车)。
原因: SSR 环境中 Pinia 实例如果被模块级单例持有,所有请求共享同一个 store,造成数据污染。
解决: 每个 SSR 请求创建独立的 Pinia 实例,通过 createPinia() + Vue 的 SSR context 传递。