不只懂 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 的核心概念


<<:  Swift 新手-Design pattern 软件开发设计模式

>>:  Day8 用python写UI-聊聊功能钮Button

.Net Core Web Api_笔记09_web api的属性路由模板两种写法_路由模板使用

在.net core mvc跟.net core web api专案中预设各自采用的一些配置 有不太...

JavaScript入门 Day28_for回圈

今天要讲的是for回圈,他跟while有异曲同工之妙 直接来看看下面的code吧~ var JsFr...

Day 16:架设 Grafana (2)

看来今天终於是可以把 Grafana 的章节结束掉了,之前提到我觉得目前找到的 dashboard ...

VM 执行个体 (二)

GCE 昨天有说到了执行个体建立,那如果有需求将相同服务性质绑在同一个群组GCP上是否可以做得到,没...

实用的 each_cons 方法,Ruby 30 天刷题修行篇第十二话

嗨,我是A Fei,今天真的忙翻,以下是今天的题目: (题目来源: Codewars) The ma...