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

Vuex作为Vue前端框架官宣的状态管理工具,一直在与时俱进。然而其繁琐的actions、mutations以及模块嵌套一直饱受开发者诟病,我们始终期待着使用一款简洁的状态管理器。而Pinia的出现,去掉了模块的多层嵌套,移除了复杂的mutation操作,极大地满足了我们的诉求。

一、安装

既然决定使用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
    }
  }
})

如果你觉得这样的方式比较麻烦那么请看下面的这种方式

// xxx.vue

import { ref, computed, defineComponent } from vue;
import { storeToRefs } from "pinia"
import { useUserStore } from @/store/userStore;
export default defineComponent({
  setup() {
    const store = useUserStore(),
      // 方案1:可以利用从pinia中引用的storeToRefs方法,进行解构赋值 再return出去 类似 vue3 中的 toRefs
      const {username,password}  = storeToRefs(store)
      return {
        store, //方案:抛出整个store
        username,
        password
      }
    }
})

六、在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();
});

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

Creativity requires the courage to let go of certainties.