看完这篇Pinia上手指南,你还会选择Vuex吗?
Vuex作为Vue前端框架官宣的状态管理工具,一直在与时俱进。然而其繁琐的actions、mutations以及模块嵌套一直饱受开发者诟病,我们始终期待着使用一款简洁的状态管理器。而Pinia的出现,去掉了模块的多层嵌套,移除了复杂的mutation操作,极大地满足了我们的诉求。
[TOC]
一、安装
既然决定使用Pinia,那我们在创建Vue项目时就不要选择Vuex。然后使用npm或yarn安装pinia。
# with yarn
yarn add pinia --save
# or with npm
npm install pinia --save
二、创建pinia并挂载到app上
导入createPinia,通过app.use(createPinia())即可, 也可以先赋值给变量pinia,再app.use(pinia)。
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import { createPinia } from "pinia";
const app = createApp(App),
pinia = createPinia();
app.use(pinia).use(router).mount("#app");
三、定义store
Pinia的各个store之间是扁平化的,因此使用起来十分简洁,不像Vuex的module嵌套那么复杂。通过defineStore方法来声明一个store,传入的第一个变量为该store的id,应保证其唯一性;第二个参数为初始化配置,包含state, getters, actions等可选项, 各项都类似Vuex。值得注意的是,state为一个函数,返回一个对象,就如同vue的options API中的data选项。此外,Pinia认为mutations会增加操作复杂度,因此去除了mutations选项有多种方式可以修改state的值,甚至可以直接修改,这些将在后面介绍到。
此处由于我在创建Vue项目的时候无意选择了typescript,就顺势随便写了下interface,使用javascript的筒子可以忽略这些。
// src/store/userStore.ts
// 导入并使用defineStore来定义store
import { defineStore } from "pinia";
interface UserState {
username: string;
password: string;
role: string;
}
export const useUserStore = defineStore("user", {
state: (): UserState => {
return {
username:"Amy",
// ...
};
},
getters: {
// ...
},
actions: {
// ...
},
});
四、store的各个属性介绍
1.state
state为一个函数,其返回值为一个对象,用于存储公共状态。对state或state的属性进行修改的行为叫做mutation。在Pinia中,不提供mutations选项。修改state的mutation行为有多种:
-
直接修改
-
// xxx.vue import { ref, computed, defineComponent } from "vue";import { useUserStore } from "@/store/userStore"; import { useUserStore } from "@/store/userStore"; export default defineComponent({ setup() { const userStore = useUserStore(); const username = computed(()=>userStore.username) userStore.username = "张三"; return {username} } })
-
-
在action里修改 (也属于直接修改)
-
先在actions声明相应的action方法,使用this可以获取store实例:
-
// src/store/userStore.ts // 导入并使用defineStore来定义store import { defineStore } from "pinia"; interface UserState { username: string; password: string; role: string; } export const useUserStore = defineStore("user", { state: (): UserState => { return { username: "", password: "", role: "", }; }, actions: { // 使用this指代本store实例 setUserInfo(username: string): void { this.username = username; }, setRole(role: string): void { this.role = role; }, }, });
-
然后在setup中调用相应的action就行了:
import { useUserStore } from "@/store/userStore"; setup(){ const userStore = useUserStore() userStore.setRole("至尊宝"); }
-
-
使用$patch来修改部分state属性, 可以传入一个对象或者函数
import { useUserStore } from "@/store/userStore"; setup(){ const userStore = useUserStore(); // 使用$patch传入对象来修改state部分属性 // 此时mutation.type为 'patch object' userStore.$patch({ username: "李四", password: "李四爱吃蛋糕", role: "father" })
-
使用$patch传入函数,修改state
// 此时mutation.type为 'patch function' userStore.$patch((state) => { state.username = "紫霞" state.password = "至尊宝" })
-
使用$state来重设整个state,此时传入的对象必须包含state的所有属性
setup(){ // 此时mutation.type为 'patch function' userStore.$state = { username: "狗蛋", password: "gogogo", role: "son" } }
2. 侦听state的变化
使用$subscribe来监测state的变化 (类似watch),接收一个callback函数作为第一个参数,接收的callback主要有mutation和state两个参数,mutation包含修改state的方式、storeId等信息,可以根据相应信息对state进行处理。
userStore.$subscribe((mutation, state) => {
mutation包含修改state的信息
// 修改state的事件信息
console.log(mutation.events);
// 创建store时传入的第一个参数
console.log(mutation.storeId);
// 'direct' | 'patch object' | 'patch function' 三种
// 其中直接修改和在action里直接修改都是对应 'direct'
// $patch一个对象是 'patch object'
// $patch一个函数或者$state重设则是对应 'patch function'
console.log(mutation.type);
// state为修改完之后的state的Proxy
if (mutation.storeId === "user") {
console.log(state)
// 实现自动持久化存储
localStorage.setItem("userInfo", JSON.stringify(state))
}
})
需要注意的是,$subscribe仅在使用的vue组件里生效,一旦该组件被卸载了,将不在继续侦听state的变化。如果我们想要让页面卸载后,$subscribe依然生效,只需要给它传入第二个参数{ detached:true } ,使其脱离组件保持独立,从而在该组件卸载后能继续工作:
export default {
setup() {
const userStore = useUserStore()
// this subscription will be kept after the component is unmounted
userStore.$subscribe(callback, { detached: true })
// ...
}
}
3.getters
-
getters同vue里的computed选项,依赖state的属性并返回一个新的值:
import { defineStore } from "pinia"; export const useUserStore = defineStore("user", { state: (): UserState => { return { username: "", password: "", role: "", }; }, getters: { // 传入参数state, 可解构 authorityLevel: ({ role }) => { return role === "elder" ? 0 : role === "father" ? 1 : role === "son" ? 2 : null; } } });
getters实现接收参数,只需要让其返回一个接收参数的函数即可,此时的getter将不再具有缓存特性。
export const useUserStore = defineStore('user', { getters: { getUserById: (state) => { return (userId) => state.users.find((user) => user.id === userId) }, }, })
我们在vue组件里就可以给相应的getter传参了:
<script> import {useUserStore} from "@/store/userStore.js"; export default { setup() { const store = useUserStore() return { getUserById: store.getUserById } }, } </script> <template> <p>User 8: {{ getUserById(8) }}</p> </template>
-
在getter中可以获取别的getter的值,甚至是其它store里的getter或state的值
// cartStore.ts import { defineStore } from "pinia"; import { useUserStore } from "./userStore"; interface Goods { name: string, price: number, count: number, } type GoodsList = Goods[] interface CartState { items: GoodsList, } export const useCartStore = defineStore("cart", { state: (): CartState => { return { items: [] } }, getters: { owner: () => { // 获取其它store的state和getters const userStore = useUserStore(); return `姓名:${userStore.username}, 权限等级:${userStore.authorityLevel}` } }, })
4.actions
actions类似于vue里的methods选项,其中定义一些方法用于修改state,可以进行异步操作。由于没有mutations选项,因此可以直接在actions中修改state,大大简化了操作。
// src/store/userStore.ts
// 导入并使用defineStore来定义store
import { defineStore } from "pinia";
interface UserState {
username: string;
password: string;
role: string;
}
export const useUserStore = defineStore("user", {
state: (): UserState => {
return {
username: "",
password: "",
role: "",
};
},
actions: {
// 使用this指代本store实例
setUserInfo(username: string): void {
this.username = username;
},
// 可以进行异步操作
setPassword(password: string): void {
const timer = setTimeout(() => {
this.password = password;
clearTimeout(timer);
}, 1000);
},
setRole(role: string): void {
this.role = role;
},
},
});
- 可以在actions中调用自己或其它store里的action:
import { defineStore } from "pinia";
import { useUserStore } from "./userStore";
export const useCartStore = defineStore("cart", {
state: (): CartState => {
return {
items: []
}
},
getters: {
owner: () => {
// 获取其它store的state和getters
const userStore = useUserStore();
return `姓名:${userStore.username}, 权限等级:${userStore.authorityLevel}`
}
},
actions: {
// 使用其它store的action
resetOwnerRole() {
const userStore = useUserStore();
userStore.setRole("elder");
}
// 使用自己store里的其它action,用this指代store实例
callOtherAction(){
this.resetOwnerRole()
}
},
})
五、在setup中使用store
这里介绍一下在Composition API中如何使用store。支持setup语法糖。
类似Vuex中的useStore函数,Pinia也提供了相似的用法,在组件的script标签中导入我们自定义的Store函数,调用后赋值给相应的变量即可。state和getters都能直接访问,可以使用computed使被赋值的变量变为响应式。
// xxx.vue
import { ref, computed, defineComponent } from "vue";
import { useUserStore } from "@/store/userStore";
export default defineComponent({
setup() {
const userStore = useUserStore(),
// state
username = computed(() => userStore.username),
// 使用computed, 则password成为了响应式数据,而username不是。
password = computed(() => userStore.password),
// getters
authority = computed(() => userSore.authorityLevel)
return {
username,
password,
authority
}
}
})
六、在setup外面使用store
需要注意useStore使用的时机,需要在app挂载pinia之后才能使用,以在路由守卫中为例:
// src/router/index.ts
// ! 无效,会报错还未安装pinia
// const userStore = useUserStore();
router.beforeEach((to, from, next) => {
// 有效, 此时vue已经挂载了router,则也挂载了pinia
const userStore = useUserStore();
to.path === "/about" && userStore.role === "" && next("/login");
next();
});
七、小结
-
安装: 可使用npm或yarn安装,注意创建项目时不选vuex。
-
挂载: 在main.js中创建并挂载pinia。
-
定义: 在store/myStore.js中导入并使用defineStore来定义store,传入一个字符串作为storeId,并初始化state,getters,actions,导出useMyStore函数。
-
state
-
state是一个函数,返回一个对象,类似options API中的data;
-
修改state的行为成为mutation,Pinia中没有mutation选项,但是有多种修改state的方式。
-
直接在setup中访问store实例的属性并修改。mutation类型属于'direct'
-
在actions中通过this访问并修改state的属性,并在setup中调用相应的action即可。mutation类型属于'direct'
-
在setup中通过store实例的$patch(),传入一个对象来局部修改state。mutation类型属于'patch object'
-
在setup中通过store实例的$patch(),传入一个函数,该函数返回一个对象来局部修改state。mutation类型属于'patch function'
-
在setup中通过store实例的$state来整体重设state,传入一个新的对象作为新的state的数据,此时传入的对象需包含state的全部属性。mutation类型属于'patch function'
-
-
侦听state的变化:通过store实例的$subscribe方法来监测state的变化,接收一个callback,callback有mutation和state两个参数。其中mutation包含修改state的信息,state参数为修改后的state,可以在此处对state进行持久化存储等操作。
- getters
-
getters类似vue的computed,getters依赖于state且具有缓存效果,接收state作为参数,且通过state访问其属性
-
getters可以通过this访问本store实例中的其它getters
-
getters里可以访问其它store实例的getters和state,需要导入其它的store
-
getters可以通过返回一个函数的方式来实现接收参数,但此时它会失去缓存效果
- actions
-
类似vue中的methods,可以修改state的值,可以进行异步操作
-
可以通过this访问本store实例的getters、actions等
-
可以调用其它store里的actions,需要导入其它的store
- 在setup中访问store
导入useMyStore函数并实例化store,即可访问其state,getters,以及调用actions或其它API。
- 在路由守卫中使用store
需要在路由钩子函数(如beforEach)内部使用,此时pinia已成功挂载到app实例上。若在路由钩子函数外部使用,则会报错,提示pinia未正确安装。
关于Pinia上手体验就到此结束啦。有空再出一期Pinia插件和在ssr中的使用。感兴趣的同学可以去查阅Pinia官方文档,有详细说明。