如何优雅使用 vuex

大纲

本文内容更多的是讲讲使用 vuex 的一些心得想法,所以大概会讲述下面这些点:

Q1:我为什么会想使用 vuex 来管理数据状态交互?

Q2:使用 vuex 框架有哪些缺点或者说副作用?

Q3:我是如何在项目里使用 vuex 的?

初识 vuex

对于 vuex,有人喜欢,有人反感

喜欢的人觉得它可以很好的解决复杂的数据交互场景

反感的人觉得它有各种缺点:繁琐冗余的代码编写、维护性差的字符串形式变量注入、过于依赖 vue 框架导致异步扩展场景差

这其中,有一个很模糊的点,复杂的数据交互场景并没有一个衡量标准,每个人都有自己的见解

再加上不同人有着不同的项目经历,这就造成了经常会出现有趣的现象:你体会不到我为什么非要使用 vuex,他理解不了这种场景何须使用 vuex,我也讲不明白选择 vuex 的缘由

借用官网文档一句话:

您自会知道什么时候需要它

很玄乎,更通俗来讲就是,多踩点坑,多遭遇些痛点,当你的最后一根稻草被压垮时,自然就会去寻找更好的方案解决

我一直都不喜欢 vuex,因为我觉得它的 mapMutations 或者 mapState 注入到 vue 里的变量和方法都是字符串,极大的破坏了代码的可读性和维护性,没办法通过 idea 快速的跳转到变量定义的地方

当然,你也可以定义一些静态变量来替换这些字符串就可以解决跳转问题,但代价就是代码更繁琐了,本来使用 vuex 时就需要写一堆繁琐的代码,这下更麻烦

还有一个不想使用 vuex 的原因是因为我的项目业务逻辑挺复杂,除了 vue 单文件外,项目里还划分了来负责业务逻辑或异步任务的 js 层代码,而 vuex 是为 vue 框架而设计的,存放在 vuex 数据中心的变量可以通过它的一些工具方法来注入到 vue 组件的 computed 计算属性里方便直接使用,比如

import { mapState } from 'vuex'

export default {
  // 映射 this.count 为 store.state.count
  computed: mapState({
    count: state => state.count
  })
}

但如果想在 js 文件里使用 vuex 里的数据,就会比较繁琐:

import store from 'store'

console.log(store.state.count);

基于以上种种原因,我迟迟未在项目里使用 vuex

那么,我最后为什么又选择使用了 vuex 呢?

一,项目的一些数据交互场景使用 vue 原生的输入输出方案已经忍不下去了

二,我想办法解决了我没法忍受的 vuex 的几个缺点了

三,这是个新项目,并没有复杂的业务场景,项目基本由 vue 单文件来书写即可

简单来说,就是有个新项目符合适用 vuex 的场景,而且一些组件交互场景使用原生方案过于繁琐,vuex 刚好能够解决这个问题,虽然 vuex 有一定的使用成本,但这些缺点恰好又被我想了一些法子解决或简化掉

这样一来,引入 vuex 既能解决我的诉求,又不会引入太多我无法接受的缺点,那自然可以来玩一玩

背景

vue 框架是基于组件机制来组装成页面,所以页面数据是分散到各个组件间,而组件间的数据交互使用的是 vue 自带的输入(props)输出($emit)机制

这种数据交互方案有个特点,数据对象都存储在组件内部,交互需要通过输入和输出

而输入输出机制有个缺点:页面复杂时,经常需要层层传递数据,因为非父子组件间的交互,只能寻找最近的祖先组件来做中转

同时,输入输出机制还有一个局限性:当不同页面的组件需要进行数据交互时,它就无能为力了

平常开发,这种输入输出的方案也基本满足了

但如果有需要跨页面的数据交互或者说,有需要将数据做持久化处理的场景时;以及如果页面组件层次复杂,存在props 和 $emit 层层传递时,那这时候如果忍不了输入输出方案的用法,那么就可以研究新方案了

解决这种场景的方案很多,但从本质上来讲,都可以统归为:数据中心方案

这种方案思路就是将数据对象从组件内部移出到外部存储维护,需要使用哪个数据变量的组件自行去数据中心取

vue 其实也有机制可以达到这种效果,如:依赖注入,但慎用,太破坏数据流的走向了

我们也可以自己用 js 来实现一个数据中心,专门建个 js 文件来存储维护数据模型,需要数据变量的组件引入 js 文件来读取

但每个数据中心都必须解决两个问题:数据复用和数据污染,通俗来讲就是数据初始化和重置,也就是数据的生命周期

数据复用是为了确保不同组件间从数据中心里读取时,是同一份数据副本,这才能达到数据交互目的

而数据污染是指不同模块间使用同个数据中心时,数据模型是否可以达到相互独立,互不影响的效果,这通常是某个功能在不同模块间被复用时会出现的场景;如果这种场景不好理解,那么也可以想想这种场景:再次加载该页面,组件再次被创建后,从数据中心里读取的数据副本是否是相互独立的

如果数据存储在 vue 组件内部,那数据的生命周期就是跟随着组件的创建和销毁,这也是为什么 data 是一个返回对象的函数,因为这样可以借助 js 的函数作用域机制来解决数据的复用和污染问题

但数据从 vue 组件内部移出,存储到数据中心,那么这些处理就需要自己来实现

所以,数据中心并不是简单建个 js 类,里面声明下数据对象就完事了的

基于此,我选择使用 vuex

vuex 副作用

先看个使用 vuex 的简单例子:

// 声明
const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      // 变更状态
      state.count++
    }
  }
})

// vue 里使用
import { mapMutations } from 'vuex'
import { mapState } from 'vuex'

export default {
  // ...
  computed: {
     ...mapState({
         // 将 `this.count` 映射为 `this.$store.state.count`
         count: state => state.count
     })   
  },
  methods: {
    ...mapMutations([
      'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
    ])
  }
}

仅仅简单定义个数据对象,就需要声明对象模型 state,声明对象操作方法 mutation,然后在相应的 vue 组件内先通过 mapState 注入变量的读方法,再通过 mapMutations 注入变量的写方法

而以上这么多繁琐的代码,在原本的 vue 机制里,就是简单的在 data 里声明下变量就完事,这一对比,vuex 的使用上,复杂度和繁琐度很大,有一定的使用成本

所以很多人不喜欢用它,官方也说简单的页面也没有必要去使用它

这是我觉得 vuex 的第一个缺点,或者说副作用:繁琐冗余的代码编写

第二个我觉得 vuex 的缺点就是,mapState 或 mapMutation 注入的变量,都是字符串的

字符串就意味着,你在 vue 单文件内其他地方通过 this.xxx 使用这些变量时,当你想查看变量的声明时,idea 无法识别!

这是我特别无法接受的一点,降低我的维护、开发效率

不过这点因人而异,有人觉得它不是个问题,或者使用个静态变量来替换字符串也可以解决,但这些我个人是没办法接受

然而 vue 原生输入输出的数据交互又不足够支撑我的一些需求场景,自己用 js 实现个数据中心吧,又担心没强制规范,没处理好,后期跑偏掉更难维护,那就想想办法搞定 vuex 的这两个缺点吧

如何更简易的使用 vuex

先说下,我虽然用了些方法,让我使用 vuex 可以达到我的预期,既满足我的需求场景,又不至于引入太多副作用

但实际上,这种方式也许就偏离了 vuex 官方的推荐方式了,别人不一定能接受我的这种用法

所以,这篇更多的是分享我的一些思路和想法,有一说一,并不通用,欢迎拍砖

就我个人对于 vuex 的缺点,我所不能接受的就两点:

  • 繁琐冗余的代码编写
  • 维护性差、可读性差的字符串变量注入

那么,就是得想办法解决这两个问题,先来说第一个

封装自动生成代码解决 vuex 使用繁琐问题

用 vuex 需要编写很多繁琐的代码,这些代码是少不了的,既然少不了,那换个思路,不用我来编写不就好了

想办法提取共性,封装个工具方法,让它来生成每次使用 vuex 的那些繁琐代码,这样一来,使用就方便了

state 里声明的数据对象模型,这些代码是没办法自动生成的,毕竟数据模型都不大一样

而修改数据变量的 mutation 代码就可以想办法来自动生成了

/**
 * 根据 state 对象属性自动生成 mutations 更新属性的方法,如:
 * state: {
 *  projectId: '',
 *  searchParams: {
 *      batchId: ''
 *  }
 * }
 *
 * ===>
 *
 * {
 *  updateProjectId: (state, payload) => { state.projectId = payLoad }
 *  updateSearchParams: (state, payload) => { state.searchParams = {...state.searchParams, ...payload} }
 *  updateBatchId: (state, payload) => { state.searchParams.batchId = payload }
 * }
 *
 * 非对像类型的属性直接生成赋值操作,对象类型属性会通过扩展运算符重新生成对象
 * 且会递归处理内部对象的属性,扁平化的生成 updateXXX 方法挂载到 mutations 对象上
 * @param {Object} stateTemplate
 */
export function generateMutationsByState(stateTemplate) {
  let handleInnerObjState = (parentKeyPath, innerState, obj) => {
    Object.keys(innerState).forEach(key => {
      let value = innerState[key];
      let updateKey = `update${key[0].toUpperCase()}${key.substr(1)}`;
      if (typeof value === 'object' && value != null && !Array.isArray(value)) {
        obj[updateKey] = (state, payload) => {
          let target = state;
          for (let i = 0; i < parentKeyPath.length; i++) {
            target = target[parentKeyPath[i]];
          }
          target[key] = { ...target[key], ...payload };
        };
        handleInnerObjState([...parentKeyPath, key], value, obj);
      } else {
        obj[updateKey] = (state, payload) => {
          let target = state;
          for (let i = 0; i < parentKeyPath.length; i++) {
            target = target[parentKeyPath[i]];
          }
          target[key] = payload;
        };
      }
    });
  };
  let mutations = {};
  Object.keys(stateTemplate).forEach(key => {
    let obj = {};
    let value = stateTemplate[key];
    let updateKey = `update${key[0].toUpperCase()}${key.substr(1)}`;
    if (typeof value === 'object' && value != null && !Array.isArray(value)) {
      obj[updateKey] = (state, payload) => {
        state[key] = { ...state[key], ...payload };
      };
      handleInnerObjState([key], value, obj);
    } else {
      obj[updateKey] = (state, payload) => {
        state[key] = payload;
      };
    }
    Object.assign(mutations, obj);
  });
  return mutations;
}

然后是 mapState 和 mapMutation 注入到 vue 组件的这些代码也可以通过 computed 计算属性的特性来自动生成,这样使用上更加方便,毕竟使用 computed 计算属性的方式就跟使用 data 里声明的变量一样,没有什么区别

import store from './index';
/**
 * 将 store 里指定的 state 转成计算属性 computed 的 set() get()
 * vue 里就可以直接类似操作 data 属性一样使用 state
 *
 * @param {String} moduleName state 所属的 store 的 module 名
 * @param {Array} states 待处理的 states e.g: ['project', 'searchParams.projectName'] 其中,
 * 挂载在 computed 上的属性名,默认等于 state,当 state 结构多层时,取最后一层的属性名
 *
 * ps: state 对应的 mutation 必须以 updateXXX 方式命名
 */
export function storeToComputed(moduleName, states) {
  if (!store) {
    throw new TypeError('store is null');
  }
  if (!moduleName) {
    throw new TypeError("state's module name is null");
  }
  if (!states || !Array.isArray(states) || states.length === 0) {
    throw new TypeError('states is null or not array');
  }
  let computed = {};
  states.forEach(state => {
    if (state.indexOf('.') !== -1) {
      let _states = state.split('.');
      let _key = _states[_states.length - 1];
      computed[_key] = {
        get() {
          let res = store.state[moduleName];
          for (let i = 0; i < _states.length; i++) {
            res = res[_states[i]];
          }
          return res;
        },
        set(value) {
          store.commit(
            `${moduleName}/update${_key[0].toUpperCase()}${_key.substr(1)}`,
            value
          );
        },
      };
    } else {
      computed[state] = {
        get() {
          return store.state[moduleName][state];
        },
        set(value) {
          store.commit(
            `${moduleName}/update${state[0].toUpperCase()}${state.substr(1)}`,
            value
          );
        },
      };
    }
  });

  return computed;
}

那么最终可以达到的效果就是:

  • 只需在 store 文件里声明 state 数据变量
  • 然后再需要注入的 vue 组件里注入即可
// 声明
import { generateMutationsByState } from './helper';

const global = {
    state: {
        count: 0
    }
}
global.mutations = generateMutationsByState(global.state);

const store = new Vuex.Store({
    modules: {
        global
    }
})
// vue里使用
import { storeToComputed } from '@/store/storeToComputed';

export default {
  // ...
  computed: {
      // 将 this.$store.state.global.count 映射成 this.count
     ...storeToComputed('global', ['count'])
  },
}

我的这种用法,其实就只是单纯将 vuex 拿来作为数据中心使用而已,在 store 文件里不编写逻辑代码,也不使用 action

这种用法的好处,我是觉得,会跟原本在 vue 的 data 里声明变量后的用法比较类似。因为就是将原本定义在 data 里的变量换成定义在专门的 store 文件里,然后再多一步将变量通过工具方法注入到 vue 的 computed 里,接下去的使用变量的任何场景,在哪赋值,在哪取值,哪里处理异步请求等等的代码,原本怎么写,现在还是怎么写,完全不影响

这就意味着,这种方案后续如果有缺陷,或者用不习惯,那么想切换到 vue 原生的输入输出方案非常方便,影响点、改动点都会比较少,就是将 storeToComputed 注入到 computed 的变量换到 data 就完事了

甚至说,后续想换掉 vuex 也会比较方便,毕竟只是单纯用它当做数据中心而已

然后再配合上 vuex 的动态挂载和卸载的用法,这个数据中心就可以像 angular 框架那样做到精确控制数据对象的作用域和生命周期,全局共享、模块间共享、页面内共享、组件内共享等都可以很方便做到,这样一来,数据交互就不怕复杂场景了

这是我之所以会这么使用 vuex 的考虑

自定义 vscode 插件解决字符串变量的跳转问题

繁琐的代码编写问题搞定了,接下去就是看看怎么解决字符串变量注入的跳转问题了

先来说说,我为什么会在意变量支不支持利用 idea 直接跳转到声明的地方

这是因为,有些页面比较复杂,数据变量比较多,或者时间久了,很容易忘记一些变量的命名、含义

而我们通常都只会在声明的地方加上一些注释

所以利用 idea 直接快速跳转到声明的地方,第一,有注释可以快速帮助回忆、理清变量含义;第二,忘记变量命名全称可以快速复制使用;第三,方便我查看其它数据变量

那么,怎么解决这个问题呢?

自然就是自己扩展开发个 vscode 插件来支持了,面向百度的话,vscode 插件开发并不困难,看几篇教程,清楚插件的生命周期和一些 API 就可以上手了

关键是,如何识别 vuex 注入的这些变量?如何跳转到 store 文件里声明数据变量的 state 位置?

如果想做成通用的插件,那可能需要多花点精力

但如果只是基于自己当前的项目来解决这个问题,那就简单多了,因为项目有一定的规范,比如 vuex 的 store 文件存放的目录地址,比如注入到 vue 组件里的使用方式,这些都是有规范和规律的

简单说下我的思路:

  1. 先扫描项目 store 目录下文件,识别出有数据模型 (state) 的文件,解析并存储数据模型各个变量名和位置
  2. 注册 vscode 的变量操作响应,当按住 ctrl 并将鼠标移到变量上时,响应我们的插件程序
  3. 判断当前聚焦操作的变量是否是通过在 computed 里注入的变量,是则继续下一步寻找变量声明的文件位置
  4. 通过变量名和模块名到 store 里匹配变量,匹配到后,记录变量的声明信息和文件位置,当点击左键时,响应跳转行为

github 地址:vuex-peek

总结

最后简单总结下,项目里并不是必须要使用 vuex,vuex 所解决的场景,用 vue 原生的输入输出机制想想办法也能解决,区别可能就是代码的可读性、维护性上的区别,数据流走向是否清晰等

vuex 作为三方库,自然就是一个可选的服务,用不用,怎么用,都因人而异;考虑好自己的诉求,对比好引入前后的影响点,权衡好自己能接受的点就好

比如我,使用 vuex 的方式上说得难听点,也有点不伦不类,毕竟并没有按照官方示例来使用,反而是自己搞了套使用规范,这也增加了别人的上手成本

所以写这篇,不在于强推使用 vuex,只是从自己的一些经历分享自己使用一些三方库的心路历程,所思所想

很多时候,当你开始吐槽某某方案、当你开始无法接受某某用法时,这其实意味着,这是一次绝佳的探索机会

吐槽完就想办法去优化、去寻找新方案;接受不了时,就想办法去研究看能否解决这些痛点

人嘛,总是在一次次的踩进坑里,再爬出来

热门相关:至尊龙帝陆鸣   至尊龙帝陆鸣   至尊龙帝陆鸣   这个大佬有点苟   星界游民