Skip to content

Pinia 状态管理 ​

Pinia 是 Vue 官方推荐的状态管理库,用 state、getters、actions 三个概念覆盖全局状态、派生数据与修改逻辑。本文基于 Pinia 2.x 与 Vue 3 项目,依次说明安装、store 定义、组件内读取、异步 action、store 相互引用、在路由守卫中获取以及数据持久化。

参考文档:Pinia 官方文档(原始笔记记录的镜像地址为 pinia.web3doc.top)。

目录 ​

  1. 特点
  2. 安装
  3. 接入应用
  4. 定义 store
  5. 在组件中使用
  6. 异步 actions
  7. store 之间相互引用
  8. 在路由守卫中使用
  9. 数据持久化

一、特点 ​

  1. 完整的 TypeScript 支持,state、getters、actions 都有类型推导。
  2. 三大核心概念:
    • state:存储的值;
    • getters:计算属性;
    • actions:改变值的方法,同时支持同步与异步。
  3. 与 Vuex 相比去掉了 mutations(actions 也能同步修改)和 modules(store 之间直接相互引用)。

Vuex 的概念在 Pinia 中的对应关系:

VuexPinia说明
statestate存储的值
gettersgetters计算属性
mutations无由 actions 承担,支持同步修改
actionsactions支持异步
modules无每个 store 独立定义,可相互引用

二、安装 ​

bash
yarn add pinia

npm 项目对应 npm install pinia。

三、接入应用 ​

1. 创建 pinia 实例 ​

新建 store/index.ts:

ts
import { createPinia } from "pinia";

const store = createPinia();

export default store;

2. 在入口文件中注册 ​

在 main.ts 中通过 app.use(store) 挂载,让组件树在挂载前就具备 store:

ts
import { createApp } from "vue";
import App from "./App.vue";
import store from "./store";

const app = createApp(App);

app.use(store);
app.mount("#app");

四、定义 store ​

新建 store/user.ts。defineStore 的第一个参数是 store 的 id,必需且值唯一;state 返回一个函数,避免多个组件实例共享同一个对象造成作用域污染。

ts
import { defineStore } from "pinia";

export const useUserStore = defineStore("user", {
  // state 返回一个函数,防止作用域污染
  state: () => {
    return {
      userInfo: {
        name: "zhangsan",
        age: 23,
      },
      token: "S1",
    };
  },
  getters: {
    newName: (state) => state.userInfo.name + "vip",
  },
  actions: {
    // 更新整个对象
    updateUserInfo(userInfo: { name: string; age: number }) {
      this.userInfo = userInfo;
    },
    // 更新对象中某个属性
    updateAge(age: number) {
      this.userInfo.age = age;
    },
    // 更新基础数据类型
    updateToken(token: string) {
      this.token = token;
    },
  },
});

五、在组件中使用 ​

useUserStore() 返回的 store 是一个用 reactive 包装的对象。直接解构读取 state 会失去响应式,因此需要用 storeToRefs 为每个响应式属性创建 ref 引用,解构出来的值通过 .value 读取。

storeToRefs 会跳过所有的 action 属性,action 直接从 store 上解构即可。

1. 完整示例 ​

vue
<template>
  <div>
    <div>姓名:{{ userInfo.name }} 年龄:{{ userInfo.age }}</div>
    <div>token:{{ token }}</div>
    <div>getter 值:{{ newName }}</div>
    <button @click="handleUser">更新用户</button>
    <button @click="handleAge">更新年龄</button>
    <button @click="handleToken">更新 token</button>
  </div>
</template>

<script setup lang="ts">
import { storeToRefs } from "pinia";
import { useUserStore } from "@/store/user";

const userStore = useUserStore();

// storeToRefs 会跳过所有的 action 属性
const { userInfo, token, newName } = storeToRefs(userStore);

// action 属性直接解构
const { updateUserInfo, updateAge, updateToken } = userStore;

const handleUser = () => {
  updateUserInfo({ name: "lisi", age: 24 });
};

const handleAge = () => {
  // userInfo 是一个 ref 响应式引用,需通过 .value 取值
  updateAge(userInfo.value.age + 1);
};

const handleToken = () => {
  updateToken("23234");
};
</script>

六、异步 actions ​

除同步方法外,action 也可以写成 async 方法。新建 store/list.ts:

ts
import { defineStore } from "pinia";

const getData = () => {
  return new Promise<number>((resolve) => {
    setTimeout(() => {
      resolve(Math.random() * 100);
    }, 200);
  });
};

export const useListStore = defineStore("list", {
  state: () => {
    return {
      list: [] as number[],
    };
  },
  actions: {
    async updateList() {
      try {
        const data = await getData();
        this.list.push(data);
      } catch {
        // 请求失败时不做处理
      }
    },
  },
});

七、store 之间相互引用 ​

每个 store 都可以看成一个 hook,相互引用就是在 action 中调用另一个 store 的 hook。新建 store/userSex.ts:

ts
import { defineStore } from "pinia";
import { useUserStore } from "./user";

enum Sex {
  man = "男人",
  woman = "女人",
}

export const userSexStore = defineStore("user2", {
  state: () => {
    return {
      sex: Sex.man,
    };
  },
  actions: {
    updateSex() {
      const userStore = useUserStore(); // 引用其他 store
      if (userStore.userInfo.name !== "zhangsan") this.sex = Sex.woman;
    },
  },
});

注意:store 的 id 必须全局唯一。这里用 user2 与 store/user.ts 中的 user 区分,避免 id 冲突。

八、在路由守卫中使用 ​

在 router.beforeEach 中可以直接调用 store 的 hook,前提是 Pinia 已被注册。路由器是在其被安装之后才开始导航的,此时 app.use(store) 已经执行完毕:

ts
import { useUserStore } from "@/store/user";

router.beforeEach((to, from) => {
  // 路由器在安装之后才开始导航,此时 Pinia 已经安装
  const userStore = useUserStore();
  if (!userStore.token && to.path !== "/login") {
    return "/login";
  }
});

九、数据持久化 ​

1. 安装插件 ​

bash
yarn add pinia-plugin-persistedstate

2. 注册插件 ​

修改 store/index.ts,在 pinia 实例上使用插件:

ts
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";

const store = createPinia();

store.use(piniaPluginPersistedstate); // 使用持久化插件

export default store;

3. 在 store 模块中启用持久化 ​

在 store/user.ts 的 defineStore 选项中添加 persist 配置项:

ts
import { defineStore } from "pinia";

export const useUserStore = defineStore("user", {
  state: () => ({
    userInfo: { name: "zhangsan", age: 23 },
    token: "S1",
  }),
  getters: {
    newName: (state) => state.userInfo.name + "vip",
  },
  actions: {
    // 与「四、定义 store」中的 actions 相同
  },
  // 开始数据持久化
  persist: true,
});

启用后,每次更新 state 都会默认写入 localStorage。

4. 修改 key 与存储位置 ​

默认写入 localStorage 的 key 就是 store 的 id(此处为 user)。把 persist: true 换成对象即可修改:

ts
persist: {
  key: 'storekey', // 修改存储的键名,默认为当前 store 的 id
  storage: sessionStorage, // 存储位置改为 sessionStorage
},

注意:插件要求 storage 是同步存储,localStorage 与 sessionStorage 都满足,异步存储(如 IndexedDB 封装)不行。

5. 自定义要持久化的字段 ​

默认持久化 state 的所有字段,可以用点符号路径数组只挑选需要缓存的字段:

ts
persist: {
  pick: ['userInfo.name'], // 只持久化 userInfo 的 name
},

需要反向排除字段时用 omit。

注意:pick 是 pinia-plugin-persistedstate v4 的选项名,v3 中同名选项为 paths。升级插件后需同步改配置。