前言
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.vue
import { defineComponent, provide } from 'vue'
import store from '@/store'
export default defineComponent({
name: 'App',
setup() {
// readonly用于确保所有属性都是只读的
provide('user', store)
},
})
3. 在组件中 inject(注入)并使用
在vue组件中使用:
// HelloWord.vue
import { 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.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,
})
3. 创建 提供/注入 store 模块的方法
我们现在不直接在组件中使用provide和inject。因为这样就不能满足我们的目标。
我们需要创建两个新的函数:createStore
:用于provide提供store模块useStore
:用于inject注入store模块,在组件内访问store。
为了让这两个函数读取到模块名和模块映射的对象,我们将这两个函数都放在index.ts中:
// src/store/index.ts
import { inject, provide } from 'vue'
import userModule from './modules/user'
const modules = {
user: userModule,
}
// 创建store,在App.vue调用该方法,自动注入所有模块的store
export 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.vue
import { 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.ts
import { 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))
})
}
// 注入 store
export 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)
}
}
}