【Vue】vuex-module-decorators详细使用手册

戴浩初
2023-12-01

英文使用说明地址https://championswimmer.in/vuex-module-decorators/

1、安装

npm install vuex-module-decorators
# or
yarn add vuex-module-decorators

2、概述

2.1 功能

这个库可以使用下面方式编写 vuex 模块

// eg. /app/store/posts.ts
import { VuexModule, Module, Mutation, Action } from 'vuex-module-decorators'
import { get } from 'axios'

interface PostEntity {
  comments: string[]
}

@Module
export default class Posts extends VuexModule {
  posts: PostEntity[] = [] // initialize empty for now

  get totalComments(): number {
    return this.posts
      .filter(post => {
        // Take those posts that have comments
        return post.comments && post.comments.length
      })
      .reduce((sum, post) => {
        // Sum all the lengths of comments arrays
        return sum + post.comments.length
      }, 0)
  }

  @Mutation
  updatePosts(posts: PostEntity[]) {
    this.posts = posts
  }

  @Action({ commit: 'updatePosts' })
  async fetchPosts() {
    return get('https://jsonplaceholder.typicode.com/posts')
  }
}

对应输出结果为

// equivalent eg. /app/store/posts.js
module.exports = {
  state: {
    posts: []
  },
  getters: {
    totalComments: (state) => {
      return state.posts
        .filter((post) => {
          return post.comments && post.comments.length
        })
        .reduce((sum, post) => {
          return sum + post.comments.length
        }, 0)
    }
  },
  mutations: {
    updatePosts: (state, posts) => {
      // 'posts' is payload
      state.posts = posts
    }
  },
  actions: {
    fetchPosts: async (context) => {
      // the return of the function is passed as payload
      const payload = await get('https://jsonplaceholder.typicode.com/posts')
      // the value of 'commit' in decorator is the mutation used
      context.commit('updatePosts', payload)
    }
  }
}

2.2 类型安全的好处

而不是使用通常的方式来dispatchcommit

store.commit('updatePosts', posts)
await store.dispatch('fetchPosts')

它没有为有效负载提供类型安全,也没有在 IDE 中提供自动完成帮助, 您现在可以使用 getModule 访问器使用更多类型安全机制

import { getModule } from 'vuex-module-decorators'
import Posts from `~/store/posts.js`

const postsModule = getModule(Posts)

// access posts
const posts = postsModule.posts

// use getters
const commentCount = postsModule.totalComments

// commit mutation
postsModule.updatePosts(newPostsArray)

// dispatch action
await postsModule.fetchPosts()

3、开始

3.1 定义module

To define a module, create a class that extends from VuexModule
and must be decorated with Module decorator

// eg. /app/store/mymodule.ts
import { Module, VuexModule } from 'vuex-module-decorators'

@Module
export default class MyModule extends VuexModule {
  someField: string = 'somedata'
}

CAREFUL
There is a Module class in the vuex package too, which is not a
decorator. Make sure you import correct Module decorator from from
vuex-module-decorators
import {Module} from 'vuex'
✔️ import {Module} from 'vuex-module-decorators'

3.2 store内使用

In your store, you use the MyModule class itself as a module.

import Vuex from 'vuex'
import MyModule from '~/store/mymodule'

const store = new Vuex.Store({
  modules: {
    myMod: MyModule
  }
})

NOTE
The way we use the MyModule class is different from classical object-oriented programming
and similar to how vue-class-component works.
We use the class itself as module, not an object constructed by the class
new MyModule()

3.3 访问状态

All the usual ways of accessing the module works -

  1. Import The store
   import store from '~/store'
   store.state.myMod.someField
  1. Use this.$store if in component
   this.$store.state.myMod.someField

In addition to that, for a much more typesafe access, we can use getModule()

  1. Use getModule() to create type-safe accessor
   import { Module, VuexModule, getModule } from 'vuex-module-decorators'
   import store from '@/store'

   @Module({ dynamic: true, store, name: 'mymod' })
   class MyModule extends VuexModule {
     someField: number = 10
   }
   const myMod = getModule(MyModule)
   myMod.someField //works
   myMod.someOtherField //Typescript will error, as field doesn't exist

4、核心内容

4.1 State

All properties of the class are converted into state props.
For example, the following code

import { Module, VuexModule } from 'vuex-module-decorators'

@Module
export default class Vehicle extends VuexModule {
  wheels = 2
}

is equivalent of this -

export default {
  state: {
    wheels: 2
  }
}

 WARNING
If state value cannot be determined, it MUST be initialized with null. Just like wheels: number | null = null.

4.2 Getters

All ES6 getter functions of the class are converted into vuex getters

For example, the following code -

import { Module, VuexModule } from 'vuex-module-decorators'

@Module
export default class Vehicle extends VuexModule {
  wheels = 2
  get axles() {
    return this.wheels / 2
  }
}

is equivalent of this -

export default {
  state: {
    wheels: 2
  },
  getters: {
    axles: (state) => state.wheels / 2
  }
}

For Method-Style Access use vanilla vuex and return a function:

@Module
export default class Vehicle extends VuexModule {
  companies = []
  get company() {
    return (companyName: string) => { this.companies.find(company => company.name === companyName) };
  }
}

4.3 Mutations

All functions decorated with @Mutation are converted into Vuex mutations
For example, the following code -

import { Module, VuexModule, Mutation } from 'vuex-module-decorators'

@Module
export default class Vehicle extends VuexModule {
  wheels = 2

  @Mutation
  puncture(n: number) {
    this.wheels = this.wheels - n
  }
}

is equivalent of this -

export default {
  state: {
    wheels: 2
  },
  mutations: {
    puncture: (state, payload) => {
      state.wheels = state.wheels - payload
    }
  }
}

NOTE
Once decorated with the @Mutation decorator Mutations are run with this (context) set to the state
So when you want to change things in the state,
state.item++ is simply this.item++

 WARNING
Mutation functions MUST NOT be async functions.
Also do not define them as arrow ➡️ functions, since we need to rebind them at runtime.

4.4 Actions

All functions that are decorated with @Action are converted into
vuex actions.

For example this code -

import { Module, VuexModule, Mutation, Action } from 'vuex-module-decorators'
import { get } from 'request'

@Module
export default class Vehicle extends VuexModule {
  wheels = 2

  @Mutation
  addWheel(n: number) {
    this.wheels = this.wheels + n
  }

  @Action
  async fetchNewWheels(wheelStore: string) {
    const wheels = await get(wheelStore)
    this.context.commit('addWheel', wheels)
  }
}

is equivalent of this -

const request = require('request')
export default {
  state: {
    wheels: 2
  },
  mutations: {
    addWheel: (state, payload) => {
      state.wheels = state.wheels + payload
    }
  },
  actions: {
    fetchNewWheels: async (context, payload) => {
      const wheels = await request.get(payload)
      context.commit('addWheel', wheels)
    }
  }
}

NOTE
Once decorated with @Action the function will be called with this
having the following shape - {...[all fields of state], context}
The action payload comes as an argument.
So to commit a mutation manually from within action’s body
simply call this.context.commit('mutationName', mutPayload)

️️ WARNING
If you are doing a long running task inside your action, it is recommended
to define it as an async function. But even if you do not, this library
will wrap your function into a Promise and await it.
If you want something to actually happen synchronously, make it a Mutation instead
Also do not define them as arrow ➡️ functions, since we need to rebind them at runtime.

4.5 MutationActions

If you have understood how Actions and Mutations work
you might have requirements for some functions that -

  1. first do an asynchronous action
  2. and then commit the resultant value to the store via a mutation

This is where a @MutationAction comes to picture.

Here is a basic example

import {VuexModule, Module, MutationAction} from 'vuex-module-decorators' 

@Module
class TypicodeModule extends VuexModule {
  posts: Post[] = [] 
  users: User[] = [] 

  @MutationAction 
  async function updatePosts() {
    const posts = await axios.get('https://jsonplaceholder.typicode.com/posts')

    return { posts }
  }
}

That gets converted to something like this


const typicodeModule = {
  state: {
    posts: [],
    users: []
  },
  mutations: {
    updatePosts: function (state, posts) {
      state.posts = posts
    }
  },
  actions: {
    updatePosts: async function (context) {
      const posts = await axios.get('https://jsonplaceholder.typicode.com/posts')
      context.commit('updatePosts', posts)
    }
  }
}

NOTE
Note that if S denotes the type of state, then the object returned from a
MutationAction function must of type Partial<S>
The keys present inside the return value (for eg, here posts) are replaced into
the store.

NOTE
When a MutationAction function returns undefined, the mutation part of the
MutationAction will not be called, and the state will remain the same.

5、进阶教程

5.1 Namespaced Modules

TIP
Before reading this, it is imperative you understand what are
namespaced modules

If you intend to use your module in a namespaced way, then
you need to specify so in the @Module decorator.

@Module({ namespaced: true, name: 'mm' })
class MyModule extends VuexModule {
  wheels = 2

  @Mutation
  incrWheels(extra: number) {
    this.wheels += extra
  }

  get axles() {
    return this.wheels / 2
  }
}

const store = new Vuex.Store({
  modules: {
    mm: MyModule
  }
})

NOTE
The name field in the decorator should match the actual name
that you will assign the module to, when you create the store.

It isn’t exactly elegant to manually keep these two same, but it
is important. We have to convert this.store.dispatch('action')
calls into this.store.dispatch('name/action'), and we need the
name to be correct in the decorator to make it work

5.1.1 Registering global actions inside namespaced modules

In order to register actions of namespaced modules globally you can add a parameter root: true to @Action and @MutationAction decorated methods.

@Module({ namespaced: true, name: 'mm' })
class MyModule extends VuexModule {
  wheels = 2

  @Mutation
  setWheels(wheels: number) {
    this.wheels = wheels
  }
  
  @Action({ root: true, commit: 'setWheels' })
  clear() {
    return 0
  }

  get axles() {
    return this.wheels / 2
  }
}

const store = new Vuex.Store({
  modules: {
    mm: MyModule
  }
})

This way the @Action clear of MyModule will be called by dispatching clear although being in the namespaced module mm.
The same thing works for @MutationAction by just passing { root: true } to the decorator-options.

NOTE
When registering an action globally it can not be called by the namespace’s name.
For the example that means, that the action can not be called by dispatching mm/clear!

5.2 Dynamic Modules

tip
Before you read this secion, it is advised that you understand how
dynamic module registration works

Modules can be registered dynamically simply by passing a few properties into
the @Module decorator, but an important part of the process is, we first
create the store, and then pass the store to the module.

Step 1: Create the Store

// @/store/index.ts
import Vuex from 'vuex'

const store = new Vuex.Store({
  /*
  Ideally if all your modules are dynamic
  then your store is registered initially
  as a completely empty object
  */
})

Step 2: Create the Dynamic Module

// @/store/modules/MyModule.ts
import store from '@/store'
import {Module, VuexModule} from 'vuex-module-decorators'

@Module({dynamic: true, store, name: 'mm'})
export default class MyModule extends VuexModule {
  /*
  Your module definition as usual
  */
}

NOTE
As of now, we do not support dynamic + nested modules.

IMPORTANT ⛔️
Make sure your imports/requires are ordered in such a way that
the store definition is executed before the module class is created.

It is important for the store to exist, and be passed into the
@Module decorator for the module to get registered dynamically

 类似资料: