前言
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出来。
状态管理 - data
我们可以像官方文档那样使用 data 从零打造简单状态管理 。
状态管理 - Composition API
得益于Vue 3 的 Composition API 与 Provide、Inject 的搭配,我们可以轻松的自己搭建出一套简易的状态管理,且很好的支持TypeScript。
可以先参考前面的文档了解 Provide / Inject - 提供 / 注入 的使用。
接下来我们一步步逐步完成自制的简易状态管理。
注意:我们这里的多个模块就代表着多个分离的store,而不是像vuex那样,一个store下有多个子模块。
极简版
首先我们先制作一个简易的版本,要满足以下的目标:
- 保留三个属性:
State、Getter、Action。state存储数据,getter类似计算属性,action用于存放修改state的方法。 - 所有属性只读,state只能通过action修改。
给
State、Getter、Action提供完善的ts类型声明1. 创建 store 对象
首先,我们在src下创建一个
store.ts的文件,创建一个store对象:使用
reactive给State创建响应式对象。- 使用
computed给Getter创建计算属性。 - 使用
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, })
创建好store对象后,我们只需要在Vue根实例上注入,然后就能在每个Vue组件中使用了。<a name="b51gt"></a>### 2. provide(提供)store 对象在 `App.vue` 中注入 store 对象:```typescript// App.vueimport { defineComponent, provide } from 'vue'import store from '@/store'export default defineComponent({name: 'App',setup() {// readonly用于确保所有属性都是只读的provide('user', store)},})
3. 在组件中 inject(注入)并使用
在vue组件中使用:
// HelloWord.vueimport { defineComponent, inject, onMounted } from 'vue'import store from '@/store'export default defineComponent({name: 'HelloWord',setup() {// 传入store作为默认值const userStore = inject('user', store)onMounted(() => {console.log(userStore.state.name) // 韩梅梅})},})
使用 inject 接收 App.vue 注入的 user 模块的store。并引入 store 对象,提供默认值的同时也给返回值提供ts类型声明。
极简版存在的问题
现在一个简易的状态管理已经可用了,但是还不够好用,且存在一定的风险:
1. 多次 provide
如果不止一个store模块,每新增一个模块,我们就得在 App.vue 中 provide 一次。操作繁琐。
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模块:
// src/store/modules/user.tsimport { 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,})
3. 创建 提供/注入 store 模块的方法
我们现在不直接在组件中使用provide和inject。因为这样就不能满足我们的目标。
我们需要创建两个新的函数:createStore:用于provide提供store模块useStore:用于inject注入store模块,在组件内访问store。
为了让这两个函数读取到模块名和模块映射的对象,我们将这两个函数都放在index.ts中:
// src/store/index.tsimport { inject, provide } from 'vue'import userModule from './modules/user'const modules = {user: userModule,}// 创建store,在App.vue调用该方法,自动注入所有模块的storeexport const createStore = () => {for (const key in modules) {if (Object.prototype.hasOwnProperty.call(modules, key)) {const store = modules[key as keyof typeof modules]provide(key, store)}}}// 组件内使用store,模块名会依照modules的值提供类型提示export const useStore = <T extends keyof typeof modules>(key: T) => {return inject(key, modules[key])}
得益于Composition API,我们可以直接将provide和inject封装在函数内。这样就能让inject能获取到所有模块的模块名和值,从而给出完整的ts声明。接下来我们来看下如何使用及效果。
4. createStore - 提供 store 对象
先在 App.vue 中注册:
// App.vueimport { defineComponent } from 'vue'import { createStore } from '@/store'export default defineComponent({name: 'App',setup() {createStore()},})
5. useStore - 注入并使用 store对象
在组件中使用:
import { defineComponent, onMounted } from 'vue'import { useStore } from '@/store'export default defineComponent({name: 'HelloWord',setup() {const userStore = useStore('user')onMounted(() => {console.log(userStore.state.userAge) // number 18})},})
6. 查看TypeScript支持效果
现在我们使用起来更加的方便了,而且拥有了完善的类型声明提示。我们来看看效果:
import { defineComponent, onMounted } from 'vue'import { useStore } from '@/store'export default defineComponent({name: 'HelloWord',setup() {const userStore = useStore('user')// Argument of type '"other"' is not assignable to parameter of type '"user"'// 提示不存在 other 这个模块const otherStore = useStore('other')onMounted(() => {console.log(userStore.state.userAge) // number 18// Cannot assign to 'userAge' because it is a read-only property.// 提示不可修改只读属性userStore.state.userAge = 20// Property 'xxx' does not exist on type '{ readonly userName: string; readonly userAge: number; }'.// 提示user模块上的state不存在名为 xxx 的属性userStore.state.xxx})},})
到了这一步我们的状态管理已经完成了。简便易用且拥有完善的TypeScript支持。
当然还可以添加其他功能,如与本地存储sessionStorage、localStorage同步,store方法调用记录等,这些就可以自己去扩展了。
扩展 - 与 浏览器存储 同步
接下来提供一份与本地存储sessionStorage、localStorage同步的完整版示例。
组件内的使用方法以及创建store模块的方法与上面的代码没有区别,因此只提供 index.ts 的示例。
index:
// src/store/index.tsimport { inject, watch, onMounted, provide } from 'vue'import userModule from './modules/user'const modules = {user: userModule,}// store 与 sessionStorage 或 localStorage 同步const usePersist = (state: { [index: string]: unknown },key: string,storage: 'sessionStorage' | 'localStorage') => {onMounted(() => {const userStoreState = window[storage].getItem(key + '_persist_storage')if (userStoreState) {const data = JSON.parse(userStoreState)for (const dataKey in state) {state[dataKey] = data[dataKey]}}})watch(state, val => {window[storage].setItem(key + '_persist_storage', JSON.stringify(val))})}// 注入 storeexport const useStore = <T extends keyof typeof modules>(key: T) => {return inject(key, modules[key])}// 提供 store,传参表示是否需要与浏览器存储同步,默认不同步export const createStore = (persistType: 'none' | 'sessionStorage' | 'localStorage' = 'none') => {for (const key in modules) {if (Object.prototype.hasOwnProperty.call(modules, key)) {const store = modules[key as keyof typeof modules]if (persistType !== 'none') {usePersist(store.state, key, persistType)}provide(key, store)}}}

