使用 Typescript 增強 Vuex 使用體驗

碰到的問題

如履針氈的狀態管理和業務邏輯調用

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, 咱們其實能夠在其之上再細化一下類型限制。編輯器

關鍵代碼

這個文件導出了一些類型,以及兩個須要傳入類型的高階函數 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)
複製代碼

接下來能夠看看順滑的編輯器提示體驗:

實現 MUTATIONS

聲明好 MutationPayloads 之後,能夠藉助一個工具函數生成一個新的相似於 commit 的函數:

/** * 一個帶有類型變量的函數,使用示例 todoItemMutate(actionContext, 'mutationName', mutationPayload) */
export const todoItemMutate = makeMutator<TodoContext, MutationPayloads>(VUEX_NS)
複製代碼

讓咱們來改一改 GET_USER_TODOS 裏提交 mutation 的方式,同時享受到代碼提示:

調用 mutator

好的,如今能夠使用另外一個工具函數,生成一個新的帶有類型限制的相似於 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 方法,略微麻煩
相關文章
相關標籤/搜索