Introduction to the

The comprehensive and powerful Vuex also shows some shortcomings in projects developed using typescript. As mentioned in Vue3+TS’s elegant use of state management. Vuex can only enjoy the full benefit of TS when using State, but does not do a good job of friendly type checking for mutation, actions, and getters. I tried vuex-module-decorators, which worked well in typescript development, but required manual re-registration of dynamic modules and the integration of both vuex and vuex-module-decorators libraries. Too bulky for small to medium sized projects.

The new Pinia is a good thing, but for the sake of vuex migration, the API style is as consistent as possible, and I personally prefer the Vuex-module-decorators style that combines classes with decorators.

So I built my own lightweight state management library based on Vue3 + TS. Taking a page from vuex-module-decorators and using ES5’s Object.defineProperty, Class and Reflect in ES6 and decorator syntax in ES7. When packed, it’s only 1.2KB, making it light to use.

warehouse

NPM Warehouse: SPS-VUE-store-NPM (nPMjs.com)

Gitee Repository: SPS-VUE-Store: Lightweight State Management based on VUE3 + TS (gitee.com)

Use the sample

Install dependencies

npm install sps-vue-store -s
Copy the code

or

yarn add sps-vue-store -s
Copy the code

Create a warehouse

// src/store/index.ts
import { createStore } from 'sps-vue-store'

const store = createStore()

export default store
Copy the code

Registration module

Similar to VUex, getter, mutation, and Action decorators are used to decorate class methods, while class attributes act as states in VUex.

// 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 The ${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
Copy the code

TSX grammar

// src/App.tsx
import { defineComponent } from 'vue'
import useAppStore from '@/store/app'

export default defineComponent({
  name: 'App'.setup() {
    // Reactive objects lose dynamic responsiveness when they are deconstructed
    // Methods are deconstructed in setup functions to improve performance
    const { setToken, asyncSetToken } = useAppStore
    /* render function */
    return () = > {
      // Properties and getters are deconstructed in the render function, keeping their dynamic properties
      const { token, tokenWithPrefix } = useAppStore
      return (
        <div>
          <div>{token}</div>
          <div>{tokenWithPrefix}</div>
          <div>
            <button onClick={()= >SetToken (' 456 ')} > synchronization</button>
          </div>
          <div>
            <button onClick={()= >> asynchronous asyncSetToken (' 789 ')}</button>
          </div>
        </div>)}}})Copy the code

SFC grammar

// src/App.vue
<template>
  <div>
    <div>{{ appStore.token }}</div>
    <div>{{ appStore.tokenWithPrefix }}</div>
    <div>
      <button @click="setToken('456')">synchronous</button>
    </div>
    <div>
      <button @click="asyncSetToken('789')">asynchronous</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
    // Attributes and getters are deconstructed in computed to maintain their dynamic properties
    const appStore = computed(() = > {
      const { token, tokenWithPrefix } = useAppStore
      return {
        token,
        tokenWithPrefix
      }
    })
    return {
      appStore,
      setToken,
      asyncSetToken
    }
  }
})
</script>
Copy the code

Realize the principle of

Create a global Reactive object:

let store: any

export function createStore() {
  store = reactive({})
  return store
}
Copy the code

Create a decorator function that the user can extend when creating a class:

// Class decorator, which stores user-assigned module names on the class metadata
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 method decorator that records the method as a getter method on the method metadata
export const Getter: MethodDecorator = (target, propertyKey, descriptor) = > {
  Reflect.defineMetadata(METHOD_TYPE, METHOD_TYPE_GETTER, target, propertyKey)
}
Copy the code

Create a module object on the global store. For example, the module name is app, and store. App is the module object.

export function createStoreModule<T extends StoreModule> (
  target: TFunction<T>
) :T {
  // Get the module name defined by the user through the decorator, and create a module object on the global store based on the module name
  const moduleName: string = Reflect.getMetadata(MODULE_NAME, target)
  const obj: any = new target()
  store[moduleName] = obj
  
  // Create a proxy object
  const result: any = {}
  // Iterate over attributes on class instances
  const properties = Object.keys(obj)
  for (const property of properties) {
    // Define a property of the same name on the proxy object, which actually returns the value of the corresponding property of the module object
    // No set descriptor is defined to avoid direct manipulation of module objects by the user
    Object.defineProperty(result, property, {
      get() {
        return store[moduleName][property]
      }
    })
  }
  // Iterate over properties (getters and methods) on class instance prototypes
  const methods = Object.getOwnPropertyNames(Object.getPrototypeOf(obj))
  for (const method of methods) {
    // The constructor doesn't need a proxy, just skip it
    if (method === 'constructor') continue
    // Get the method type from the method metadata
    const type = Reflect.getMetadata(METHOD_TYPE, target.prototype, method)
    // Proxy getter method
    if (type === METHOD_TYPE_GETTER) {
       // Define the getter method on the proxy object as a property of the same name, which actually returns the corresponding getter value of the module object
      Object.defineProperty(result, method, {
        get() {
          return obj[method]
        }
      })
    }
    // Define the synchronization method of the same name on the proxy object, and actually execute the corresponding method on the module object
    else if (type === METHOD_TYPE_MUTATION) {
      Object.defineProperty(result, method, {
        value(. args:any){ obj[method].call(store[moduleName], ... args) } }) }// Define the asynchronous method of the same name on the proxy object, and actually execute the corresponding method on the module object (return 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
}
Copy the code

summary

Vue3’s powerful reactive functions, combined with several new features in ES5-ES7, make it relatively easy to implement a lightweight state management library with better typescript support. However, vuex, as the official warehouse, has more rich and powerful functions. In the future, we will continue to improve and expand functions with vuex as the target.