使用 Typescript 加强 Vuex 使用体验

栏目: 编程语言 · 发布时间: 6年前

内容简介: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, 我们其实可以在其之上再细化一下类型限制。

关键代码

这个文件导出了一些类型,以及两个需要传入类型的高阶函数 makeDispatchermakeMutator ,具体使用可以看再之后的示例。

// 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)
复制代码

接下来可以看看顺滑的编辑器提示体验:

使用 Typescript 加强 Vuex 使用体验

声明好 MutationPayloads 以后,可以借助一个工具函数生成一个新的类似于 commit 的函数:

/**
 * 一个带有类型变量的函数,使用示例 todoItemMutate(actionContext, 'mutationName', mutationPayload)
 */
export const todoItemMutate = makeMutator<TodoContext, MutationPayloads>(VUEX_NS)
复制代码

让我们来改一改 GET_USER_TODOS 里提交 mutation 的方式,同时享受到代码提示:

使用 Typescript 加强 Vuex 使用体验

好的,现在可以使用另一个工具函数,生成一个新的带有类型限制的类似于 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 函数,其他文件里的代码若想享受这个类型增强,必须显式地 import todoItemDispatch / todoItemMutate 方法,略微麻烦

以上所述就是小编给大家介绍的《使用 Typescript 加强 Vuex 使用体验》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

软件测试的艺术

软件测试的艺术

梅尔斯 / 机械工业出版社 / 2006年01月 / 22.0

《软件测试的艺术》(原书第2版)成功、有效地进行软件测试的实用策略和技术:    基本的测试原理和策略      验收测试    程序检查和走查         安装测试    代码检查            模块(单元)测试    错误列表            测试规划与控制    同行评分            独立测试机构    黑盒、白盒测试    ......一起来看看 《软件测试的艺术》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

html转js在线工具
html转js在线工具

html转js在线工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具