简介
全面而强大的 vuex
在使用 typescript
开发的工程中也暴露出一些短板。正如 Vue3+TS 优雅地使用状态管理 中提到的一样。vuex只能在使用 state
时,能够享受到TS的完全加持,但是却不能很好地对 mutation
, action
以及 getter
进行很友好的类型检查。后来我又尝试了 vuex-module-decorators
,在 typescript
开发环境下表现十分优秀,但需要手动解决动态模块重复注册的问题,而且需要同时引入 vuex
与 vuex-module-decorators
两个库,在中小型项目中显得过于笨重。
新出炉的Pinia
是个好东西,但是为了考虑vuex
的迁移问题,API风格尽量与vuex
保持一致,而我个人更喜欢 vuex-module-decorators
那种通过 class
与装饰器结合起来的风格。
于是自己动手撸了一个基于 Vue3 + TS 的轻量级状态管理库,参考 vuex-module-decorators
中的思想,利用了ES5的 Object.defineProperty
,ES6中的 class
和 Reflect
以及 ES7中的装饰器语法来实现。打包后仅1.2KB,用起来十分轻便。
仓库
npm 仓库:sps-vue-store - npm (npmjs.com)
gitee 仓库:sps-vue-store: 基于 vue3 + ts 的轻量级状态管理 (gitee.com))
使用示例
安装依赖
npm install sps-vue-store -s
or
yarn add sps-vue-store -s
创建仓库
// src/store/index.ts
import { createStore } from 'sps-vue-store'
const store = createStore()
export default store
注册模块
类似vuex
,使用getter
,mutation
,action
装饰器来修饰类方法,而类属性充当vuex
中state
的角色。
// src/store/app.ts
import { createStoreModule, Getter, Mutation, Action, Module, StoreModule } from 'sps-vue-store'
import store from '.'
@Module({ name: 'app', store })
class AppStore extends StoreModule {
public token: string = '123'
@Getter
get tokenWithPrefix() {
return `token is ${this.token}`
}
@Mutation
setToken(token: string) {
this.token = token
}
@Action
async asyncSetToken(token: string) {
this.token = await new Promise((resolve) => {
setTimeout(() => {
resolve(token)
}, 1000)
})
}
}
const useAppStore = createStoreModule(AppStore)
export default useAppStore
tsx语法
// src/App.tsx
import { defineComponent } from 'vue'
import useAppStore from '@/store/app'
export default defineComponent({
name: 'App',
setup() {
// reactive对象解构后会失去动态响应特性
// 方法在setup函数中解构以提高性能
const { setToken, asyncSetToken } = useAppStore
/* render 函数 */
return () => {
// 属性与getter在render函数中解构,保持其动态特性
const { token, tokenWithPrefix } = useAppStore
return (
<div>
<div>{token}</div>
<div>{tokenWithPrefix}</div>
<div>
<button onClick={() => setToken('456')}>同步</button>
</div>
<div>
<button onClick={() => asyncSetToken('789')}>异步</button>
</div>
</div>
)
}
}
})
sfc语法
// src/App.vue
<template>
<div>
<div>{{ appStore.token }}</div>
<div>{{ appStore.tokenWithPrefix }}</div>
<div>
<button @click="setToken('456')">同步</button>
</div>
<div>
<button @click="asyncSetToken('789')">异步</button>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import useAppStore from '@/store/app'
export default defineComponent({
name: 'App',
setup() {
const { setToken, asyncSetToken } = useAppStore
// 属性与getter在computed中解构,保持其动态特性
const appStore = computed(() => {
const { token, tokenWithPrefix } = useAppStore
return {
token,
tokenWithPrefix
}
})
return {
appStore,
setToken,
asyncSetToken
}
}
})
</script>
实现原理
创建一个全局的 reactive
对象:
let store: any
export function createStore() {
store = reactive({})
return store
}
创建装饰器函数,供用户在创建类时对其拓展:
// 类装饰器,将用户为模块的命名保存在类的元数据上
export function Module(params: ModuleDecoratorParams): ClassDecorator {
return (target) => {
const { name, store } = params
if (!store) {
throw new Error('Can not use module before create store.')
}
Reflect.defineMetadata(MODULE_NAME, name, target)
}
}
// getter方法装饰器,在方法元数据上记录该方法为 getter 方法
export const Getter: MethodDecorator = (target, propertyKey, descriptor) => {
Reflect.defineMetadata(METHOD_TYPE, METHOD_TYPE_GETTER, target, propertyKey)
}
在全局store上创建模块对象,例如模块名为app,store.app就是模块对象,创建并返回一个代理对象,用于操作模块对象上的属性值:
export function createStoreModule<T extends StoreModule>(
target: TFunction<T>
): T {
// 获取用户通过装饰器定义的模块名,根据模块名创建全局store上的模块对象
const moduleName: string = Reflect.getMetadata(MODULE_NAME, target)
const obj: any = new target()
store[moduleName] = obj
// 创建代理对象
const result: any = {}
// 遍历类实例上的属性
const properties = Object.keys(obj)
for (const property of properties) {
// 在代理对象上定义同名属性,实际返回模块对象对应属性的值
// 没有定义set描述符,可避免用户直接操作模块对象
Object.defineProperty(result, property, {
get() {
return store[moduleName][property]
}
})
}
// 遍历类实例原型上的属性(getter与方法)
const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(obj))
for (const method of methods) {
// 构造函数不需要代理,直接跳过
if (method === 'constructor') continue
// 从方法的元数据中获取方法类型
const type = Reflect.getMetadata(METHOD_TYPE, target.prototype, method)
// 代理getter方法
if (type === METHOD_TYPE_GETTER) {
// 在代理对象上将getter方法定义为同名属性,实际返回模块对象对应的getter值
Object.defineProperty(result, method, {
get() {
return obj[method]
}
})
}
// 在代理对象上定义同名同步方法,实际执行模块对象上对应的方法
else if (type === METHOD_TYPE_MUTATION) {
Object.defineProperty(result, method, {
value(...args: any) {
obj[method].call(store[moduleName], ...args)
}
})
}
// 在代理对象上定义同名异步方法,实际执行模块对象上对应的方法(需返回Promise)
else if (type === METHOD_TYPE_ACTION) {
Object.defineProperty(result, method, {
value(...args: any) {
return new Promise((resolve) => {
resolve(obj[method].call(store[moduleName], ...args))
})
}
})
}
}
return result
}
小结
vue3
提供的 reactive
函数十分强大,再加上ES5 - ES7中的几个新特性,比较容易就实现了一个对 typescript
支持更好的轻量级状态管理库。不过 vuex
作为官方仓库,功能更加丰富和强大,后续还要继续以 vuex
为目标,继续完善与拓展功能。