Vue & TypeScript 初体验 - 使用Vuex (vuex-module-decorators)

12,202 阅读7分钟

上一篇: Vue & TypeScript 初体验

在上一篇文章中简单介绍了Vue项目中使用TypeScript, 以及相比原来Vue组件写法的一些改变.
本文主要针对在TypeScript下Vuex写法如何更贴近TS的面向对象编程.

选项

简单查了下, 目前有2个库可用于Vue typescript

vuex-class 最近一次更新在8个月前, 且更新频率不高, 故此选择了vuex-module-decorators用以学习&验证

安装 vuex-module-decorators

官方文档: championswimmer.in/vuex-module…

npm install -D vuex-module-decorators

or

yarn install vuex-module-decorators

Babel 6/7

在@vue/cli 3.0创建的项目中, @vue/babel-preset-app已经包含了此项Babel项插件.

npm install babel-plugin-transform-decorators --dev

or

yarn install babel-plugin-transform-decorators --dev

TypeScript(3.x)配置(tsconfig.json)

在tsconfig.json中需要设置:

experimentalDecorators: true,
importHelpers: true

在TypeScript 2中需要设置emitHelpers: true

配置

在vuex-module-decorators@0.9.3版本开始, 代码最终发布为ES5格式, 因此下面的部分主要针对v0.9.2及以前的版本

该包最终生成的代码是ES2015(ES6)格式的, 因此, 如果你的Vue项目如果最终生成的是ES6, 那不需要做什么. 但如果是ES5(为兼容旧浏览器, 如IE9, IE10, IE11),那么在通过@vue/cli创建的工程中, 需要添加以下配置进行转换:

// in your vue.config.js
module.exports = {
  /* ... other settings */
  transpileDependencies: ['vuex-module-decorators']
}

效果展示

使用该库之后, 写vuex modules可以像下面这样:

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

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

    get totalComments (): number {
        return 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 function fetchPosts() {
        return await 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',帖子)
await store.dispatch('fetchPosts'

这样写, 不能利用类型安全的特性, 且IDE也不会提供相关的帮助提示等. 最好的方式是使用用getModule访问器, 来利用更多类型安全机制
新的写法:

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

const postsModule = getModule(Posts)

// 访问posts模块
const posts = postsModule.posts

// 使用getters
const commentCount = postsModule.totalComments

// commit mutation
postsModule.updatePosts(newPostsArray)

// dispatch action
await postsModule.fetchPosts()

开始使用

1. 定义一个module

定义一个modules, 需要创建一个class且extends至VuexModule, 并且Module装饰它.

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

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

vuex中同样有一个名为Module的类, 但它不是一个装饰器, 千万不要使用错了.
import {Module} from 'vuex'
️ import {Module} from 'vuex-module-decorators'

2. 在store中使用

在你的Store, 可以将类MyModule作为一个module

// src/store/index.ts

import Vue from 'vue'
import Vuex from 'vuex'

import MyModule from './MyModule'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
    myMod: MyModule
  }
})

注意: 我们使用MyModule类的方式与经典的面向对象编程不同,类似于vue-class-component的工作方式。将类本身用作模块,而不是由类构造的对象. 因此下面的写法是错误的.
new MyModule()

3. 访问state

直接通过Store访问

Import The store
import store from '~/store'

store.state.myMod.someField

在组件中使用this.$store访问

this.$store.state.myMod.someField

4. 通过getModule()安全访问Store

除此之外,对于更多类型安全的访问,我们可以使用getModule(), 修改之前编写的文件:

  • src/store/index.ts
  • src/store/MyModule.ts
  • src/views/useVuex.vue
// src/store/index.ts
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {}
})

注意: src/store/index.ts没有定义任何Module

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

@Module({ dynamic: true, store, name: 'mymod' })
export default class MyModule extends VuexModule {
  someField: string = 'somedata'
}
// src/views/useVuex.vue
<template>
  <div>
    {{hello}}
  </div>
</template>

<script lang='ts'>
import { Vue, Component, Emit, Inject, Model, Prop, Provide, Ref, Watch, PropSync } from 'vue-property-decorator'
import { Module, VuexModule, getModule } from 'vuex-module-decorators'

import MyModule from '@/store/MyModule'

const $myMod = getModule(MyModule) // 取得Store中某一module

@Component
export default class UseVuex extends Vue {
  hello: string = $myMod.someField
}
</script>

核心

1. state

类中所有定义的属性都会被定义为state属性, 例如:

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

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

等价于:

export default {
  state: {
    wheels: 2
  }
}

2. Getters

所有get 函数, 都会都转化为vuex的getters, 例如:

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

@Module
export default class Vehicle extends VuexModule {
  wheels = 2
  // get axles 将会转化为vuex的getters
  get axles() {
    return wheels / 2
  }
}

等价于:

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

3. Mutations

所有被@Mutation装饰器装饰的函数, 都会被转化为vuex mutations. 例如:

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
  }
}

等价于:

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

注意

一旦使用@Mutation装饰某一函数后, 函数内的this上下文即指向当前的state.
因此, 如果想修改state中某些属性的值, 可以由原来的state.item++直接写为this.item++

警告

Muation函数不可为async函数, 也不能使用箭头函数来定义, 因为在代码需要在运行重新绑定执行的上下文.

4. Actions

所有被@Actions装饰器装饰的函数, 都会被转化为vuex Actions. 例如:

import { Module, VuexModule, Mutation } 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)
  }
}

等价于

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)
    }
  }
}

注意

用@Action装饰后,在函数内commit时, 只需调用this.context.commit('mutationName',mutPayload)

警告

  • 如果需要在action函数中运行耗时很长的任务/函数, 建议将该任务定义为异步函数. 即使你没有这么做, 该库会将你的函数用Promise包装它并等待它的执行结束.
  • 如果你坚持要使用同步函数执行任务, 请改使用Mutation
  • 千万不要使用(=>)箭头函数来定义action函数, 因为在运行时需要动态绑定this上下文.

高级用法

1. 名字空间

阅读本节前, 需要先了解什么是vuex名字空间

如果你想通过名字空间的形式来使用module, 需在@Module装饰器中添加额外的参数. 例如, 以下示例代码中添加一个namespacedmm的module

@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
  }
})

注意

@Module装饰器的属性字段name值, 必须要new store({modules: {}})中注册module name名称要一致.

手动保持这两个相同会使编码显得不优雅,但这一点很重要。我们必须将:
this.store.dispatch('action')调用转换为this.store.dispatch('name / action'),并且我们需要在装饰器中使用正确的名称才能使其正常工作

2. 动态module

阅读本节前, 需要先了解vuex - dynamic-module-registration

创建一个动态module很简单, 只需要在 @Module装饰器中多传入几个参数, 但前提条件是, 需要先创建一个Store对象, 并将其传入到module中.

1) 创建Store

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

Vue.use(Vuex)

export default new Vuex.Store({
  /*
  若所有的module都是动态创建的, 那这里的参数为空
  */
})

2) 创建一个动态module

// @/store/modules/MyModule.ts
import store from '@/store' // 注意引入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
  */
}
注意

到目前为止, vuex-module-decorators 暂不支持动态嵌套module

重要

确保你在import/required时, store实例已经创建, 它必须要在动态module之前已经创建好.

store实例必须提前创建好, 这很重要. 因为store实例将在以参数的形式传递到@Module装饰器中, 这样动态module才能注册成功.

相关链接