Vue2.5+ Typescript 引入全面指南 - Vuex篇
系列目錄:
- Vue2.5+ Typescript 引入全面指南
- Vue2.5+ Typescript 引入全面指南 - Vuex篇
前言
Vuex 正是我下決心引入Typescript的核心痛點。與 vuex 相關的程式碼中,到處充斥著此般寫法:
再加上vuex的 dispatch/commit
並非直接引用程式碼,而是是通過一個string型別的 type
來標記,如此一來上圖中寫法,如果想檢視 payload
的具體內容,甚至不能借助於編輯器的查詢定義,只能手動去切程式碼檢視!簡直苦不堪言。
而藉助於 typescript
的 interface
介面,至少可以簡化成如下效果:
這麼寫一樣麻煩,不是還需要記類似PostLoginParams的Interface型別?這簡單,建個輔助函式就是:
編輯器裡一個 Ctrl + 空格
,payload
裡有哪些引數就全出來,再也不用去一遍遍翻程式碼,效率直線提升!
現狀概述
截至當前2017年11月,Vuex對Typescript的支援,仍十分薄弱,官方庫只是新增了一些.d.ts
宣告檔案,並沒有像vue 2.5
這樣內建支援。
第三方衍生庫 vuex-typescript
, vuex-ts-decorators
, vuex-typex
, vuex-class
等等,我個人的總結,除了vuex-class
外,基本都存在侵入性太強的問題,引用不算友好。而vuex-class
提供的功能其實也是薄薄一層,並不能解決核心痛點。因此,需要手動新增輔助的地方,其實頗多。
核心痛點:每次呼叫 this.$store.dispatch
/ this.$store.commit
/ this.$store.state
/ this.$store.getters
都會伴隨著型別丟失。
其中,dispatch/commit
可以通過建立輔助函式形式,簡單繞開。 state/getters
沒有太好辦法,只能手動指定,若覺得麻煩,可以全都指成 any
,等官方支援。官方動態見此 issue
動手改造第一步:從 shopping-cart 示例搬運程式碼
以下示例基於
vuex
官方examples
中最複雜的一個 shopping-cart,
改造後的完整程式碼見 vue-vuex-typescript-demo
準備工作:
-
shopping-cart
程式碼複製至專案目錄下 -
.js
檔案統一重新命名為.ts
, -
currency.js
/api/shop.js
/components/App.vue
等外圍檔案的ts改造 -
npm i -D vuex
新增依賴
詳細步驟這裡略去,參照 程式碼庫 即可
動手改造第二步:State改造
用到state變數的地方實在太多,不僅store
目錄下 action/getter/mutation 均有可能需要,甚至在 .vue
檔案裡,mapState
也有引用,因此我個人總結的一套實踐:
- store/modules下的每個子模組,均維護自己名為 State 的 Interface 宣告
- store/index.ts 檔案中,彙總各子模組,維護一個總的State宣告
store/modules
下檔案舉例:
// ./src/store/modules/cart.ts
interface Shape {
id: number
quantity: number
}
export interface State {
added: Shape[]
checkoutStatus: 'successful' | 'failed' | null
}
// initial state
// shape: [{ id, quantity }]
const state: State = {
added: [],
checkoutStatus: null
}
// 需引用state的地方舉例:
const getters = {
checkoutStatus: (state: State) => state.checkoutStatus
}
store/index.ts
檔案總 State 舉例:
// ./src/store/index.ts
import { State as CardState } from './modules/cart'
import { State as ProductsState } from './modules/products'
export interface State {
cart: CardState,
products: ProductsState
}
總 State
引用示例:
// ./src/store/getters.ts
import { State } from './index'
const cartProducts: Getter<State, any> = (state: State) => {
return state.cart.added.map(shape => {
// 此處shape自動推匯出Shape型別
// ... 詳見原始碼
})
}
如此,所有直接引用 state
的地方,均可啟用型別推導
動手改造之 Mutation
Mutation
對應 store.commit
命令,常見寫法:
const mutations = {
[types.ADD_TO_CART] (state, { id }) {
// ...
}
}
state 上步已處理{ id }
,payload
引數,即為開篇介紹型別缺失的重災區。
我的一套個人實踐:
-
store/modules
下的子模組檔案,為自己的mutations
維護 payload Interface宣告 - 子模組共用 payload(多個模組響應同一 commit 等),在
store/index.ts
中統一維護 - 新建檔案
store/dispatches.ts
檔案,為每一個直接呼叫的帶參commit
維護輔助函式,以應用型別推導
子模組 payload
宣告舉例:
// ./src/store/modules/products.ts
import { Product, AddToCartPayload } from '../index'
export interface ProductsPayload {
products: Product[]
}
const mutations = {
[types.RECEIVE_PRODUCTS] (state: State, payload: ProductsPayload) {
state.all = payload.products
},
[types.ADD_TO_CART] (state: State, payload: AddToCartPayload) {
const product = state.all.find(p => p.id === payload.id)
// ...
}
}
// mutations呼叫舉例:
const actions = {
getAllProducts (context: ActionContextBasic) {
shop.getProducts((products: Product[]) => {
const payload: ProductsPayload = {
products
}
context.commit(types.RECEIVE_PRODUCTS, payload)
})
}
}
store/index.ts
檔案公共 payload
宣告舉例:
// ./src/store/index.ts
export interface AddToCartPayload {
id: number
}
store/dispatches.ts
檔案,commit
輔助函式,參見下步同檔案dispatch
輔助函式
動手改造之 Action
Action
對應 store.dispatch
命令,常見寫法:
const actions = {
checkout ({ commit, state }, products) {
// ...
}
}
其中第二個引數products
,payload
引數,用法同上步 Mutation
的 payload
引數,不再贅述。
第一個引數{ commit, state }
,context
引數,vuex
的 d.ts
提供有型別 ActionContext
,用法如下:
import { ActionContext } from 'vuex'
const actions = {
checkout (context: ActionContext<State, any>, products: CartProduct[]) {
context.commit(types.CHECKOUT_REQUEST)
// ...
}
}
ActionContext<State, RootState>
傳入兩個大部分Action根本用不到的引數,才能得到需要的dispatch
, commit
,在我看來,難用至極。
個人更喜歡如下寫法:
const actions = {
checkout (context: { commit: Commit, state: State }, products: CartProduct[]) {
context.commit(types.CHECKOUT_REQUEST)
// ...
}
}
Action
payload 改造參見步驟 Mutation
,不再贅述。
store/dispatches.ts
檔案,dispatch
輔助函式:
// ./src/store/dispatches.ts
import store, { CartProduct, Product } from './index'
export const dispatchCheckout = (products: CartProduct[]) => {
return store.dispatch('checkout', products)
}
.vue
檔案呼叫舉例:
// ./src/components/Cart.vue
import { dispatchCheckout } from '../store/dispatches'
export default Vue.extend({
methods: {
checkout (products: CartProduct[]) {
// this.$store.dispatch 寫法可用,但不帶型別推導
// this.$store.dispatch('checkout', products)
dispatchCheckout(products) // 帶有型別智慧提示
}
}
})
動手改造之 Getter
Getter
常見寫法:
const getters = {
checkoutStatus: state => state.checkoutStatus
}
需要改的不多,state 加上宣告即可:
const getters = {
checkoutStatus: (state: State) => state.checkoutStatus
}
動手改造之獨立的 Mutations/Actions/Getters 檔案
獨立檔案常規寫法:
// ./src/store/getters.js
export const cartProducts = state => {
return state.cart.added.map(({ id, quantity }) => {
const product = state.products.all.find(p => p.id === id)
return {
title: product.title,
price: product.price,
quantity
}
})
}
引用:
// ./src/store/index.js
import * as getters from './getters'
export default new Vuex.Store({
getters
})
typescript
下均需改造:
// ./src/
import { GetterTree, Getter } from 'vuex'
import { State } from './index'
const cartProducts: Getter<State, any> = (state: State) => {
return state.cart.added.map(shape => {
// ...
})
}
const getterTree: GetterTree<State, any> = {
cartProducts
}
export default getterTree
Actions/Mutations 檔案改造同上,型別換成 ActionTree, Action, MutationTree, Mutation即可
引用:
// ./src/store/index.js
import getters from './getters'
export default new Vuex.Store({
getters
})
原因是vuex
定義,new Vuex.Store
引數型別 StoreOptions 如下:
export interface StoreOptions<S> {
state?: S;
getters?: GetterTree<S, S>;
actions?: ActionTree<S, S>;
mutations?: MutationTree<S>;
modules?: ModuleTree<S>;
plugins?: Plugin<S>[];
strict?: boolean;
}
於是,獨立Gettes/Actions/Mutations檔案,export 必須是GetterTree/ActionTree/MutationTree型別
動手改造之 .vue 檔案呼叫
- 傳統寫法全部相容,只需
mapState
為state新增型別 (state: State) => state.balabal 等很少改動即可正常執行。只是型別均為any
- 建議不使用
mapState
/mapGetters
/mapActions
/mapMutations
,以明確指定型別 -
dispatch
及commit
呼叫可通過上述store/dispatches.ts
下輔助函式,手動開啟型別推導 -
state
及getters
型別推導,暫時只能手動指定。自動推導,估計得等官方內建支援了。
完整呼叫示例:
// ./src/components/ProductList.vue
import Vue from 'vue'
// import { mapGetters, mapActions } from 'vuex'
import { Product } from '../store'
import { dispatchAddToCart } from '../store/dispatches'
export default Vue.extend({
computed: {
// ...mapGetters({
// products: 'allProducts'
// })
products (): Product[] {
return this.$store.getters.allProducts
}
},
methods: {
// ...mapActions([
// 'addToCart'
// ])
addToCart (p: Product) {
dispatchAddToCart(p)
}
},
created () {
this.$store.dispatch('getAllProducts')
}
})
vue-class-component + vuex-class 元件式寫法
如果覺得以上廢棄 mapState
/ mapGetters
後的寫法繁瑣,可引入vue-class-component
+ vuex-class
,開啟元件式寫法
-
vue-class-component
,vue官方維護,學習成本低 -
vuex-class
,作者ktsn
,vuex及vue-class-component貢獻排第二(第一尤雨溪了)的活躍開發者,質量還是有保障的
引入這倆依賴後,須在 tsconfig.json
新增配置:
{
"compilerOptions": {
// 啟用 vue-class-component 及 vuex-class 需要開啟此選項
"experimentalDecorators": true,
// 啟用 vuex-class 需要開啟此選項
"strictFunctionTypes": false
}
}
Component
寫法示例:
import Vue from 'vue'
import { Product } from '../store'
// import { dispatchAddToCart } from '../store/dispatches'
import Component from 'vue-class-component'
import { Getter, Action } from 'vuex-class'
@Component
export default class Cart extends Vue {
@Getter('cartProducts') products: CartProduct[]
@Getter('checkoutStatus') checkoutStatus: CheckoutStatus
@Action('checkout') actionCheckout: Function
get total (): number {
return this.products.reduce((total, p) => {
return total + p.price * p.quantity
}, 0)
}
checkout (products: CartProduct[]) {
// dispatchCheckout(products)
this.actionCheckout(products)
}
}
總結
在現階段 vuex
官方未改進 typescript
支援下,用 typescript 寫 vuex 程式碼,的確有些繁瑣,而且支援也稱不上全面,不過,總比沒有強。哪怕都用 any
,也能借助智慧提示減輕一些程式碼翻來翻去的痛苦。
至於再進一步更完美的支援,等官方更新吧。
完整程式碼
見 Github 庫:vue-vuex-typescript-demo