iT邦幫忙

2021 iThome 鐵人賽

DAY 14
0
Modern Web

不只懂 Vue 語法:Vue.js 觀念篇系列 第 14

不只懂 Vue 語法:為什麼要用 Vuex? Vuex 基本架構是怎樣?

問題回答

使用 Vuex 是為了當元件之間都需要共用資料時,使用一個像是公用容器來管理資料,我們把所有要共用的資料都拉進此容器中,讓所有元件都能在此容器取得或操作資料。

使用 Vuex 的好處:

  • 不用 Vuex 來操作資料,就可能會出現有時用 props,有時用 event bus 這種不統一的情況。但使用 Vuex 的話,就能統一操作。
  • 如果頻繁使用 event bus 或 mitt,就要不斷在不同元件綁上監聽和事件觸發,比起 Vuex 的集中管理顯得較散亂,難以追蹤資料流,提高除錯的難度。

Vuex 的基本架構:

  • state:存放資料狀態。
  • actions:負責觸發 mutation 來改變 state 的資料。Actions 會以非同步方式執行程式碼。
  • mutations:修改 state 資料。
  • getters:取得資料。也可以像 computed 一樣,自定義運算處理資料。
  • modules:按專案功能需求,分拆為不同 module。每個 module 裏都有自己的 state、actions、mutations、getters,也可以設定巢狀 modules。

關於 Vuex 的知識會比較多,因篇幅所限,以下只會簡述 Vuex 最基本的架構,並以我自己作為新手覺得較重要或常常忽略的部分去解說。

為什麼元件能取到 Vuex 的資料?

因為 Vuex 會透過 Vue 插件,把 store 實體從根元件注入到子元件裏。因此,子元件透過 this.$store 就能操作在 Vuex 裏的資料。

Vuex 基本架構

當你建立一個有 Vuex 的 Vue CLI 專案,在 store/index.js 就會出現以下的結構,並先利用 createStore 來建立一個 store 的實體。並 export 回到 main.js 裏使用。

import { createStore } from 'vuex'

export default createStore({
  state: {
      // 所有在 store 裏的資料
  },
  actions: {
      // 負責觸發 mutations
      // 可處理非同步程式(e.g: 打 API)
  },
  mutations: {
      // 負責改變 state 裏的資料
  },
  getters: {
      // 像 computed 一樣,運算處理 state 資料
  },
  modules: {
      // 按需求分拆 module
      // 每個 module 都有自己的state, actions, mutations, getters, modules
  }
})

Vuex 單一資料流

未使用 Vuex 之前,資料流會像是下圖描述,以 props / emit 為例,當 emit 觸發事件,就會修改資料,最後畫面會再渲染那筆更新的資料。

畫面觸發事件(View) --> emit (Actions) --> 修改資料,包括修改 props所傳遞的資料(State) --> 更新畫面 (View)

比起以上做法,Vuex 仍然會遵從單一資料流的做法,但執行過程會更嚴謹,明顯不同是:

  • actions 與 state 之間多了一個 mutaions。
  • 修改資料時(state),建議透過 actions 觸發(commit) mutations,再由 mutations 來修改資料。不是直接讓 actions 修改資料
  • actions 可以非同步執行程式,因此打 API 的動作要在 actions 裏處理。
  • mutations 只能同步執行程式。

Todo list - 此文篇所使用的例子

整篇文章會以一個簡單的 todo list 為例子,但不會每個部分都作講說,只會針對看看 Vuex 裏所有函式需要注意的地方。例子如下:

https://codesandbox.io/s/vuex-vuex-todo-list-shi-fan-b6iud

Actions(觸發 mutations)

Actions 作為就是觸發 mutations,在函式裏可使用 context 參數,例如以下我在例子中的寫法:

toggleComplete(context, id) {
    this.commit("updateComplete", { id });
}

context 物件就是 store 所有的屬性和方法,包括 state, getter 等等:

Mutations(修改 states)

使用 mutations 時,有兩點要先注意:

  • mutations 必須是同步函式,而 actions 則可以是非同步。
  • 建議不論在元件還是在 Vuex 裏,不要直接觸發 mutations 來修改資料。謹守只能透過 actions 來觸發 mutations 來修改資料的原則,否則 debug 時難以追蹤資料變化的來源。

關於第二點,舉例說,以下寫法並不建議

this.$store.commit('changeSth', 你想傳遞的資料)

這寫法是在元件裏,直接跳過 actions 來觸發 mutations。雖然這不會報錯,但是不是良好的寫法。因為在眾多元件裏,有時使用 commit,有時使用 dispatch,就會導致資料流不統一,以致程式難以維護。所以一律建議遵從官方做法,保持使用 dispatch 來觸發 actions,再由 actions 觸發 mutations。並非直接使用 commit 觸發 mutations。

另外,有一個 payload 的物件可以使用。如果我們傳送參數到 mutations 時是以物件的方式傳送,我們可以在 payload 裏找到,例如我在 store/actions.js 是以 { id } 這樣來觸發 mutations,並把此物件傳到 mutaions 裏:

TodoCard.vue:

methods: {
    changeComplete(id) {
        this.$store.dispatch('toggleComplete', id)
    }
}

store/actions.js

toggleComplete(context, id) {
    this.commit("updateComplete", { id });
}

store/mutations.js

updateComplete(state, payload) {
    const target = state.todos.find((todo) => todo.id === payload.id);
    target.complete = !target.complete;
}

例如,當我按下 Watch movie,把此 todo 事項的 id (即是 2),傳到 toggleComplete actions 和 updateComplete mutations 裏,在 mutation 再用 console.log(payload) 查看 payload 物件:

如果在 actions 裏,不用物件包起來也行。但官方建議是使用物件。有些時候我們會傳多筆資料而需要用到的物件,因此統一使用物件會比較單純。

map helpers

在元件中操作 store 資料時,可使各種 map helpers 來引入 store 的各種屬性到元件裏使用。例如我在 computed 裏:

import { mapGetters } from 'vuex';

computed: {
    ...mapGetters({ allTodos: 'getTodos' })
}

以上寫法即等於:

computed: {
    ...mapGetters(['getTodos']),
    allTodos() {
      return this.getTodos;
    }
}

mapGetters 本身是一個函式,它會回傳在 store 的 getters 裏的函式,例如你傳入了 'getTodos',它就回傳在 store 的 getter 裏的 getTodos 函式給你:

mapGetters(['getTodos']) // {getTodos: ƒ}

官方文件這裏提到,如果你想為 getTodos 這函式設置另一個屬性,就需要傳入物件:

mapGetters({ allTodos: 'getTodos' }) // {allTodos: ƒ}

最後,使用展開語法 ... ,把 {allTodos: ƒ} 裏的屬性和值,展開在我們例子中的 computed 裏。

除了 mapGetters,還有其他 map helpers 可以使用。使用方法也是大致相同:

import { mapGetters, mapState,  mapActions, mapMutations} from 'vuex';

分拆 store 的做法

當專案規模較大時,可以把 store 裏的 state、getters、mutations、actions 都獨立拆成一個個檔案,再逐一引入到 store/index.js 這個主檔案中。

檔案結構:

store
    index.js
    getters.js
    actions.js
    mutations.js
    state.js

store/index.js:

import { createStore } from 'vuex';
import state from './state';
import actions from './actions';
import mutations from './mutations';
import getters from './getters';

export default createStore({
  state,
  actions,
  mutations,
  getters
});

以 index/actions.js 為例,actions.js 就會放我們所有的 actions 函式:

export default {
  toggleComplete(context, id) {
    this.commit('updateComplete', { id });
  },
  ...
};

什麼是 modules?

另外,在大型專案裏,可以按功能需求去拆分 module,再在主檔案 store/index.js 裏的 moduleds 屬性裏引入。每個 module 就是另一個 store,裏面同樣可以有自己的 state、actions、mutations、getters、modules。

例如在示範的 todo list 例子中,我把備註欄訊息拆成一個 module:

store/Note/index.js

export default {
  // 當 namespaced 是 true 時,載入這裏的資料時,必須寫 'Note/...'
  namespaced: true,
  state: {
    note: 'Dummy text'
  },
  actions: {
    createNote(context, note) {
      context.commit('setNote', note);
    }
  },
  mutations: {
    setNote(state, note) {
      state.note = note;
    }
  },
  getters: {
    getNote(state) {
      return state.note;
    }
  }
};

回到主檔案 store/index.js,在 modules 屬性引入 Note module:

export default createStore({
  state,
  actions,
  mutations,
  getters,
  modules: {
    Note
  }
});

並在 App.vue 裏引入,把 Dummy text 顯示出來:

App.vue:

  computed: {
    ...mapGetters({ allTodos: 'getTodos', note: 'Note/getNote' })
  },

當 namespaced 設定為 true 時,引入時需標示它所屬的 module

以上示範中,我需要用 Note/getNote 來引入 Note module 裏的 note 資料。因為我在 Note/index.js 裏設定了 namespaced: true

設定 namespaced 好處是,在命名所有 state、actions 或 mutations 等等的資料或函式名稱時,不用擔心與主檔案 store/index.js 裏的已定義的 state、actions 等資料或函式名稱相同,以致產生衝突。同時,提高程式碼的可讀性,讓人一眼就看到此東西是存在於公用的 store,還是某個 module 裏。

總結

  • Vuex 是為了方便元件之間大量傳遞資料。它像是一個公用容器,所有元件都能操控這容器裏的資料。這種集中管理的方法讓程式碼更易維護。
  • Vuex 遵從單一資料流。
    由畫面觸發事件 --> 觸發 actions --> 觸發 mutations --> 修改 state --> 更新畫面
  • Actions 可處理非同步程式,所以打 API 的程式碼都寫在 actions 裏。而 mutations 只能同步執行程式。
  • 若要取得資料(state),建議一律使用 getters 來取得。
  • 若要修改資料(state),建議一律透過觸發 actions,再觸發 mutations 來修改資料。
  • 觸發 mutations 函式時,如需帶入參數,建議使用物件型別來帶入,此物件會成為 mutations 函式裏的 payload。
  • 在元件引入 store 裏的屬性時,可使用 map helpers。
  • 大型專案中,可考慮把所有 store 裏的屬性(state, getters, actions, mutations)拆分為獨立檔案。
  • 大型專案中,可考慮按功能需求拆分 module。每個 module 是一個 store,有自己的 state, getters, actions, mutations, modules 屬性。然後再在主檔案 store/index.js 的 modules 屬性裏引入這些 module。

參考資料

2021 Vue3 專業職人 - 入門篇
LEARN VUEX IN 15 MINUTES (VUE.JS STATE MANAGEMENT) FOR 2020 // DAD JOKE GENERATOR APP -VUEX TUTORIAL
Vuex 面試題:使用 vuex 的核心概念


上一篇
不只懂 Vue 語法:如何透過路由實現跨頁面傳遞資料?
下一篇
不只懂 Vue 語法:請說明 keep-alive 以及 is 屬性的作用?
系列文
不只懂 Vue 語法:Vue.js 觀念篇31

尚未有邦友留言

立即登入留言