Vuex 使用了统一的 dispatch
/commit
方法去触发 Action 和 Mutation, 若是使用嵌套的 module, Vuex 还会解析命名空间,以找到正确的 Action/Mutation 函数。vue
对于组件来讲很友好,可是对于项目维护来讲可能就稍显痛苦。虽然对状态的更改都从视图逻辑里分离出来放在了 store 文件夹下,一目了然,可是对于触发 Action 的组件来讲,维持与 store 部分的函数签名和接口统一,就不得不靠全局搜索了(由此催发了一些 Vuex 下的最佳实践,例如动做名称都用 CAPITAL_SNAKE_CASE 命名法,或是抽取出 mutation-types.js 文件)。vuex
随着项目的推动,一旦 Action 有变更(增减参数数量或是改变类型等等),项目里有5个地方用到了它,而你粗心地只改了 4 个地方,在一个犄角旮旯的平时不会用到也没有测试覆盖的地方,一个炸弹默默地就冒了出来。typescript
在 Javascript 能力的限制下,只好靠命名规范和当心翼翼(不断搜索和确认修改)来规避的问题。其实借助 Typescript 强大的类型推断能力,这种心智负担是彻底能够避免的。bash
vuex 的 d.ts 文件提供了一些颇有用的类型,不过里面有好多 any, 咱们其实能够在其之上再细化一下类型限制。编辑器
这个文件导出了一些类型,以及两个须要传入类型的高阶函数 makeDispatcher
和 makeMutator
,具体使用能够看再以后的示例。函数
// vuex-util.ts
import { ActionContext, Store, Module } from 'vuex'
type DictOf<T> = {[key: string]: T }
export type ActionDescriptor = [any, any]
export type ModuleActions<Context, Descriptor extends DictOf<ActionDescriptor>> = {
[K in keyof Descriptor]: (ctx: Context, payload: Descriptor[K][0]) => Descriptor[K][1]
}
export type ModuleMutations<State, PayloadTree> = {
[K in keyof PayloadTree]: (state: State, payload: PayloadTree[K]) => any
}
function isStore(context: any) {
return ('strict' in context)
}
export function makeDispatcher<Context extends ActionContext<any, any>, Descriptor extends DictOf<ActionDescriptor>>(ns?: string) {
return <K extends keyof Descriptor>(
context: Store<any> | Context,
action: K,
payload: Descriptor[K][0],
) => {
const _context: any = context
let actionName = action as string
if (ns && isStore(context)) {
actionName = `${ns}/${action}`
}
return _context.dispatch(actionName, payload)
}
}
/** * 当此模块为 namespaced 的时候, `$store.commit(mutation)` 和在 action handler 内的 `ctx.commit(mutation)` 是不同的 */
export function makeMutator<Context extends ActionContext<any, any>, MutationPayloadTree>(ns?: string) {
return <K extends keyof MutationPayloadTree>(
context: Store<any> | Context,
mutation: K,
payload: MutationPayloadTree[K],
) => {
let mutationName = mutation as string
if (ns && isStore(context)) {
mutationName = `${ns}/${mutation}`
}
return context.commit(mutationName, payload)
}
}
复制代码
首先列一下最后导出的 Vuex Module 声明:工具
import { ActionContext, Module } from 'vuex'
export const VUEX_NS = 'todo'
/** 此对象接收类型变量, 分别表明 module state 和 rootState 类型 */
type TodoContext = ActionContext<TodosState, GlobalState>
// ...
export default {
namespaced: true,
state: {...},
actions: ACTIONS,
mutations: MUTATIONS,
} as Module<TodosState, GlobalState>
复制代码
声明 ACTIONS,很是简单地从 localStorage 里取出数据,而后调用 SET_TODOS
mutation:单元测试
type ActionDescriptors = {
GET_USER_TODOS: [{}, void]
}
const ACTIONS: ModuleActions<TodoContext, ActionDescriptors> = {
GET_USER_TODOS(ctx) {
const todos = JSON.parse(localStorage.getItem('todoItems')) || []
ctx.dispatch('SET_TODOS', { todos })
},
}
复制代码
接下来实现 MUTAIONS,先声明好全部 Mutation 须要的参数,并使用工具类型 ModuleMutations
标注即将给 Vuex Module 传递的 mutations 对象:测试
type MutationPayloads = {
SET_TODOS: { todos: TodoItem[] }
}
const MUTATIONS: ModuleMutations<TodosState, MutationPayloads> = {
}
复制代码
此时,TS 编译器会提示错误,是由于 ModuleMutations
这个工具类型要求包含第二个类型参数中全部的 key,一个空 Object 没有实现 MutationPayloads
所要求的 SET_TODOS
方法,所以会抛出 TS Error。优化
const MUTATIONS: ModuleMutations<TodosState, MutationPayloads>
'MUTATIONS' is declared but its value is never read.ts(6133)
Property 'SET_TODOS' is missing in type '{}' but required in type 'ModuleMutations<TodosState, MutationPayloads>'.ts(2741)
复制代码
接下来能够看看顺滑的编辑器提示体验:
声明好 MutationPayloads
之后,能够借助一个工具函数生成一个新的相似于 commit
的函数:
/** * 一个带有类型变量的函数,使用示例 todoItemMutate(actionContext, 'mutationName', mutationPayload) */
export const todoItemMutate = makeMutator<TodoContext, MutationPayloads>(VUEX_NS)
复制代码
让咱们来改一改 GET_USER_TODOS
里提交 mutation 的方式,同时享受到代码提示:
好的,如今能够使用另外一个工具函数,生成一个新的带有类型限制的相似于 dispatch
的函数,并在别处调用:
/** * 一个带有类型变量的函数,使用示例 todoItemDispatch(actionContext, 'actionName', actionName) */
export const todoItemDispatch = makeDispatcher<TodoContext, ActionDescriptors>(VUEX_NS)
...
todoItemDispatch(store, 'GET_USER_TODOS', {})
复制代码
此时你决定 TodoApp 须要能支持多用户,每一个用户有本身的记录。那么 ACTIONS 接口说明须要更改:
type ActionDescriptors = {
GET_USER_TODOS: [{ userName: string }, void]
}
复制代码
此时编译器会报错,在上一节最后的 todoItemDispatch
调用处:
Argument of type '{}' is not assignable to parameter of type '{ userName: string; }'.
Property 'userName' is missing in type '{}' but required in type '{ userName: string; }'.ts(2345)
todo.ts(., .): 'userName' is declared here.
复制代码
好的,如今开心地来到出错地方,改一改:
todoItemDispatch(store, 'GET_USER_TODOS', { userName: 'hikerpig' })
复制代码
没有全局搜索,不用作无谓的参数检查单元测试。
dispath
函数,其余文件里的代码若想享受这个类型加强,必须显式地 import todoItemDispatch
/todoItemMutate
方法,略微麻烦