使用层面
安装
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 的比较
- pinia 更加轻量,只有 1kb
- pinia 无论是 vue2 还是 vue3 都可以使用
- 弃用了 vuex 里面异步使用的 mutation
- 在 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");
最佳实践
- 避免直接操作 store 的状态
这样做的好处在于提高了代码的可维护性,应该数据的改变始终来自于 actions 的方法,而不是分散于组件的各个部分。
- 使用 TypeScript
Pinia 本身就是使用 typescript 编写的,因此我们在使用 pinia 的时候,能够非常方便的、非常自然的使用 typescript,使用 typescript 可以更好的提供类型检查和代码提示,让我们的代码更加可靠和易于维护。
- 将状态划分为多个模块
在一个大型应用中,如果将所有组件的状态放置在一个状态仓库中,那么会显得该状态仓库非常的臃肿。因此一般在大型项目中,是一定会将状态仓库进行拆分的。
订阅功能
订阅就是在某一个仓库的数据改变或者调用某个方法的钩子函数,你可以在数据改变的时候去做任何其他你要做的事情
订阅 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()