看完这篇Pinia上手指南,你还会选择Vuex吗?

看完这篇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主要有mutationstate两个参数,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();
});
复制代码

七、小结

  1. 安装: 可使用npm或yarn安装,注意创建项目时不选vuex。

  2. 挂载: 在main.js中创建并挂载pinia。

  3. 定义: 在store/myStore.js中导入并使用defineStore来定义store,传入一个字符串作为storeId,并初始化state,getters,actions,导出useMyStore函数。

  4. 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进行持久化存储等操作。

  1. getters
  • getters类似vue的computed,getters依赖于state且具有缓存效果,接收state作为参数,且通过state访问其属性

  • getters可以通过this访问本store实例中的其它getters

  • getters里可以访问其它store实例的getters和state,需要导入其它的store

  • getters可以通过返回一个函数的方式来实现接收参数,但此时它会失去缓存效果

  1. actions
  • 类似vue中的methods,可以修改state的值,可以进行异步操作

  • 可以通过this访问本store实例的getters、actions等

  • 可以调用其它store里的actions,需要导入其它的store

  1. 在setup中访问store

导入useMyStore函数并实例化store,即可访问其state,getters,以及调用actions或其它API。

  1. 在路由守卫中使用store

需要在路由钩子函数(如beforEach)内部使用,此时pinia已成功挂载到app实例上。若在路由钩子函数外部使用,则会报错,提示pinia未正确安装。

关于Pinia上手体验就到此结束啦。有空再出一期Pinia插件和在ssr中的使用。感兴趣的同学可以去查阅Pinia官方文档,有详细说明。

おすすめ

転載: juejin.im/post/7068483852674531335