前言

Vuex 4 是适配 Vue 3 的新版本,由官方推出,集成到 vue-devtools,拥有时光旅行调试的功能。
选择官方推出的库在一般情况下都是一个好选择,但是在TypeScript开发的情况下,单一的Vuex 4可能不是一个好的选择。

Vuex 4存在的问题

我们不能否认Vuex 4的功能强大,状态管理大部分最终也都会走向Flux架构。但是在我们项目不是特别大的情况下,杀鸡焉用牛刀。

不推荐Vuex 4除了上述原因外,最主要的原因就是TypeScript的支持不够完善。Vuex 4仅对State、Getter提供了较为完善的类型声明,而对于Mutation、Action的支持就有心无力了,这是由于以下几点:

  • Vuex短期难以解决TypeScript支持。这是由于代码架构的问题,Vuex 4在设计时,就没有像Vue3那样使用TypeScript重构全部代码。
  • 开发者明确表示,不打算为Vuex3、4重写TypeScript代码库。
  • 对于 vuex issue 中对于社区提供的一些TypeScript支持方案,开发者表示将会在Vuex 5中采用。这也说明要完善的vuexts支持,至少得等到Vuex5出来。

image.png
image.png

状态管理 - data

我们可以像官方文档那样使用 data 从零打造简单状态管理

状态管理 - Composition API

得益于Vue 3 的 Composition API 与 ProvideInject 的搭配,我们可以轻松的自己搭建出一套简易的状态管理,且很好的支持TypeScript。
可以先参考前面的文档了解 Provide / Inject - 提供 / 注入 的使用。
接下来我们一步步逐步完成自制的简易状态管理。

注意:我们这里的多个模块就代表着多个分离的store,而不是像vuex那样,一个store下有多个子模块。

极简版

首先我们先制作一个简易的版本,要满足以下的目标:

  • 保留三个属性:StateGetterAction。state存储数据,getter类似计算属性,action用于存放修改state的方法。
  • 所有属性只读,state只能通过action修改。
  • StateGetterAction 提供完善的ts类型声明

    1. 创建 store 对象

    首先,我们在src下创建一个 store.ts 的文件,创建一个store对象:

  • 使用 reactiveState 创建响应式对象。

  • 使用 computedGetter 创建计算属性。
  • 使用 readonly 包裹整个 store,让所有属性只读。 ```typescript // src/store.ts import { reactive, computed, readonly } from ‘vue’

const state = reactive({ name: ‘韩梅梅’, age: 18, })

const getters = { userInfo: computed(() => { return state.name }), }

const actions = { setName: (name: string) => { state.name = name }, }

export default readonly({ state, getters, actions, })

  1. 创建好store对象后,我们只需要在Vue根实例上注入,然后就能在每个Vue组件中使用了。
  2. <a name="b51gt"></a>
  3. ### 2. provide(提供)store 对象
  4. `App.vue` 中注入 store 对象:
  5. ```typescript
  6. // App.vue
  7. import { defineComponent, provide } from 'vue'
  8. import store from '@/store'
  9. export default defineComponent({
  10. name: 'App',
  11. setup() {
  12. // readonly用于确保所有属性都是只读的
  13. provide('user', store)
  14. },
  15. })

3. 在组件中 inject(注入)并使用

在vue组件中使用:

  1. // HelloWord.vue
  2. import { defineComponent, inject, onMounted } from 'vue'
  3. import store from '@/store'
  4. export default defineComponent({
  5. name: 'HelloWord',
  6. setup() {
  7. // 传入store作为默认值
  8. const userStore = inject('user', store)
  9. onMounted(() => {
  10. console.log(userStore.state.name) // 韩梅梅
  11. })
  12. },
  13. })

使用 inject 接收 App.vue 注入的 user 模块的store。并引入 store 对象,提供默认值的同时也给返回值提供ts类型声明。

极简版存在的问题

现在一个简易的状态管理已经可用了,但是还不够好用,且存在一定的风险:

1. 多次 provide

如果不止一个store模块,每新增一个模块,我们就得在 App.vueprovide 一次。操作繁琐。

2. 未知 inject 的第一个参数

当我们使用 inject 注入store时,编辑器不清楚要 inject 的 property 名称有哪些。只能通过我们手动查看哪些 property 是已经 provide 了的。

3. 类型声明不保险

上述代码中,我们给store添加类型声明的方式,是通过引入我们要引用的store对象,传入 inject 第二个参数中,作为默认值的同时,提供类型声明。

但这样也存在很大风险,在有多个模块的情况下,我们就要inject多次,要引入多个store模块。
万一代码写错了,第一个参数传入的 property 和 第二个参数传入的 store 对象对应不上的话,编辑器不会告诉我们有任何错误。

完整版

鉴于以上的问题,我们需要改进引入时的方式,以达成新的目标。

组件内使用store时,需要提供的类型声明和校验有:

  • 当前有哪些模块的 store 已经存在且可以引入
  • 传入property 名称后,返回对应模块的 store 对象,并提供完善的类型声明

要达成以上的目标,我们需要改进这个状态管理。

1. 起步

我们想要达成上述目标,首先需要一个可以给provide和inject共享的对象,这个对象存着所有模块以及这些模块对应的模块名。

现在我们在 src 下创建一个 store 目录。
并在 store 中创建一个 index.ts 文件和一个 modules 文件夹。

用途:modules 用于存放所有的模块, index.ts作为统一的导出。

2. 创建一个 store 模块

我们先按照极简版的模板,创建一个store模块:

  1. // src/store/modules/user.ts
  2. import { reactive, computed, readonly } from 'vue'
  3. const state = reactive({
  4. name: '韩梅梅',
  5. age: 18,
  6. })
  7. const getters = {
  8. userInfo: computed(() => {
  9. return state.name
  10. }),
  11. }
  12. const actions = {
  13. setName: (name: string) => {
  14. state.name = name
  15. },
  16. }
  17. export default readonly({
  18. state,
  19. getters,
  20. actions,
  21. })

3. 创建 提供/注入 store 模块的方法

我们现在不直接在组件中使用provide和inject。因为这样就不能满足我们的目标。

我们需要创建两个新的函数:
createStore:用于provide提供store模块
useStore:用于inject注入store模块,在组件内访问store。

为了让这两个函数读取到模块名和模块映射的对象,我们将这两个函数都放在index.ts中:

  1. // src/store/index.ts
  2. import { inject, provide } from 'vue'
  3. import userModule from './modules/user'
  4. const modules = {
  5. user: userModule,
  6. }
  7. // 创建store,在App.vue调用该方法,自动注入所有模块的store
  8. export const createStore = () => {
  9. for (const key in modules) {
  10. if (Object.prototype.hasOwnProperty.call(modules, key)) {
  11. const store = modules[key as keyof typeof modules]
  12. provide(key, store)
  13. }
  14. }
  15. }
  16. // 组件内使用store,模块名会依照modules的值提供类型提示
  17. export const useStore = <T extends keyof typeof modules>(key: T) => {
  18. return inject(key, modules[key])
  19. }

得益于Composition API,我们可以直接将provide和inject封装在函数内。这样就能让inject能获取到所有模块的模块名和值,从而给出完整的ts声明。接下来我们来看下如何使用及效果。

4. createStore - 提供 store 对象

先在 App.vue 中注册:

  1. // App.vue
  2. import { defineComponent } from 'vue'
  3. import { createStore } from '@/store'
  4. export default defineComponent({
  5. name: 'App',
  6. setup() {
  7. createStore()
  8. },
  9. })

5. useStore - 注入并使用 store对象

在组件中使用:

  1. import { defineComponent, onMounted } from 'vue'
  2. import { useStore } from '@/store'
  3. export default defineComponent({
  4. name: 'HelloWord',
  5. setup() {
  6. const userStore = useStore('user')
  7. onMounted(() => {
  8. console.log(userStore.state.userAge) // number 18
  9. })
  10. },
  11. })

6. 查看TypeScript支持效果

现在我们使用起来更加的方便了,而且拥有了完善的类型声明提示。我们来看看效果:

  1. import { defineComponent, onMounted } from 'vue'
  2. import { useStore } from '@/store'
  3. export default defineComponent({
  4. name: 'HelloWord',
  5. setup() {
  6. const userStore = useStore('user')
  7. // Argument of type '"other"' is not assignable to parameter of type '"user"'
  8. // 提示不存在 other 这个模块
  9. const otherStore = useStore('other')
  10. onMounted(() => {
  11. console.log(userStore.state.userAge) // number 18
  12. // Cannot assign to 'userAge' because it is a read-only property.
  13. // 提示不可修改只读属性
  14. userStore.state.userAge = 20
  15. // Property 'xxx' does not exist on type '{ readonly userName: string; readonly userAge: number; }'.
  16. // 提示user模块上的state不存在名为 xxx 的属性
  17. userStore.state.xxx
  18. })
  19. },
  20. })

到了这一步我们的状态管理已经完成了。简便易用且拥有完善的TypeScript支持。
当然还可以添加其他功能,如与本地存储sessionStorage、localStorage同步,store方法调用记录等,这些就可以自己去扩展了。

扩展 - 与 浏览器存储 同步

接下来提供一份与本地存储sessionStorage、localStorage同步的完整版示例。
组件内的使用方法以及创建store模块的方法与上面的代码没有区别,因此只提供 index.ts 的示例。

index:

  1. // src/store/index.ts
  2. import { inject, watch, onMounted, provide } from 'vue'
  3. import userModule from './modules/user'
  4. const modules = {
  5. user: userModule,
  6. }
  7. // store 与 sessionStorage 或 localStorage 同步
  8. const usePersist = (
  9. state: { [index: string]: unknown },
  10. key: string,
  11. storage: 'sessionStorage' | 'localStorage'
  12. ) => {
  13. onMounted(() => {
  14. const userStoreState = window[storage].getItem(key + '_persist_storage')
  15. if (userStoreState) {
  16. const data = JSON.parse(userStoreState)
  17. for (const dataKey in state) {
  18. state[dataKey] = data[dataKey]
  19. }
  20. }
  21. })
  22. watch(state, val => {
  23. window[storage].setItem(key + '_persist_storage', JSON.stringify(val))
  24. })
  25. }
  26. // 注入 store
  27. export const useStore = <T extends keyof typeof modules>(key: T) => {
  28. return inject(key, modules[key])
  29. }
  30. // 提供 store,传参表示是否需要与浏览器存储同步,默认不同步
  31. export const createStore = (persistType: 'none' | 'sessionStorage' | 'localStorage' = 'none') => {
  32. for (const key in modules) {
  33. if (Object.prototype.hasOwnProperty.call(modules, key)) {
  34. const store = modules[key as keyof typeof modules]
  35. if (persistType !== 'none') {
  36. usePersist(store.state, key, persistType)
  37. }
  38. provide(key, store)
  39. }
  40. }
  41. }