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 传递。


参见

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog