内容简介:Vuex 使用了统一的对于组件来说很友好,但是对于项目维护来说可能就稍显痛苦。虽然对状态的更改都从视图逻辑里分离出来放在了 store 文件夹下,一目了然,但是对于触发 Action 的组件来说,维持与 store 部分的函数签名和接口统一,就不得不靠全局搜索了(由此催发了一些 Vuex 下的最佳实践,例如动作名称都用 CAPITAL_SNAKE_CASE 命名法,或是抽取出 mutation-types.js 文件)。随着项目的推进,一旦 Action 有变动(增减参数数量或是改变类型等等),项目里有5个
Vuex 使用了统一的 dispatch
/ commit
方法去触发 Action 和 Mutation, 如果使用嵌套的 module, Vuex 还会解析命名空间,以找到正确的 Action/Mutation 函数。
对于组件来说很友好,但是对于项目维护来说可能就稍显痛苦。虽然对状态的更改都从视图逻辑里分离出来放在了 store 文件夹下,一目了然,但是对于触发 Action 的组件来说,维持与 store 部分的函数签名和接口统一,就不得不靠全局搜索了(由此催发了一些 Vuex 下的最佳实践,例如动作名称都用 CAPITAL_SNAKE_CASE 命名法,或是抽取出 mutation-types.js 文件)。
随着项目的推进,一旦 Action 有变动(增减参数数量或是改变类型等等),项目里有5个地方用到了它,而你粗心地只改了 4 个地方,在一个犄角旮旯的平时不会用到也没有测试覆盖的地方,一个炸弹默默地就冒了出来。
在 Javascript 能力的限制下,只好靠命名规范和小心翼翼(不断搜索和确认修改)来规避的问题。其实借助 Typescript 强大的类型推断能力,这种心智负担是完全可以避免的。
解决方案
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) } } 复制代码
使用示例, 一个 Todo App
导出 Vuex Module
首先列一下最后导出的 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 和 MUTATIONS
声明 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' }) 复制代码
没有全局搜索,不用做无谓的参数检查单元测试。
总结
优点
- 优化开发时的体验
- 不改变原先 actions 和 mutations 成员函数实现的方式,也不像 vuex-typescript 引入了一些额外的 class
缺点
- 接口描述的类型需要提前单独声明,没法直接使用函数的签名
- 没法把所有类型增强合并为一个统一的
dispath
函数,其他文件里的代码若想享受这个类型增强,必须显式地 importtodoItemDispatch
/todoItemMutate
方法,略微麻烦
以上所述就是小编给大家介绍的《使用 Typescript 加强 Vuex 使用体验》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- Java基础加强笔记——测试、反射、注解
- 实施DevOps加强开发商管控(6.10)
- Firefox 85.0 正式发布,加强用户隐私保护
- 值得收藏的awesome-go中文加强版
- MariaDB 加强了其在开源 RDBMS 市场的地位
- NetApp收购Kubernetes初创公司StackPointCloud加强云产品组合
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。