Vue3的引入了新特性Composition Api,相比于Vue2的代码出现了重大变化,也为状态管理方式提供了新的途径。而Vue3中利用typescript的全面加持则可以让我们更优雅地使用状态管理。
传统状态管理 - Vuex
示例
Vuex作为Vue官方出品的经典,支持vue-devtools调试工具(目前6.0.0 beta7版本还不支持,但未来肯定会支持),依旧是全局状态管理的首选项。尤其是有了TS的加持,state用起来更加丝滑。下面是一个简单的实例:
// store/modules/test.ts
import { Module } from 'vuex'
export interface User {
name: string
}
export interface TestState {
users: User[]
}
export default {
state: {
users: [{ name: 'sps' }]
},
getters: {
userCount (state) {
return state.users.length
}
},
mutations: {
ADD_USER (state, user: User) {
state.users.push(user)
}
},
namespaced: true
} as Module<TestState, Object>
// store/index.ts
import { createStore } from 'vuex'
import test, { TestState } from './modules/test'
export interface State {
test: TestState
}
const store = createStore({
modules: {
test
}
})
export default store
// App.tsx
import { defineComponent } from 'vue'
import { useStore } from 'vuex'
import { State } from './store'
export default defineComponent({
name: 'App',
setup () {
const { state: { test }, getters, commit } = useStore<State>()
const addUser = () => {
commit('test/ADD_USER', { name: 'haha' })
}
return () => {
const items = test.users.map(user => (
<li>{ user.name }</li>
))
return (
<div>
<div>用户总数: { getters['test/userCount'] }</div>
<ul>{ items }</ul>
<button onClick={ addUser }>添加</button>
</div>
)
}
}
})
使用体验
从上述代码可以看到,对于Vuex模块中的state,TS能够进行完全加持,但是却不能很好地对mutation, action以及getter进行很友好的类型检查,可以看看Vuex源码中的类型定义:
// Vuex源码 type/index.d.ts
// 泛型中S为模块state的类型,R为根state的类型
export interface Module<S, R> {
namespaced?: boolean;
state?: S | (() => S);
getters?: GetterTree<S, R>;
actions?: ActionTree<S, R>;
mutations?: MutationTree<S>;
modules?: ModuleTree<R>;
}
export interface GetterTree<S, R> {
[key: string]: Getter<S, R>;
}
export interface ActionTree<S, R> {
[key: string]: Action<S, R>;
}
export interface MutationTree<S> {
[key: string]: Mutation<S>;
}
key值是通过[key: string]
的方式来定义的,所以我们并不能知道 commit 或 dispatch 具体可以执行哪些方法,只能通过commit('test/ADD_USER', { name: 'haha' })
这样的方式来使用。而且 mutation,action,getter 中的每一个函数都是统一定义的,没法对单独的每一个函数进行类型定义,所以在执行 commit 或 dispatch 时,并不能根据执行的函数名对其参数进行相应的类型检查。
为了更充分利用ts的类型检查,可以使用vuex-module-decorators
。
Vuex 装饰器 vuex-module-decorators
笔者在看 vue-vben-admin 源码时发现其用了这个神器后就一直在用。这个神器在Vue2的时代就已经存在了(可惜当时Vue和TS还没有合体),其优势是通过class的方式来定义Vuex模块,可以充分利用TS的类型检查。
用前说明
在使用 vuex-module-decorators 之前有两点需要注意:
- 在tsconfig.json中配置,启用装饰器语法:
{
"compilerOptions": {
"experimentalDecorators": true
// ...
}
}
- 创建一个工具函数,用以解决在调试热更新时,动态模块重复注册的问题:
// store/index.ts
export function hotModuleUnregisterModule (name: string) {
if (!name) return
if ((store.state as any)[name]) {
store.unregisterModule(name)
}
}
示例
使用 vuex-module-decorators 的动态模块功能可以不用在 store 中对模块手动注册,下面是一个简单是使用示例:
// store/modules/test.ts
import { getModule, Module, Mutation, VuexModule } from 'vuex-module-decorators'
import store, { hotModuleUnregisterModule } from '@/store'
const MODULE_NAME = 'test'
hotModuleUnregisterModule(MODULE_NAME)
export interface User {
name: string
}
@Module({ name: MODULE_NAME, dynamic: true, namespaced: true, store })
export default class UserModule extends VuexModule {
users: User[] = [{ name: 'sps' }]
get userCount () {
return this.users.length
}
@Mutation
ADD_USER (user: User) {
this.users.push(user)
}
}
export const testStore = getModule(UserModule)
// App.tsx
import { defineComponent } from 'vue'
import { testStore } from './store/modules/test'
export default defineComponent({
name: 'App',
setup () {
const { users, ADD_USER } = testStore
return () => {
// getter解构后不是响应式的,需要在render函数中解构
const { userCount } = testStore
const items = users.map(user => (
<li>{ user.name }</li>
))
return (
<div>
<div>用户总数: { userCount }</div>
<ul>{ items }</ul>
<button onClick={() => { ADD_USER({ name: 'haha' }) }}>添加</button>
</div>
)
}
}
})
使用体验
使用 vuex-module-decorators 可以充分利用TS的特性,但是有一个小缺陷。原版的 Vuex 的 getters 是可以接收参数的,但在 class 中的 get 属性是无法传入参数。不过在 getters 中传参数的应用场景不算多,而且也可以通过写工具函数来解决,总之体验还是相当不错的。
Vue3 新特性实现状态管理
VueConf 2021大会上,Vue.js核心团队成员Anthony Fu讲到了利用 Vue3 的 composition Api 中的 reactive 对象可以天然实现状态管理,不用引入任何其他库。在其基础上,将相关操作函数封装到一起就能以 hook 函数的方式来使用。
示例
// store/modules/test.ts
import { computed, reactive } from 'vue'
export interface User {
name: string
}
export interface TestState {
users: User[]
}
// reactive对象放到use函数外就可以作为全局状态
const state: TestState = reactive({
users: [{ name: 'sps' }]
})
export default function useTestStore () {
const userCount = computed(() => {
return state.users.length
})
const ADD_USER = (user: User) => {
state.users.push(user)
}
return {
state,
userCount,
ADD_USER
}
}
// App.tsx
import { defineComponent } from 'vue'
import useTestStore from './store/modules/test'
export default defineComponent({
name: 'App',
setup () {
const { state, userCount, ADD_USER } = useTestStore()
return () => {
const items = state.users.map(user => (
<li>{ user.name }</li>
))
return (
<div>
<div>用户总数: { userCount.value }</div>
<ul>{ items }</ul>
<button onClick={() => { ADD_USER({ name: 'haha' }) }}>添加</button>
</div>
)
}
}
})
使用体验
用 composition Api 的方式来实现全局状态管理充分利用了 hook 函数带来的便利性,能在 hook 函数中使用 watch, onMounted, onUnmounted 等 Api 来实现更丰富的功能,还能享受TS的完美加持,且不用引入其他库,减少打包后的文件大小。
不过还是有一些缺点:
- 无法使用 Vuex 的插件(Vuex有成熟的社区,其中有很多好用的插件)
- 丢失了 Vuex 的封装性,可在外部对 reactive 对象进行直接操作
局部状态管理
上述的方法可以实现全局的状态管理,在一些场景下,我们需要的是在某个组件X及其所有子孙组件中维护一个局部状态,假如程序中用到了多个组件X,则有多个相对独立的局部状态,这种时候就需要用到 provide/inject。
示例
Anthony Fu在VueConf 2021上还提到了类型安全的 provide/inject 的使用方式:即通过给 provide/inject 使用的 key 进行类型定义(看到这里的时候笔者突然想到可以同理实现类型安全的localStorage,Cookie存取)。同样地这基础上,再将其相关操作函数一起进行一点简单的封装,就能像 hook 函数一样来使用了。
// components/useTestState.ts
import { computed, inject, InjectionKey, reactive } from 'vue'
export interface User {
name: string
}
export interface TestState {
users: User[]
}
// 创建局部状态
export const createTestState = () => {
const state: TestState = reactive({
users: [{ name: 'sps' }]
})
return state
}
// 定义 InjectionKey
export const injectTestKey: InjectionKey<TestState> = Symbol('Test')
export const useTestState = () => {
// 这里一定要确保使用hook函数的组件其祖先组件进行了provide操作
const state = inject(injectTestKey)!
const userCount = computed(() => {
return state.users.length
})
const ADD_USER = (user: User) => {
state.users.push(user)
}
return {
state,
userCount,
ADD_USER
}
}
// components/Test.tsx
import { defineComponent } from 'vue'
import { useTestState } from './useTestState'
export default defineComponent({
name: 'Test',
setup () {
const { state, userCount, ADD_USER } = useTestState()
return () => {
const items = state.users.map(user => (
<li>{ user.name }</li>
))
return (
<div>
<div>用户总数: { userCount.value }</div>
<ul>{ items }</ul>
<button onClick={() => {ADD_USER({ name: 'haha' })}}>添加</button>
</div>
)
}
}
})
// App.tsx
import { defineComponent, provide } from 'vue'
import Test from '@/components/Test'
import { createTestState, injectTestKey } from '@/components/useTestState'
export default defineComponent({
name: 'App',
setup () {
const state = createTestState()
provide(injectTestKey, state)
return () => {
return (
<Test />
)
}
}
})
使用体验
provide/inject 实现的局部状态管理使用体验也符合 Vue3 composition Api 的开发风格。最大的缺点仍然在于封装性的问题,即 reactive 对象可被子组件进行直接操作。在需要确保 reactive 对象不被子组件直接操作的场景下,可以不将 reactive 对象直接提供给子组件,而是提供给子组件一个只有 get 的ComputedRef,其内容为 reactive 对象的值,或者提供一个函数,其返回 reactive 对象克隆后的值。
总结
本文讲述的几种状态管理方式各有特点,需要根据实际场景来选择具体使用哪一个。由于 Vue3 composition Api 天然的共享特性让 Vuex 不再成为了状态管理的几乎唯一选择。在 Vue2 中很多需要用 Bus 和 mixin 来解决的问题都能依靠 composition Api 的共享性来解决,只能说 composition Api 真是太强大了!