在快应用开发中使用 vuex module

上一篇文章「在快应用开发中使用 Vuex」介绍了 Vuex 五个部分中的前四个部分:State、Getter、Mutation、Action 以及一些辅助函数,那么这篇文章就来介绍一下 Vuex 中的 module。Vuex 使用的是单一状态树,应用的所有状态会集中到一个对象中。如果项目比较大,那么相应的状态数据肯定就会更多,这样的话,store 对象就会变得相当的臃肿,非常难管理。所以这种情况下,我们就需要使用 vuex module。

单一状态树

Vuex 使用单一状态树来管理状态。所谓单一状态树,就是用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 (SSOT)”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。当然,单一状态树和模块化是并不冲突的。

Vuex module

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决这个问题,Vuex 允许我们将 store 分割成大大小小的对象,每个对象也都拥有自己的 state、getter、mutation、action,这个对象我们把它叫做 module(模块),在模块中还可以继续嵌套子模块、子子模块 ……

引入 module

首先,新建一个 modules 文件夹,用于保存各个模块的状态。然后,新建一个 moduleA.js 文件

export default {
  state: {
    text: "moduleA"
  },
  getters: {
    text(state) {
      return "getter" + state.text;
    }
  },
  mutations: {
    setText(state) {
      state.text = "set moduleA";
    }
  },
  actions: {}
};

再新建一个 moduleB.js 文件,内容和 moduleA.js 类似就不再重复了。

然后在 store.js 导入 moduleA 和 moduleB

import Vuex from "quickapp-vuex";
import moduleA from "./modules/moduleA";
import moduleB from "./modules/moduleB";

export default new Vuex.Store({
  modules: {
    moduleA,
    moduleB
  }
});

之后,在 app.ux 文件中导入就可以在页面和组件中使用了。

<template>
  <div class="demo-page">
    <text class="title">{{ text1 }}</text>
    <text class="title">{{ text2 }}</text>
    <div>
      <text class="btn" onclick="setText">更改文本</text>
    </div>
  </div>
</template>

<script>
import { mapMutations, Component, mapState } from "quickapp-vuex";

export default Component({
  computed: {
    ...mapState({
      text1: state => state.moduleA.text,
      text2: state => state.moduleB.text
    })
  },

  ...mapMutations(["setText"])
});
</script>

我们在页面中使用 state.moduleA.text 和 state.moduleB.text 引入了 moduleA 和 moduleB 的 state,并映射为 text1 和 text2。由此可知,模块内部的 state 是局部的,只属于模块本身所有,所以外部必须通过对应的模块名进行访问。

但是需要注意了!模块内部的 action、mutation 和 getter 默认是注册在全局命名空间的,这样使得多个模块能够对同一 mutation 或 action 作出响应。比如,点击上面页面中的「更改文本」按钮,由于 moduleA 和 moduleB 都有一个名为 setText 的 mutation ,你会发现 name1 和 name2 的值都发生了改变,这可能并不是我们想要的结果。

命名空间

如上所述,模块内部的 action、mutation 和 getter 默认是注册在全局命名空间的。如果我们只想让他们在当前的模块中生效,通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。

我们在 moduleA.js 中添加 namespaced: true

export default {
    namespaced: true,
    // ...
}

这样,在 moduleA 中定义的 action、mutation 和 getter 就只属于 moduleA 了。为了能在页面中获取到 moduleA 下的 mutation,我们需要修改一下代码

export default Component({
  //...
  ...mapMutations({
    call: "moduleA/setText"
  })
});

上面,我们把 moduleA 模块下的 setText 映射为 call,现在,我们在点击「更改文本」按钮,就会发现只有 moduleA 的 state 发生了改变。

有时候,我们需要使用一个模块下的多个 mutation,如果每一次都要加上一长串模块名未免有些繁琐,所以 Vuex 提供了一种更简单的写法

export default Component({
  computed: {
    ...mapState("some/nested/module", {
      a: state => state.a,
      b: state => state.b
    })
  },
  ...mapMutations("some/nested/module", ["foo", "bar"])
});

将模块的空间名称字符串作为第一个参数传递给辅助函数,这样所有绑定都会自动将该模块作为上下文。

如果觉得需要对每个辅助函数都绑定一次命名空间也有些繁琐,那么我们还可以通过使用 createNamespacedHelpers 创建基于某个命名空间的辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的辅助函数

import Vuex from "quickapp-vuex";
const { mapGetters, mapMutations } = Vuex.createNamespacedHelpers(
  "some/nested/module"
);
export default Vuex.Component({
  computed: {
    // 在 `some/nested/module` 中查找
    ...mapState({
      a: state => state.a,
      b: state => state.b
    })
  },
  methods: {
    // 在 `some/nested/module` 中查找
    ...mapActions(["foo", "bar"])
  }
});

访问根节点数据

我们已经知晓,模块内部的 state 是局部的,只属于模块本身所有。那么如果我们要想在模块中访问 store 根节点的数据 state,该怎么办呢?很简单,我们可以在模块内部的 getter 和 action 中,通过 rootState 这个参数来获取。

export default {
    // ...
    getters: {
        // 注意:rootState必须是第三个参数
        text(state, getters, rootState) {
            return state.text + '-' + rootState.text;
        }
    },
    actions: {
        callAction({state, rootState}) {
            console.log(state.text, rootState.text);
        }
    }
}

这里需要注意的是在 getters 中,rootState 是以第三个参数暴露出来的,另外,还有第四个参数 rootGetters,用来获得根节点的 getters 信息。而在 action 中,由于它接收过来的数据都被包在 context 对象中的,通过解构获取 rootState 则没有什么顺序的限制。

除了访问根节点的数据,如果想要在模块内分发全局 action 或提交全局 mutation 的话,那么我们只需要将 { root: true } 作为第三参数传给 dispatch 或 commit 即可。

export default {
  namespaced: true,
  // ...
  actions: {
    callAction({ state, commit, rootState }) {
      commit("setName", "改变", { root: true });
      console.log(state.text, rootState.text);
    }
  }
};

同时,若需要在带命名空间的模块注册全局 action,你可添加 root: true,并将这个 action 的定义放在函数 handler 中

export default {
  namespaced: true,
  // ...
  actions: {
    callAction: {
      root: true,
      handler(namespacedContext, payload) {
        let { state, commit } = namespacedContext;
        commit("setText"); //这个mutation是模块内的mutation
        console.log(state.text);
      }
    }
  }
};

这里的 namespacedContext 就相当于当前模块的上下文对象,payload 是调用的时候所传入的参数,也就是载荷。然后,你可以在全局命名空间内引入callAction,当你 dispatch 这个 Action 后,模块命名空间内的数据将被修改。也就是说,在带命名空间的模块注册全局 action 提供了一种在全局命名空间内访问模块命名空间的方式。

参考文档