英文使用说明地址https://championswimmer.in/vuex-module-decorators/
npm install vuex-module-decorators
# or
yarn add vuex-module-decorators
这个库可以使用下面方式编写 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)
}
}
}
而不是使用通常的方式来dispatch
和commit
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()
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 aModule
class in thevuex
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'
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()
❌
All the usual ways of accessing the module works -
import store from '~/store'
store.state.myMod.someField
this.$store
if in component this.$store.state.myMod.someField
In addition to that, for a much more typesafe access, we can use getModule()
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
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 withnull
. Just likewheels: number | null = null
.
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) };
}
}
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 simplythis.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.
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 withthis
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 callthis.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 aMutation
instead
Also do not define them as arrow ➡️ functions, since we need to rebind them at runtime.
If you have understood how Actions and Mutations work
you might have requirements for some functions that -
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, hereposts
) are replaced into
the store.
NOTE
When aMutationAction
function returnsundefined
, the mutation part of the
MutationAction
will not be called, and the state will remain the same.
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
Thename
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
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 dispatchingmm/clear
!
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.
// @/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
*/
})
// @/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