Vuex 實戰:如何在大規模 Vue 應用中組織 Vuex 程式碼 | 掘金技術徵文

滴滴出行·DDFE發表於2016-12-26

作者:滴滴公共前端團隊

前言:

最早我們在設計《Vue.js權威指南》這本書的時候也一直思考要不要加入 Vuex 相關的內容,也有很多同學抱怨說我們沒有加入這個章節。

其實整體我們應用的還是比較早,也在 1.0 和 2.* 都踩了一些坑,但是也不期望大家在任何複雜不復雜的場景裡面濫用 Vuex。

後面我們在 vue 2.0 全家桶原始碼分享系列裡面也分享了一篇《Vuex 2.0 原始碼分析》,沒有看過的同學可以在文末連結檢視

正文:

Vuex 作為中大型 Vue 應用中的“御用”集中資料管理工具,在滴滴很早就得到了廣泛使用。本文旨在以儘可能簡潔的文字向讀者展示:如何在一個頗具規模的 Vue 應用中組織和管理 Vuex 的程式碼

注:雖然目前 Vuex 的最新版本已經來到 2.x。2.x 在1.0 的基礎上進行了一些優化,提升了命名的語義化以及 增強了模組的可移植性和可組合性,但基本思想和架構並沒有改變。

本文基於 Vuex 1.0 版本,讀者大可不必擔心出現類似 Angular 1.x 升級到 2.x 式的斷崖式更新。

首先,介紹一下專案的背景:
一個採用 Vue.js 編寫的富互動的 H5 編輯器,由於各個元件中的資料互動繁多,頁面的生成也極度依賴儲存的狀態,使用 Vuex 進行管理便勢在必行。
專案引入 Vuex 的方式如下:

import App from 'components/home/App'
import store from 'vuex/editor/store'

// 在 Vue 例項的初始化中宣告 store。
new Vue({
  el: 'body',
  components: {
    App
  },
  store
})複製程式碼

在根例項中註冊 store 選項,這樣該 store 例項會注入到根元件下的所有子元件中,方便後面我們在每個子元件中呼叫 store 中 state 裡儲存的資料。

然後看一下 vuex 資料夾下的目錄,後面我們會逐個分析每個檔案的作用:

└── editor

    ├── mutation-types.js
    ├── actions
    │   └── index.js
    ├── mutations
    │   └── index.js
    ├── plugins
    │   └── index.js
    ├── state
    │   └── index.js
    └── store
        └── index.js複製程式碼

建立 store 物件的程式碼放在 vuex/editor/store/index.js 中,如下所示:

// vuex/editor/store/index.js
import Vuex from 'vuex'
import state from 'vuex/editor/state'
import mutations from 'vuex/editor/mutations'
import { actionLogPlugin } from 'vuex/editor/plugins'

const store = new Vuex.Store({
  state,
  mutations,
  plugins: [actionLogPlugin]
})

export default store複製程式碼

這裡又宣告瞭 state 和 mutations 物件,以及宣告瞭使用到的 plugins。plugins 後面再說,先看 state 和 mutations,相信各位讀者已經對 Vuex 中各個部件的作用已經瞭如指掌,但是為防遺忘,還是貼一下這張圖吧:

Vuex 實戰:如何在大規模 Vue 應用中組織 Vuex 程式碼 | 掘金技術徵文

state 是用於儲存各種狀態的核心倉庫,讓我們一瞥 vuex/editor/state/index.js 中的內容:

// 編輯器相關狀態
const editor = {
  ...
}

// 頁面相關狀態

let page = {
  ...
}

const state = {
  editor,
  page
}

export default state複製程式碼

state 中儲存了 editorpage 兩個物件,用於儲存不同模組的狀態。需要說明的是,這裡完全可以使用模組機制將其拆開,在 editor.js 裡儲存編輯器相關的 state 和 mutations,在 page.js 中儲存頁面相關的 state 和 mutations,以使結構更加清晰。不過這裡沒有使用模組機制,由於模組數量並不多,也是完全可以接受的。

這些 state 需要反映到元件中。

跳過官方文件中對為何不使用計算屬性的解釋,我們直接來看最佳實踐:在子元件中通過 vuex.getters 來獲取該元件需要用到的所有狀態:

// src/components/h5/Navbar.vue

...
export default {
    data () {
      return {
        ...
      }
    },
    methods: {
      ...
    },
    vuex: {
      actions: {
        ...
      },
      getters: {
        editor(state) {
          return state.editor
        },
        page(state) {
          return state.page
        },
        ...
      }
    }
}複製程式碼

vuex.getters 物件中,每個屬性對應一個 getter 函式,該函式僅接收 store 中 state,也就是總的狀態樹作為唯一引數,然後返回 state 中需要的狀態,然後在元件中就可以以 this.editor 的方式直接呼叫,類似計算屬性。

再看一下 vuex/editor/mutations/index.js 中的內容:

import * as types from '../mutation-types'

const mutations = {
  [types.CHANGE_LAYER_ZINDEX] (state, dir, index) {
    ...
  },
  [types.DEL_LAYER] (state, index) {
    ...
  },
  [types.REMOVE_FROM_ARR] (state, arr, itemToRemove) {
    ...
  },
  [types.ADD_TO_ARR] (state, arr, itemToAdd) {
    ...
  },
  [types.DEL_SCENE] (state, index) {
    ...
  },
  ...
}

export default mutations複製程式碼

具體業務邏輯這裡不展開,mutations 中主要就是定義各種對 state 的狀態修改。每個 mutation 函式接收第一個引數為 state 物件,其餘引數則為一路從元件中觸發 action 時傳過來的 payload。所有的 mutation 函式必須為同步執行,否則無法追蹤狀態的改動。

注意到,這裡引入了 mutation-types.js。該檔案主要作用為放置所有的命名 Mutations 的常量,方便合作開發人員釐清整個 app 包含的 mutations。在採用模組機制時,可以在每個模組內只引入相關的 mutations,也可以像本專案一樣使用 import * as types 簡單粗暴地引入全部。

mutation-types.js 中內容大致如下:

export const CHANGE_LAYER_ZINDEX = 'CHANGE_LAYER_ZINDEX'
export const DEL_LAYER = 'DEL_LAYER'複製程式碼

然後我們來到 actions,照例先看一下 vuex/editor/actions/index.js 中的內容:

import * as types from '../mutation-types'

export function delLayer( { dispatch }, index) {
  dispatch(types.DEL_LAYER, index)
}

export function delScene( { dispatch }, index) {
  dispatch(types.DEL_SCENE, index)
}

export function removeFromArr( { dispatch }, arr, itemToRemove) {
  dispatch(types.REMOVE_FROM_ARR, arr, itemToRemove)
}

export function addToArr( { dispatch }, arr, itemToAdd) {
  dispatch(types.ADD_TO_ARR, arr, itemToAdd)
}複製程式碼

actions 的主要工作就是 dispatch (中文譯為分發)mutations。初入門的同學可能覺得這是多此一舉,actions 這一步看起來完全可以省略。

事實上,actions 的出現是為了彌補 mutations 無法實現非同步操作的缺陷。所有的非同步操作都可以放在 actions 中,比如如果想在呼叫 delScene 函式 5 秒後再分發 mutations,可以寫成這樣:

function delScene ({ dispatch }, index) {
  setTimeout(() => {
    dispatch(types.DEL_SCENE, index)
  }, 5000)
}複製程式碼

觸發 mutations 的程式碼不會在元件中出現,但 actions 會出現在每個需要它的元件中,其也是連線元件和 mutations 的橋樑(額,另一條橋樑是 state,見上面那張經典老圖)。在子元件中引入 actions 的方式類似 state,也是註冊在 vuex 選項下:

// src/components/h5/Navbar.vue
...

import { 
  undoAction, 
  redoAction,
  togglePreviewStatus,
  ...
} from 'vuex/editor/actions'

export default {
    data () {
      return {
        ...
      }
    },
    methods: {
      ...
    },
    vuex: {
      actions: {
        undoAction,
        redoAction,
        togglePreviewStatus,
        ...
      },
      getters: {
        ...
      }
    }
}複製程式碼

這樣,元件中可以直接呼叫各個 actions,比如 this.togglePreviewStatus(status),等價於this.togglePreviewStatus( this.$store, status)(還記得我們在 actions 中定義的各個函式的第一個引數是 store 嗎?)。這是最基本的使用 actions 的方式,在此基礎上你還可以玩出別的花樣來,比如給 actions 取別名、定義內聯 actions、繫結所有 actions 等,具體用法參見官方文件。

回過頭去看 vuex 資料夾下的目錄結構,發現還有一個 plugins 我們沒有介紹。老規矩,先看一下 vuex/editor/plugins/index.js 中的內容:

...
export function actionLogPlugin(store) {

  store.subscribe((mutation, state) => {

    // 每次 mutation 之後呼叫
    // mutation 的格式為 { type, payload }
    ...
  })
}複製程式碼

核心部分在於採用 store.subscribe 註冊了一個函式。

該函式會在每次 mutation 之後被呼叫。這裡 actionLogPlugin 函式完成的是記錄每次 mutation 操作,實現撤銷重做功能。具體實現邏輯此處不作贅述。

後續我們也會深入地給大家分享 vuex 應用相關的內容

附:

Vuex 2.0 原始碼分析知乎地址:zhuanlan.zhihu.com/p/23921964

「掘金技術徵文」活動:gold.xitu.io/post/58522d…


歡迎關注DDFE
GITHUB:github.com/DDFE
微信公眾號:微信搜尋公眾號“DDFE”或掃描下面的二維碼

Vuex 實戰:如何在大規模 Vue 應用中組織 Vuex 程式碼 | 掘金技術徵文

相關文章