sps-vue-store: 基于 Vue3 + TS 的轻量级状态管理

1,160 阅读4分钟

简介

全面而强大的 vuex 在使用 typescript 开发的工程中也暴露出一些短板。正如 Vue3+TS 优雅地使用状态管理 中提到的一样。vuex只能在使用 state 时,能够享受到TS的完全加持,但是却不能很好地对 mutation , action 以及 getter 进行很友好的类型检查。后来我又尝试了 vuex-module-decorators,在 typescript 开发环境下表现十分优秀,但需要手动解决动态模块重复注册的问题,而且需要同时引入 vuexvuex-module-decorators 两个库,在中小型项目中显得过于笨重。

新出炉的Pinia是个好东西,但是为了考虑vuex的迁移问题,API风格尽量与vuex保持一致,而我个人更喜欢 vuex-module-decorators 那种通过 class 与装饰器结合起来的风格。

于是自己动手撸了一个基于 Vue3 + TS 的轻量级状态管理库,参考 vuex-module-decorators 中的思想,利用了ES5的 Object.defineProperty ,ES6中的 classReflect 以及 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,使用gettermutationaction装饰器来修饰类方法,而类属性充当vuexstate的角色。

// 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 为目标,继续完善与拓展功能。