前置知识: JavaScript、HTML5、CSS

Pinia 状态管理详解

4 min中级

Store 定义、状态读写、异步操作与插件机制。

前置知识

学习目标

  • 掌握「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. 最佳实践

  1. 使用模块化:将不同功能的状态分离到不同的 Store 中
  2. 使用组合式 API:对于复杂的 Store,使用组合式 API 风格
  3. 使用 TypeScript:提供类型安全,减少运行时错误
  4. 合理使用持久化:只持久化必要的状态
  5. 使用 actions 处理复杂逻辑:将业务逻辑封装在 actions 中
  6. 使用 getters 处理派生状态:避免在组件中重复计算
  7. 监听状态变化:使用 $subscribe 监听状态变化,执行副作用
  8. 测试 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批量修改∗∗‘store.patch 批量修改** `store.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订阅state∗∗‘store.subscribe 订阅 state** `store.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订阅action∗∗‘store.onAction 订阅 action** `store.onAction(, [detached]);`

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>