◆

Vue Pinia 状态管理入门资源

2023-02-02 01:370

(官网域名后来迁到了 https://pinia.vuejs.org,pinia.esm.dev 会跳转过去。)

两种写 store 的方式

Pinia 的 defineStore 支持两种风格,功能等价,选哪个看团队习惯。

Options 风格,结构上最接近 Vuex,迁移时改动最小:

// stores/counter.js
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0, name: 'wfly' }),
  getters: {
    double: state => state.count * 2,
  },
  actions: {
    // 注意这里必须用普通函数,箭头函数会丢掉 this
    increment() {
      this.count++;
    },
    async fetchCount() {
      const res = await fetch('/api/count');
      this.count = (await res.json()).count;
    },
  },
});

Setup 风格,写法和组件的 setup() 完全一致:

import { ref, computed } from 'vue';
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0);
  const double = computed(() => count.value * 2);
  function increment() {
    count.value++;
  }
  // 必须把要暴露的东西都 return 出去,漏掉的外部访问不到
  return { count, double, increment };
});

Setup 风格的好处是可以直接用组合式函数(useRoute、useLocalStorage 之类)和 watch,灵活度高。代价是 SSR 场景下要多留意,且 $reset() 默认不可用——Options 风格靠 state 那个工厂函数就能重建初始状态,Setup 风格 Pinia 不知道初始值是什么,需要自己实现:

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0);
  function $reset() {
    count.value = 0;
  }
  return { count, $reset };
});

解构会丢响应性

这是新手最容易踩的坑。store 实例是一个 reactive 对象,直接解构会把响应式属性变成普通值:

const store = useCounterStore();
// 错:count 是一次性快照,store.count 变了这里不会更新
const { count } = store;

// 对:storeToRefs 会把 state 和 getter 都转成 ref
import { storeToRefs } from 'pinia';
const { count, double } = storeToRefs(store);

storeToRefs 只处理 state 和 getter,会跳过 action。action 是普通函数,解构出来不影响使用(Pinia 已经把 this 绑好了):

// action 直接解构没问题
const { increment } = store;
increment();

修改 state 的几种方式

const store = useCounterStore();

// 直接改,最常用
store.count++;

// 批量改,只触发一次订阅回调
store.$patch({ count: 10, name: 'new' });

// 函数式 patch,改数组/复杂结构时用这个
store.$patch(state => {
  state.items.push({ id: 1 });
  state.hasChanged = true;
});

// 整体替换(保留未提及的字段用 $patch,全量替换用 $state 赋值)
store.$state = { count: 0, name: 'wfly' };

// 重置为初始状态(仅 Options 风格开箱可用)
store.$reset();

对象形式的 $patch 是深合并,函数形式则给你直接操作 state 的机会。往数组里 push 大量元素时函数形式性能明显更好,因为对象形式需要 Pinia 遍历比对。

相比 Vuex 的实际差别

原文那篇对比文章值得看,我补几条在项目里体感最强的:

没有 mutation。 Vuex 要求同步修改走 mutation、异步走 action,实际写起来大量 mutation 只是一行赋值,纯样板代码。Pinia 只有 action,同步异步都行。

没有嵌套模块。 Vuex 的 modules 加上 namespaced: true 之后,dispatch('user/profile/fetch') 这种字符串路径既没有类型提示也容易写错。Pinia 是扁平的多 store,store 之间要互相引用就直接调用对方的 useXxxStore():

// stores/cart.js
import { useUserStore } from './user';

export const useCartStore = defineStore('cart', {
  actions: {
    checkout() {
      const user = useUserStore(); // 在 action 内部调用,此时 Pinia 已激活
      if (!user.isLogin) return;
      // ...
    },
  },
});

注意 useUserStore() 要写在 action 内部而不是模块顶层。写在顶层会在 app.use(pinia) 之前执行,报 getActivePinia() was called but there was no active Pinia。

TypeScript 类型自动推导。 Vuex 4 的类型支持基本靠手写 declare module 补,Pinia 不需要任何额外声明,state / getter / action 的类型全自动推出来。这是我从 Vuex 换过来最主要的动力。

体积更小,约 1.5kb。 且支持 tree shaking,没用到的 store 不会进产物。

常用的两个进阶点

订阅变化,做持久化或日志:

store.$subscribe((mutation, state) => {
  // mutation.type: 'direct' | 'patch object' | 'patch function'
  localStorage.setItem('counter', JSON.stringify(state));
});

// 监听 action 的调用、成功和失败
store.$onAction(({ name, args, after, onError }) => {
  after(result => console.log(`${name} 成功`, result));
  onError(err => console.error(`${name} 失败`, err));
});

默认情况下组件卸载时订阅会被自动清理,想让它跟着 store 生命周期就传 { detached: true }。

插件机制,给所有 store 统一加能力:

// 给每个 store 注入一个 $router
pinia.use(({ store }) => {
  store.$router = markRaw(router);
});

持久化通常不用自己写,pinia-plugin-persistedstate 已经做得很完整,配一个 persist: true 就行。

💬留言板0

还没有留言,来发表第一条吧~

PAGE LOAD / LIVEVue Pinia 状态管理入门资源加载中