在快应用开发中使用 vuex module

May 26, 2020

上一篇文章「在快应用开发中使用 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 提供了一种在全局命名空间内访问模块命名空间的方式。

参考文档

vivo developer

快应用引擎、工具开发者、快应用生态拓展达人(vivo)。

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.