vue + ts Vuex篇


Vuex對Typescript的支持,仍十分薄弱,官方庫只是添加了一些.d.ts聲明文件,並沒有像vue 2.5這樣內置支持。

第三方衍生庫 vuex-typescriptvuex-ts-decoratorsvuex-typexvuex-class等等,我個人的總結,除了vuex-class外,基本都存在侵入性太強的問題,引用不算友好。而vuex-class提供的功能其實也是薄薄一層,並不能解決核心痛點。因此,需要手動添加輔助的地方,其實頗多。

核心痛點:每次調用 this.$store.dispatch / this.$store.commit / this.$store.statethis.$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) { // ... } }

其中第二個參數productspayload 參數,用法同上步 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根本用不到的參數,才能得到需要的dispatchcommit,在我看來,難用至極。

個人更喜歡如下寫法:

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)
  }
}

 




歡迎關注公眾號,進一步技術交流:

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM