LOADING

pinia

2024/4/13

使用层面

安装

npm install pinia

导入

import { createPinia } from "pinia";
// 创建 pinia 实例
const pinia = createPinia();
createApp(App).use(router).use(pinia).mount("#app");

使用(选项式 API)

该风格基本上和之前的 Vuex 是非常相似的,只不过没有 mutation 了,无论是同步的方法还是异步的方法,都写在 actions 里面。

// 仓库文件
import { defineStore } from "pinia";

// 第二个参数支持两种风格:options api 以及 composition api
export const useCounterStore = defineStore("counter", {
  state: () => {
    return {
      num: 0,
    };
  },
  getters: {
    // 针对上面 state 的数据做一个二次计算
    // 可以看作是计算属性
    doubleCount: (state) => state.num * 2,
  },
  actions: {
    // 同步方法
    increment() {
      this.num++;
    },
    async asyncDecrement() {
      await new Promise((resolve) => setTimeout(resolve, 1000));
      this.decrement();
    },
  },
});

import { storeToRefs } from "pinia";
// 接下来我们可以从仓库中解构数据出来
// 想要拿到响应式数据要包一层storeToRefs
const { num, doubleCount } = storeToRefs(store);

组合式 API

相当于在组件里面写代码,只不过状态是通用的

import { defineStore } from "pinia";
import { reactive, computed } from "vue";

// 引入其他仓库
import { useCounterStore } from "./useCounterStore.js";

export const useListStore = defineStore("list", () => {
  //要使用store,直接调用导出的函数
  const counterStore = useCounterStore();
  // 组合 api 风格

  // 创建仓库数据,类似于 state
  const list = reactive({
    items: [
      {
        text: "学习 Pinia",
        isCompleted: true,
      },
      {
        text: "打羽毛球",
        isCompleted: false,
      },
      {
        text: "玩游戏",
        isCompleted: true,
      },
    ],
    counter: 100,
  });

  // 使用 vue 里面的计算属性来做 getters
  const doubleCounter = computed(() => {
    return list.counter * 2;
  });
  // 接下来我们再来创建一个 getter,该 getter 使用的是其他仓库的数据
  const otherCounter = computed(() => {
    return counterStore.doubleCount * 3;
  });

  // 添加新的事项
  function addItem(newItem) {
    list.items.push({
      text: newItem,
      isCompleted: false,
    });
  }

  // 切换事项对应的完成状态
  function completeHandle(index) {
    list.items[index].isCompleted = !list.items[index].isCompleted;
  }

  // 删除待办事项对应下标的某一项
  function deleteItem(index) {
    list.items.splice(index, 1);
  }

  return {
    list,
    doubleCounter,
    otherCounter,
    addItem,
    completeHandle,
    deleteItem,
  };
});

编写插件

在 Pinia 中可以非常方便的添加插件。一个插件就是一个函数,该函数接收一个 context 上下文对象,通过 context 对象可以拿到诸如 store、app 等信息。

每个插件在扩展内容时,会对所有的仓库进行内容扩展,如果想要针对某一个仓库进行内容扩展,可以通过 context.store.$id 来指定某一个仓库来扩展内容。(每一个仓库都相互独立,会去执行插件里的代码)

使用插件的时候,可以传第三个参数给插件,插件的 context 里面可以拿到这个数据

插件书写完毕后,需要通过 pinia 实例对插件进行一个注册操作。

export function myPiniaPlugin1() {
  // 给所有的仓库添加了一条全局属性
  return {
    secret: "the cake is a lie",
  };
}

export function myPiniaPlugin2(context) {
  //   context里面有你需要用到的所有数据,这个时候都可以进行处理
  const { store } = context;
  store.test = "this is a test";
}

/**
 * 给特定的仓库来扩展内容
 * @param {*} param0
 */
export function myPiniaPlugin3({ store }) {
  if (store.$id === "counter") {
    // 为当前 id 为 counter 的仓库来扩展属性
    return {
      name: "my name is pinia",
    };
  }
}

/**
 * 重置仓库状态
 */
export function myPiniaPlugin4({ store }) {
  // 我们首先可以将初始状态深拷贝一份
  const state = deepClone(store.$state);
  store.reset = () => {
    store.$patch(deepClone(state));
  };
}

使用

// 引入自定义插件
import {
  myPiniaPlugin1,
  myPiniaPlugin2,
  myPiniaPlugin3,
  myPiniaPlugin4,
} from "./plugins";

pinia.use(myPiniaPlugin1);
pinia.use(myPiniaPlugin2);
pinia.use(myPiniaPlugin3);
pinia.use(myPiniaPlugin4);

也可以使用一些第三方插件

import piniaPersistPlugin from "pinia-plugin-persist";
const pinia = createPinia().use(piniaPersistPlugin).use(myPiniaPlugin1);

pinia 和 vuex 的比较

  1. pinia 更加轻量,只有 1kb
  2. pinia 无论是 vue2 还是 vue3 都可以使用
  3. 弃用了 vuex 里面异步使用的 mutation
  4. 在 Pinia 中,组织状态仓库的形式不再采用像 Vuex 一样的嵌套,而是采用的是扁平化的设计,每一个状态仓库都是独立的,这个其实也是 Pinia 这个名字的来源。

vuex 会使用嵌套模块,pinia 是单独模块

import Vuex from "vuex";
import Vue from "vue";
import counter from "./counter";
import loginUser from "./loginUser";
Vue.use(Vuex);

const store = new Vuex.Store({
  modules: {
    counter,
    loginUser,
  },
  strict: true, // 开启严格模式后,只允许通过mutation改变状态
});

//counter
export default {
  state: {
    count: 0,
  },
  mutations: {},
  actions: {},
};

//loginUser
import * as userApi from "../api/user";

export default {
  namespaced: true, // 开启命名空间
  state: {
    loading: false,
    user: null,
  },
  getters: {
    status(state) {
      if (state.loading) {
        return "loading";
      } else if (state.user) {
        return "login";
      } else {
        return "unlogin";
      }
    },
  },
  mutations: {
    setLoading(state, payload) {
      state.loading = payload;
    },
    setUser(state, payload) {
      state.user = payload;
    },
  },
  actions: {
    async login(ctx, payload) {
      ctx.commit("setLoading", true);
      const resp = await userApi.login(payload.loginId, payload.loginPwd);
      ctx.commit("setUser", resp);
      ctx.commit("setLoading", false);
      return resp;
    },
    async whoAmI(ctx) {
      ctx.commit("setLoading", true);
      const resp = await userApi.whoAmI();
      ctx.commit("setUser", resp);
      ctx.commit("setLoading", false);
    },
    async loginOut(ctx) {
      ctx.commit("setLoading", true);
      await userApi.loginOut();
      ctx.commit("setUser", null);
      ctx.commit("setLoading", false);
    },
  },
};

//使用

//调用mutations
this.$store.dispatch("login");

//调用actions
this.$store.commit("setLoading");

最佳实践

  1. 避免直接操作 store 的状态

这样做的好处在于提高了代码的可维护性,应该数据的改变始终来自于 actions 的方法,而不是分散于组件的各个部分。

  1. 使用 TypeScript

Pinia 本身就是使用 typescript 编写的,因此我们在使用 pinia 的时候,能够非常方便的、非常自然的使用 typescript,使用 typescript 可以更好的提供类型检查和代码提示,让我们的代码更加可靠和易于维护。

  1. 将状态划分为多个模块

在一个大型应用中,如果将所有组件的状态放置在一个状态仓库中,那么会显得该状态仓库非常的臃肿。因此一般在大型项目中,是一定会将状态仓库进行拆分的。

订阅功能

订阅就是在某一个仓库的数据改变或者调用某个方法的钩子函数,你可以在数据改变的时候去做任何其他你要做的事情

订阅 state

cartStore.$subscribe((mutation, state) => {
  // import { MutationType } from 'pinia'
  mutation.type // 'direct' | 'patch object' | 'patch function'
  // 和 cartStore.$id 一样
  mutation.storeId // 'cart'
  // 只有 mutation.type === 'patch object'的情况下才可用
  mutation.payload // 传递给 cartStore.$patch() 的补丁对象。

  // 每当状态发生变化时,将整个 state 持久化到本地存储。
  localStorage.setItem('cart', JSON.stringify(state))
})

订阅 action

const unsubscribe = someStore.$onAction(
  ({
    name, // action 名称
    store, // store 实例,类似 `someStore`
    args, // 传递给 action 的参数数组
    after, // 在 action 返回或解决后的钩子
    onError, // action 抛出或拒绝的钩子
  }) => {
    // 为这个特定的 action 调用提供一个共享变量
    const startTime = Date.now()
    // 这将在执行 "store "的 action 之前触发。
    console.log(`Start "${name}" with params [${args.join(', ')}].`)

    // 这将在 action 成功并完全运行后触发。
    // 它等待着任何返回的 promise
    after((result) => {
      console.log(
        `Finished "${name}" after ${
          Date.now() - startTime
        }ms.\nResult: ${result}.`
      )
    })

    // 如果 action 抛出或返回一个拒绝的 promise,这将触发
    onError((error) => {
      console.warn(
        `Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`
      )
    })
  }
)

// 手动删除监听器
unsubscribe()