介绍

Pinia.js 是新一代的状态管理器,由 Vue.js团队中成员所开发的,因此也被认为是下一代的 Vuex,即 Vuex5.x,在 Vue3.0 的项目中使用也是备受推崇。
Pinia.js 有如下特点:

  • 完整的 typescript 的支持;
  • 足够轻量,压缩后的体积只有1.6kb;
  • 去除 mutations,只有 state,getters,actions(这是我最喜欢的一个特点);
  • actions 支持同步和异步;
  • 没有模块嵌套,只有 store 的概念,store 之间可以自由使用,更好的代码分割;
  • 无需手动添加 store,store 一旦创建便会自动添加;

    开始使用

    安装 Pinia

    1. $ yarn add pinia
    2. $ npm i pinia
    3. $ pnpm i pinia

    创建Store

    根据Pinia官方给的格式建议,我们可以在src目录下创建store目录,再在其下创建index.ts文件作为store的全局配置文件,并导出store,方便全局挂载。 ```typescript //src/store/index.ts import {App} from “vue” import {createPinia} from “pinia”

const store = createPinia()

export function setupStore(app:App){ app.use(store) }

export {store}

  1. main.ts文件中进行全局挂载store文件:
  2. ```typescript
  3. //src/main.ts
  4. import { createApp } from 'vue'
  5. import App from './App.vue'
  6. import {setupStore} from "@stores/index"
  7. const app = createApp(App);
  8. // 挂载store
  9. setupStore(app)
  10. app.mount("#app")

简单示例

先定义一个counter的store状态管理文件。

  1. //src/store/modules/counter.ts
  2. import {defineStore} from "pinia"
  3. export const useCounterStore = defineStore("counter",{
  4. state:()=>{
  5. return {
  6. count:0
  7. }
  8. },
  9. actions:{
  10. increment(){
  11. this.count++
  12. }
  13. }
  14. })

而后在vue文件进行使用:

  1. <template>
  2. <div>count:{{counter.count}}</div>
  3. </template>
  4. <script lang="ts" setup>
  5. import { useCounterStore } from '@store/modules/counter';
  6. const counter = useCounterStore();
  7. counter.count++
  8. counter.$patch({count:counter.count})
  9. counter.increment()
  10. </script>

显示结果:
image.png

State 数据状态

定义state状态

在使用仓库状态数据前,你需要先使用defineStore()进行定义一个仓库,需要使用唯一的名字进行命名。

  1. //src/store/modules/counter.ts
  2. import {defineStore} from "pinia"
  3. export const useCounterStore = defineStore("counter",{
  4. state:()=>{
  5. return {
  6. count:0
  7. }
  8. }
  9. })

使用state状态

在pinia中,state是使用函数进行定义,并返回初始化数据,这样可以允许pinia作为服务端和客户端进行使用。
我们已经定义了一个store仓库,使用useStore()进行获取仓库state数据。

  1. <template>
  2. <div>count:{{count}}</div>
  3. <div>doubleCount:{{doubleCount}}</div>
  4. </template>
  5. <script lang="ts" setup>
  6. import { useCounterStore } from '@store/modules/counter';
  7. const counter = useCounterStore();
  8. //使用computed获取响应式数据
  9. const doubleCount = computed(()=>counter.count*2);
  10. const {count} = counter;
  11. </script>

需要注意的是,在导入store使用state状态时,直接进行解构会使得变量失去响应式。上面的数据中,count变量是失去了响应式的,它的值一直恒定不变。对此,应该像composition API使用props一样,从pinia中导出storeToRefs进行获取响应式数据。

  1. const counter = useCounterStore();
  2. const {count} = storeToRefs(counter)

对此,count现在转为响应式的数据了,这样将会把store中的state变量成了响应式,storeToRefs将会自动跳过所有的actions和不需要进行响应式处理的属性。

修改state状态

通常,你可以通过store实例进行直接获取和修改state数据,可以使用store.$reset()进行重置state的数据回复到初始值。

  1. import { useCounterStore } from '@stores/modules/counter';
  2. const counter = useCounterStore();
  3. counter.count++;

当然,你还可以通过使用pinia的内置API即$patch()进行更新,它允许你在同一时间进行多个数据的改变。$patch()允许你去组合多个改变的值,可以像数组进行增删改查。

  1. import { useTodosStore } from "@stores/modules/todos";
  2. const todosStore = useTodosStore()
  3. todosStore.$patch(state=>{
  4. state.todos.push({
  5. name:"yichuan",
  6. age:18
  7. })
  8. state.isTrue = true
  9. })

$patch()允许你去组合多个改变的值,可以像数组进行增删改查。
替换state的值,可以通过设置$state的值进行替换store中的state值。

  1. store.$state = {counter:666, name:"onechuan"}

当然也可以通过pinia的实例去改变整个state的值。

  1. pinia.state.value= {}

但是,一般不建议直接修改state的值,Pinia中建议使用actions方法去修改和管理state的值。

监听state状态变化

订阅state的值:你可以通过store的$subscribe()方法去观察state的改变,类似于subscribe方法。与常规watch()相比,使用$subscribe()的优势在于,在补丁发布后,订阅只会触发一次。

  1. numerStore.$subscribe((mutation,state)=>{
  2. mutation.counter
  3. mutation.name
  4. mutation.isAdmin
  5. localStorage.setItem("numer",JSON.stringify(state))
  6. })

默认情况下,状态订阅被绑定到添加它们的组件上(如果存储在组件的setup()中)。这就以为着当组件被卸载的时候,将自动移除。如果你想再组件被移除后继续保持它们,可以通过设置{detached:true}作为第二个参数来从当前组件中分离状态订阅。

  1. const someStore = useSomeStore()
  2. someStore.$subscribe(callback, { detached: true })

Getters

getters与store状态的computed的值完全相同,可以通过defineStore()中的getters属性来定义,它们通过接收state作为箭头函数的第一个参数。

  1. export const useStore = defineStore('counter', {
  2. state: () => ({
  3. counter: 0,
  4. }),
  5. getters: {
  6. doubleCount: (state) => state.counter * 2,
  7. },
  8. })

绝大多数情况,getters将只依赖于state,然而,它们也有可能需要去使用其它的getters。因此,在定义一个普通函数时,我们可以通过这个函数访问整个store实例,但需要定义返回类型的类型。由于ts中的已知的限制,并不影响使用箭头函数定义的getter和不使用this。

  1. import {defineStore} from "pinia"
  2. export const useNumerStore = defineStore("numer",{
  3. state:()=>({
  4. counter:0,
  5. name:"numer",
  6. isAdmin:true
  7. }),
  8. getters:{
  9. doubleCount(state){
  10. return state.counter * 2
  11. },
  12. // 当使用this的时候,必须准确地设置返回值的类型
  13. doublePlusOne():number{
  14. return this.counter * 2 + 1
  15. }
  16. }
  17. })

当然你也可以通过计算属性去获取多个getters,需要通过this去获取任意的其他getter。

  1. export const useStore = defineStore("main",{
  2. state:()=>({
  3. counter:0
  4. }),
  5. getters:{
  6. doubleCount:state=>state.counter * 2,
  7. doubleCountPlusOne(){
  8. return this.doubleCount + 1
  9. }
  10. }
  11. })

getter只是在幕后的计算属性,因此不能向其传递任何参数。但是你可以从getter返回一个函数来接收任何参数。

  1. export const useStore = defineStore('main', {
  2. getters: {
  3. getUserById: (state) => {
  4. return (userId) => state.users.find((user) => user.id === userId)
  5. },
  6. },
  7. })

在组件使用:

  1. <div>用户:{{getUserById(1)}}</div>
  2. <script setup lang="ts">
  3. const numerStore = useNumerStore()
  4. const {getUserById} = numerStore
  5. </script>

注意,当这样做时,getter不再被缓存,它们只是您调用的函数。不过,您可以在getter本身中缓存一些结果,这并不常见,但应该会证明更高效.

  1. export const useStore = defineStore('main', {
  2. getters: {
  3. getActiveUserById(state) {
  4. const activeUsers = state.users.filter((user) => user.active)
  5. return (userId) => activeUsers.find((user) => user.id === userId)
  6. },
  7. },
  8. })

获取其它store的getters,要使用另一个store getter,你可以直接在getter中使用它,其实和在vue文件中使用差别不大。

  1. import { useOtherStore } from './other-store'
  2. export const useStore = defineStore('main', {
  3. state: () => ({
  4. // ...
  5. }),
  6. getters: {
  7. otherGetter(state) {
  8. const otherStore = useOtherStore()
  9. return state.localData + otherStore.data
  10. },
  11. },
  12. })

Actions

actions等价于组件中的方法,它们可以在defineStore()中进行定义actions属性,并且可以完美地去定义业务逻辑。

  1. export const useStore = defineStore('main', {
  2. state: () => ({
  3. counter: 0,
  4. }),
  5. actions: {
  6. increment() {
  7. this.counter++
  8. },
  9. randomizeCounter() {
  10. this.counter = Math.round(100 * Math.random())
  11. },
  12. },
  13. })

在上面的代码中,我们可以看到actions有点类似getters,但事实上是有所不同的。

  • 相同点:actions和getters都可以全类型支持来访问整个store实例。
  • 不同点:actions操作可以是异步的,可以在其中等待任何api调用甚至其他操作。

注意,只要你得到了一个Promise,你使用的库并不重要,你甚至可以使用本地的fetch函数(仅适用于浏览器):

  1. import { mande } from 'mande'
  2. const api = mande('/api/users')
  3. export const useUsers = defineStore('users', {
  4. state: () => ({
  5. userData: null,
  6. // ...
  7. }),
  8. actions: {
  9. async registerUser(login, password) {
  10. try {
  11. this.userData = await api.post({ login, password })
  12. showTooltip(`Welcome back ${this.userData.name}!`)
  13. } catch (error) {
  14. showTooltip(error)
  15. // let the form component display the error
  16. return error
  17. }
  18. },
  19. },
  20. })

同样的,action也可以像state和getters进行相互使用,action可以通过this直接访问。

  1. // src/store/user.ts
  2. export const useUserStore = defineStore({
  3. "user",
  4. state: () => ({
  5. userData: null
  6. }),
  7. actions:{
  8. async login(account, pwd) {
  9. const { data } = await api.login(account, pwd)
  10. this.setData(data) // 调用另一个 action 的方法
  11. return data
  12. },
  13. setData(data) {
  14. this.userData = data
  15. }
  16. }
  17. })

也可以在action 里调用其他 store 里的 action,引入对应的 store 后即可访问其内部的方法了。

  1. // src/store/user.ts
  2. import { useAppStore } from './app'
  3. export const useUserStore = defineStore({
  4. id: 'user',
  5. actions: {
  6. async login(account, pwd) {
  7. const { data } = await api.login(account, pwd)
  8. const appStore = useAppStore()
  9. appStore.setData(data) // 调用 app store 里的 action 方法
  10. return data
  11. }
  12. }
  13. })
  1. // src/store/app.ts
  2. export const useAppStore = defineStore({
  3. "app",
  4. state:()=>{
  5. userData: null
  6. },
  7. actions: {
  8. setData(data) {
  9. this.userData = data
  10. }
  11. }
  12. })

参考

【1】可爱简约又轻量的Pinia,你确定不用它吗?