> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Pinia 完全指南
- URL: https://blog.vercanti.com/pinia-wan-quan-zhi-nan/
- Published: 2026-08-28T14:35:25.000Z
- Updated: 2026-08-28T14:58:47.000Z
- Description: 相关文档：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 只管理真正
- Author: yellowdog
- Tags: 前端开发, Vue生态

> 官方文档：<https://pinia.vuejs.org/zh/>  
> 适用版本：Pinia 2.x（2026-05-07 核实）

相关文档：[Vue3入门](https://blog.vercanti.com/vue-3-ru-men-zhi-nan/) [TanStack Query 完全指南](https://blog.vercanti.com/tanstack-query-wan-quan-zhi-nan/) [Axios完全指南](https://blog.vercanti.com/axios-wan-quan-zhi-nan/)

---

## 1\. 基础概念

### Pinia 是什么

Pinia 是 Vue 官方推荐的状态管理库，是 Vuex 的继任者。

| 特性            | Pinia                   | Vuex 4                 |
| ------------- | ----------------------- | ---------------------- |
| TypeScript 支持 | 完整类型推断                  | 需大量类型体操                |
| API 风格        | Composition API（选项式也支持） | mutations + actions 分离 |
| 模块化           | 每个 store 独立，无需 modules  | 嵌套 modules             |
| DevTools      | 支持                      | 支持                     |
| 体积            | \~1KB                   | \~10KB                 |
| SSR           | 支持                      | 支持                     |

### 安装

```bash
npm install pinia

```

```ts
// 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 风格（推荐，更灵活）

```ts
// 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，适合简单场景）

```ts
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\. 在组件中使用

```vue
<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("alice@example.com", "password");
}
</script>

<template>
  <div>
    <p v-if="isLoggedIn">欢迎，{{ currentUser?.name }}（共 {{ userCount }} 个用户）</p>
    <button @click="handleLogin">登录</button>
    <button @click="logout">退出</button>
  </div>
</template>

```

---

## 4\. 直接修改 State

```ts
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 变化

```ts
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 调用

```ts
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 间相互访问

```ts
// 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）

```bash
npm install pinia-plugin-persistedstate

```

```ts
// src/main.ts
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";

const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);

```

```ts
// 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\. 常用代码段

### 通用加载状态封装

```ts
// 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（完整示例）

```ts
// 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 会失去响应性

```ts
const store = useUserStore();

// 错误：解构后的值是普通变量，不是响应式
const { count } = store;

// 正确
const { count } = storeToRefs(store);  // ref
// 或
const count = computed(() => store.count);

```

### Setup Store 默认不支持 $reset()

选项式 store 的 `$reset()` 会重置到 `state()` 返回的初始值，但 setup store 没有这个默认行为。需手动实现：

```ts
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) 之后调用）：

```ts
// 在 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 函数调用即可。

```ts
// 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 包裹。

```ts
const store = useCounterStore()
const { count, doubleCount } = storeToRefs(store)  // 保持响应性
const { increment, reset } = store                 // action 直接解构

```

**用 `$patch` 批量更新 state 减少响应式触发次数**：多个 state 字段同时修改时，用 `$patch` 只触发一次响应更新，比逐个赋值效率更高。

```ts
store.$patch({ count: 10, name: 'new' })
// 或函数式（处理数组等复杂更新）
store.$patch((state) => { state.items.push(newItem); state.count++ })

```

**持久化 store 用 `pinia-plugin-persistedstate`**：登录状态、用户偏好等需要跨会话保存的数据，使用官方推荐插件持久化到 localStorage。

```ts
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 调用之前执行，或将初始化逻辑移到组件或路由守卫中。

```ts
// 正确：先 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。

```ts
// 错误：丢失响应性
const { count } = useCounterStore()

// 正确
const { count } = storeToRefs(useCounterStore())

```

---

### 陷阱：在 SSR 中 store 跨请求共享状态

**现象：** 服务端渲染时，不同用户的请求看到了相同的 store 数据（用户 A 看到了用户 B 的购物车）。

**原因：** SSR 环境中 Pinia 实例如果被模块级单例持有，所有请求共享同一个 store，造成数据污染。

**解决：** 每个 SSR 请求创建独立的 Pinia 实例，通过 `createPinia()` \+ Vue 的 SSR context 传递。

---

## 参见

- [Vue3入门](https://blog.vercanti.com/vue-3-ru-men-zhi-nan/)
- [Vue Router完全指南](https://blog.vercanti.com/vue-router-wan-quan-zhi-nan/)
- [TanStack Query 完全指南](https://blog.vercanti.com/tanstack-query-wan-quan-zhi-nan/)
- [Axios完全指南](https://blog.vercanti.com/axios-wan-quan-zhi-nan/)