Vuex业务模块划分项目实例

在一个多人协作的vue项目中使用vuex,如果能合理的封装module,将业务模块化,将提高开发效率、降低维护成本。这其中就包括封装module、目录层次划分等。
(PS:本文代码在vue-cli3构建的基础vue项目之上进行编写:vue create my-project

一、回顾Vuex基础

  首先回顾下Vuex的基础知识。

1. 应用场景

  假设有这样的两个页面,数据一样,就是页面布局不一样。同一份数据,我们在两个页面里面操作两次感觉还行,如果有10个,20个甚至更多的此类页面,那么这就是一个让人奔溃的事情,而且数据的同步刷新也很痛苦。所以就需要Vuex了。
  Vuex是一个专为Vue.js应用程序开发的状态管理模式

  当然了,虽然Vuex可以帮助我们管理共享状态,但也附带了更多的概念和框架。比如你没有或者只有很少的数据需要在组件间共享,那么使用Vuex是繁琐冗余的,你也许并不需要Vuex,完全可以采用 cookie,sessionStorage,localstorage 或者 EventBus 等多种实现方式。
  如果你是在构建一个中大型单页应用,有多种状态需要管理,您很可能会考虑如何更好地在组件外部管理状态,或者你需要考虑到系统后续的可扩展性,希望早期就使用这种更成熟的解决方案,那么 Vuex将是你很好的选择
  总之根据实际情况,不要为了使用而使用Vuex

2. 使用方式

  首先安装vuex(npm install vuex --save),如果是用vue-cli3搭建的vue项目,那么在项目的创建过程中就可以选择预安装了。
  安装完后在 src目录 下就有了一个文件 store.js
  官网demo中示范了store.js中的一般性内容,我们现在写一个如下所示:
store.js

import Vue from "vue";
// 1. 引入Vuex对象
import Vuex from "vuex";

// 2. 安装插件
Vue.use(Vuex);

// 3.创建store对象并导出
let store = new Vuex.Store({
  // 配置 module:{state,getter,mutation,action}
  state: {
    num: 1
  },
  // 获取器距离state很近,可以直接拿到state
  getters: {
    getNum(state) {
      return state.num;
    }
  },
  // 更改也和state很近,
  // 最多可以接收两个参数,第一个参数就是state,第二个参数是传入的数据data
  mutations: {
    addNumByOne(state) {
      state.num++;
    },
    addNumByNum(state, num) {
      state.num += num;
    }
  },
  // mutations对state的操作只能是同步的,否则会丢失记录
  // 异步处理需要调用action
  // 最多可以接收两个参数,第一个参数是store(整个的store对象),第二个参数是传入的数据
  actions: {
    addNumByAction({ commit }, num) {
      // 异步操作
      setTimeout(function() {
        // 调用mutations里的方法来修改state
        commit("addNumByNum", num);
      }, 1000);
    }
  }
});
export default store;

3. 注意事项

  vuex有五大核心:State,Getters,Mutations,Actions,Modules。在使用时,前四个如果使用不当,很容易引发问题,例如在mutation执行异步行为,会导致本次改动数据的记录丢失(通过Chrome插件Vue Devtools可以看到vuex数据快照状态)。
  因此,最防止出现bug的方式就是:

  ① dispatch去调用actionaction去调用mutationmutation去更改state,从而改变视图。

  ② 同时还建议通过getters去获取数据,以上面的代码为例,不推荐通过$store.state.num方式去直接访问store对象。
  getters一般不直接通过$store.getters.getNum方式来使用,而是结合组件的computed使用,监视里面属性的更改,从而得到通知。

  下面我们来看一个完整的获取/更改store数值的例子,演示推荐的用法并论证上面的结论。
Home.vue

<template>
  <div class="home">
    获取state数据:
    {{ $store.state.num }}      <!-- 可以这么用,但不推荐 -->
    {{ $store.getters.getNum }} <!-- 可以这么用,但不推荐 -->
    {{ getNum }}                <!-- 结合computed使用,推荐 -->

    更改state数据:
    <!--
    通过下面三个按钮,分别对应三个方法,论证为什么推荐用:
    dispatch去调用action → action去调用mutation → mutation去更改state,从而改变视图。
    -->
    <button @click="addByOne">自增1</button>              <!-- 不传递参数 -->
    <button @click="addByNum">增加指定数</button>         <!-- 传递参数 -->
    <button @click="addByAction">异步增加指定数</button>  <!-- 异步操作 -->
  </div>
</template>

<script>
import { mapGetters } from "vuex";

export default {
  name: "home",
  components: {},
  // getters结合computed使用
  computed: {
    ...mapGetters([
      "getNum"
    ])
  },
  methods: {
    addByOne() {
      // 不建议直接这么做,会有异步的问题
      this.$store.commit("addNumByOne");
    },
    addByNum() {
      // 不建议直接这么做,会有异步的问题
      this.$store.commit("addNumByNum", 10);
    },
    addByAction() {
      // 推荐:dispatch去调用action → action去调用mutation → mutation去更改state,从而改变视图。
      this.$store.dispatch("addNumByAction", 20);
    }
  }
};
</script>

二、单一模块项目 - 分割文件

1. 分割标准

  只有一个module(或不分module)时,如果你的 store.js 文件太大,可以将 state、getter、action、和 mutation 分割到单独的文件,放置于 store目录 下,然后在该目录下创建 index.js 来引入这几个文件并导出。
  在main.js中引入store文件夹后,就会默认寻找index文件,接下来的内部逻辑和前面不分割的时候都一样。

  看下此时的src目录下项目结构示例:

├── main.js
├── App.vue
├── components
│   ├── HelloWorld.vue
│   └── ...
├── views
│   ├── Home.vue
│   └── ...
└── store
    ├── index.js          # 引入四个文件并导出store的地方
    ├── state.js          # 存放所有变量
    ├── getters.js        # 获取变量的方法
    ├── mutations.js      # 存放同步读取/修改state的方法
    └── actions.js        # 存放异步读取/修改state的的方法

2. 代码演示

  演示此时 store目录 中的各文件代码内容:
index.js

import Vue from "vue";
// 1. 引入Vuex对象
import Vuex from "vuex";
// 2. 引入分割出去的四个部分
import state from "./state";
import getters from "./getters";
import mutations from "./mutations";
import actions from "./actions";

// 3. 安装插件
Vue.use(Vuex);

// 4.创建store对象并导出
let store = new Vuex.Store({
  namespaced: true,
  state,
  getters,
  mutations,
  actions
});
export default store;

state.js

const state = {
  num: 1
};

export default state;

getters.js

const getters = {
  getNum(state) {
    return state.num;
  },
  getDoubleNum(state) {
    return state.num * 2;
  }
};

export default getters;

mutations.js

const mutations = {
  addNumByOne(state) {
    state.num++;
  },
  addNumByNum(state, num) {
    state.num += num;
  }
};

export default mutations;

actions.js

const actions = {
  addNumByAction({ commit }, num) {
    // 异步操作
    setTimeout(function() {
      // 调用mutations里的方法来修改state
      commit("addNumByNum", num);
    }, 1000);
  }
};

export default actions;

3. 引用方法

  在 main.js 中引用方法这一步不用修改,和默认的一样就行。
  只需两行:

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store"; // 引入store文件

Vue.config.productionTip = false;

new Vue({
  router,
  store, // 将store挂载到Vue实例
  render: h => h(App)
}).$mount("#app");

  前面也说到过,在main.js中引入store文件夹后,就会默认寻找index文件,接下来的内部逻辑和前面不分割的时候都一样。

三、多个模块项目 - 分割模块

1. 分割标准

  对于中大型应用,我们会希望把 Vuex 相关代码分割到模块中。
  官网demo中示范了这种分割方式,我们仿照它写一个如下所示:
  看下此时的src目录下项目结构示例:

├── main.js
├── App.vue
├── components
│   ├── HelloWorld.vue
│   └── ...
├── views
│   ├── Home.vue
│   └── ...
└── store
    ├── index.js          # 组装模块并导出store的地方
    └── modules           # 拆分出来的各个模块目录
        └── num.js        # num模块对象,包含state,getters,mutations,actions
        └── ...           # 其它模块对象

2. 代码演示

  演示此时 store目录 中的各文件代码内容:
index.js

import Vue from "vue";
// 1. 引入Vuex对象
import Vuex from "vuex";
// 2. 引入模块
import num from "./modules/num";

// 3. 安装插件
Vue.use(Vuex);

// 4.创建store对象并导出
let store = new Vuex.Store({
  // 出现同名函数或变量的时候,为了保护其不被覆盖,
  // 在官方文档中还有个命名空间的概念
  modules: {
    num: num
  }
});
export default store;

num.js

const state = {
  num: 1
};

const getters = {
  getNum(state) {
    return state.num;
  },
  getDoubleNum(state) {
    return state.num * 2;
  }
};

const mutations = {
  addNumByOne(state) {
    state.num++;
  },
  addNumByNum(state, num) {
    state.num += num;
  }
};

const actions = {
  addNumByAction({ commit }, num) {
    setTimeout(function() {
      commit("addNumByNum", num);
    }, 1000);
  }
};

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions
};

3. 引用方法

  (同上,重复内容。为了阅读方便直接复制过来了)
  在 main.js 中引用方法这一步不用修改,和默认的一样就行。
  只需两行:

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store"; // 引入store文件

Vue.config.productionTip = false;

new Vue({
  router,
  store, // 将store挂载到Vue实例
  render: h => h(App)
}).$mount("#app");

  前面也说到过,在main.js中引入store文件夹后,就会默认寻找index文件,接下来的内部逻辑和前面不分割的时候都一样。

四、中大型项目 - 分割模块和文件

1. 分割标准

  在一个中大型项目中,我们需要做进一步的分割(同时分割模块和文件)。
  参考我们公司的一个项目,我大致总结了下并稍加演变,下面是项目结构示例:

├── main.js
├── App.vue
├── components
│   ├── HelloWorld.vue
│   └── ...
├── views
│   ├── Home.vue
│   └── ...
└── store
    ├── index.js                    # 组装模块并导出store的地方
    └── modules                     # 拆分出来的各个模块目录
        └── module-a                # 模块a,里面对每一个对象进一步拆分
            ├── index.js            # 引入模块a对象拆分出来的文件,并导出store
            ├── state.js            # 存放所有变量
            ├── getters.js          # 获取变量的方法
            ├── mutation-types.js   # 对mutations的统一管理
            ├── mutations.js        # 存放同步读取/修改state的方法
            └── actions.js          # 存放异步读取/修改state的方法
        ├── module-b                # 模块b
        └── ...                     # 其它模块

  细心的你可能发现,我们这里多了一个 mutation-types.js 文件,我看过一些项目,有的人也喜欢命名成 types.js。它的作用是什么呢?
  mutation-types.js 是对mutations的统一管理,将所有函数用常量保存,它里面写的内容一般如下:

export const ADD_NUM_BY_ONE = "addNumByOne";
export const ADD_NUM_BY_NUM = "addNumByNum";

  这样的好处是:
  ● 多人开发时,对mutations统一管理
    将方法(函数)名统一保存后,找函数很直观,维护起来比较方便。
  ● 用常量来引用,可维护性更高
    当你想修改一个方法名时,只需在这个文件里修改一次变量的值,而在项目的其它地方引用的都是该变量,不需要逐个去查找和修改。
  ● 可以根据模块分类来给mutation type命名,名字多长都可以,常量名简短就好了
    比如:export const RESET_USER_INFO = "user/reset_user_info";

  注意:
  • 使用常量替代 Mutation 事件类型在Vuex的文档中有提及
  • 在[]中放入表达式,计算结果可以当做属性名。这种写法是出自 ES6风格的计算属性命名,待会在 mutations.js 文件中我们会用到。
  • 如果个人小项目,就没必要特地拆分出 mutation-types.js 了,转来转去麻烦。

2. 代码演示

  演示此时 store目录 中的各文件代码内容:
index.js

import Vue from "vue";
// 1. 引入Vuex对象
import Vuex from "vuex";
// 2. 引入模块
import moduleA from "./modules/module-a";
import moduleB from "./modules/module-b";

// 3. 安装插件
Vue.use(Vuex);

// 4.创建store对象并导出
let store = new Vuex.Store({
  // 出现同名函数或变量的时候,为了保护其不被覆盖,
  // 在官方文档中还有个命名空间的概念
  modules: {
    moduleA: moduleA,
    moduleB: moduleB
  }
});
export default store;

module-a/index.js

import state from "./state";
import getters from "./getters";
import mutations from "./mutations";
import actions from "./actions";

export default { 
  namespaced: true,
  state,
  getters,
  mutations,
  actions
};

module-a/state.js

const state = {
  num: 1
};

export default state;

module-a/getters.js

const getters = {
  getNum(state) {
    return state.num;
  },
  getDoubleNum(state) {
    return state.num * 2;
  }
};

export default getters;

module-a/mutation-types.js

export const ADD_NUM_BY_ONE = "addNumByOne";
export const ADD_NUM_BY_NUM = "addNumByNum";

module-a/mutations.js

import * as types from "./mutation-types";

const mutations = {
  [types.ADD_NUM_BY_ONE](state) {
    state.num++;
  },
  [types.ADD_NUM_BY_NUM](state, num) {
    state.num += num;
  }
};

export default mutations;

module-a/actions.js

import * as types from "./mutation-types";

const actions = {
  addNumByAction({ commit }, num) {
    // 异步操作
    setTimeout(function() {
      // 调用mutations里的方法来修改state
      commit(types.ADD_NUM_BY_NUM, num);
    }, 1000);
  }
};

export default actions;

3. 引用方法

  (同上,重复内容。为了阅读方便直接复制过来了)
  在 main.js 中引用方法这一步不用修改,和默认的一样就行。
  只需两行:

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store"; // 引入store文件

Vue.config.productionTip = false;

new Vue({
  router,
  store, // 将store挂载到Vue实例
  render: h => h(App)
}).$mount("#app");

  前面也说到过,在main.js中引入store文件夹后,就会默认寻找index文件,接下来的内部逻辑和前面不分割的时候都一样。

4. 模块中使用

<template>
  <div class="home">
    获取state数据:
    获取state值:{{ myNum }}                     <!-- 获取state值 -->
    &#8195;
    通过getters获取值:{{ getNum }}              <!-- 通过getters获取值 -->
    &#8195;
    通过getters获取*2后的值:{{ getDoubleNum }}   <!-- 通过getters获取处理过后的值 -->
    <br/><br/>
    更改state数据:
    <button @click="addNumByOne">自增1</button>                    <!-- 不传递参数 -->
    <button @click="addNumByNum(10)">增加指定数</button>           <!-- 传递参数 -->
    <button @click="addNumByAction(20)">异步增加指定数</button>     <!-- 异步操作 -->
  </div>
</template>

<script>
import { mapState, mapGetters, mapMutations, mapActions } from "vuex";

export default {
  name: "home",
  components: {},
  computed: {
    // 模块名(嵌套层级要用模块的别名写清楚)例如:moduleA
    // 获取state值
    ...mapState("moduleA", {
      myNum: state => state.num
    }),
    // 通过getters获取值
    ...mapGetters("moduleA", [
      "getNum",
      "getDoubleNum"
    ])
  },
  methods: {
    ...mapMutations("moduleA", [
      "addNumByOne", // 将 `this.addNumByOne()` 映射为 `this.$store.commit("addNumByOne")`
      "addNumByNum" // 使用了扩展函数之后,直接在调用的地方传入参数,会自动传递的。;例如`@click(addNumByNum(10))`
    ]),
    ...mapActions("moduleA", [
      "addNumByAction" // 将 `this.addByAction()` 映射为 `this.$store.dispatch("addNumByAction", 20)`,同上,调用的地方传入参数
    ])
  }
};
</script>

五、总结

  以上就是在实际项目中对Vuex的设计与封装,根据我在学习和实际项目中的经验,总结了三类。但这仅是提供思路和简单的实现方法,落实到具体的开发人员和具体的项目,需要具体分析,是否要用Vuex,采用怎样的划分形式,那就是大家为自己项目定制的东西了。

  好了,以上就是整篇博客的所有内容,希望对大家有所帮助。如果文中有错误的地方还请大家及时指出。


参考
https://vuex.vuejs.org/zh/guide/structure.html


  目录