如何构建一个不到100行的小程序端mini版本redux

在刚刚加入这家公司的时候,技术Leader就和我说过一件事情,但愿可以落地前端的自动化,但愿我可以出一个可行方案。而此时,在公司前端团队还很是年轻,可是业务的发展致使团队规模扩大了一倍多。加上toC业务变幻无穷,致使各类bug满天飞。前端自动化测试的成本是很是高昂的,在长期加班感需求的状况下还要去顾及自动化测试的脚本开发几乎是很难实施的。如何去寻找一个低成本可行的自动化测试技术方案就是我作这个miniredux的初衷。之因此叫mini版。一方面是本身能力的不足还没法实现一套完整的小程序版本redux的技术实现,另外一方面以渐进迭代的原则,现知足基本须要前提下,根据后续使用状况,逐步优化。也有可能不久后dan大神本身出了一个也说不定。前端

实例以及源码

实例和源码vue

1. 互联网 toC 应用研发之痛

缺人,缺人,咱们缺乏高质量前端,这多是绝大多数技术管理者的诉求。面对系统中漫天的bug,蟑螂同样杀不尽的低级错误,是否老是那么的无能为力。虽然咱们有不少测试工具以及自动化测试的库,可是咱们依然会困惑于为何作前端自动化测试实施起来这么难?react

在十一期间,我开发了一个mini版本的Redux用于解决这个问题,而且在这周团队内部讨论后方案是可行的,也开始准备运用到项目中去验证。所以也将思路分享给你们git

2. 前端应用自动化实施目前的困惑:

  • 前端的Javacript语言是一种若类型脚本语言,并且不少错误是运行时才会被发现。所以这种特性也就致使了前端代码质量难以保证。所以有的团队会毅然决然的选择了TSTS确实极大的改善了这一情况。
  • 因为现代前端对于代码分层掌握的不是很好,尤为以Vue项目最为严重。导出都是耦合在一块儿的UI交互逻辑和业务逻辑。也致使了前端目前的一个困境,耦合严重。耦合就意味着每一个地方的影响范围都特别大。常常会致使,明明一样的业务,这里改了,另外一个地方不应改的地方也受影响了。(这一点咱们不得不认可Angular为何会被称为企业级前端框架,它自身已经提供了本身的模块划分标准和规则,React也有本身的Flux架构方法去指导你们,而Vue在这块的缺失也使它在日渐复杂的系统中产生混乱,而尤大也说了Vuex并不适用于大型应用。也侧面反映了这一点)
  • 绝大多数年轻前端对于业务模型的设计能力不足,也致使目前项目代码中状态的混乱。虽然不少优秀的前端工程师能把mvvmvdom,双向绑定的实现,单项数据流讲的头头是道,甚至是本身均可以当场给你写一套实现。可是在实践中,由于缺少对业务数据建模的理解。常常会发生混乱。这也是不少团队实践react-redux遇到的最大困惑
  • 一旦UI逻辑和业务逻辑耦合,那么咱们只能经过虚拟页面DOM来进行测试,可是对于toC业务基本上每两三个月就大变脸的UI来讲,这种自动化测试方式的开发成本是很是巨大的。可是对业务逻辑的变化实际上是很小的。

所以针对以上几种问题,寻找一个方法将业务与视图层解偶,只对业务层单独进行测试,这样既能够大大下降toC应用自动化测试开发成本,又能够极大的提升项目中业务的准确性。至于视图层,由于基于MVVM的前端应用大多都是数据驱动的系统,所以只要业务数据模型的正确就能够极大的保证系统的健壮性。github

3. 如何解偶视图层与业务逻辑层

其实模块拆分一直都是一个软件开发领域的难题,如何去解偶各个模块,虽然咱们有各类方法论去指导咱们去实施,可是方法论毕竟只是一个理论。真正作起来的时候会受各类因素影响,而并非每一个团队都有具有这种能力的牛人。算法

而在前端领域,MVC过于复杂,对于前端来讲过重,而长期关注与视图的呈现,大部分人是缺少这方面设计能力的。而MVVM在这方面给前端提供了一个方向(Flux全文翻译):编程

Flux原文:redux

Flux eschews MVC in favor of a unidirectional data flow. When a user interacts with a React view, the view propagates an action through a central dispatcher, to the various stores that hold the application's data and business logic, which updates all of the views that are affected. This works especially well with React's declarative programming style, which allows the store to send updates without specifying how to transition views between states.小程序

译文:设计模式

Flux 避开了 MVC,采起了单向数据流,当用户与 React 视图进行交互的时候,视图经过 dispatcher 方法传递一个 action 对象到保存数据和业务逻辑的各个存储对象区 store 中。这些存储区的数据变化会影响全部视图,并致使视图发生更新。这与 React 的编程风格有关,该风格容许经过数据的变化来改变视图,而不须要指定如何经过状态切换视图。

经过Flux的讲解,咱们能够清楚的意识到视图中,在对用户各类行为的响应中,经过派发器(dispatch)将新的业务数据以动做(action)为载体灌入用于处理和存储业务数据模型(store)中。以下图:

而在基于Flux的架构方法论基础上衍生出来的Redux就是其中被人普遍熟知的,而Vuex也随着Vue的火爆而被人普遍认识。

可是ReduxVuex虽然在开发上起着一样的做用,可是在本质上却存在着很大的不一样,这也是为何Vuex不适用于大型前端应用:

  • Redux是框架无关的,Vuex须要依托于Vue的响应式属性
  • Redux是纯原生JS,与视图无关,这也就意味着它能够帮助咱们方便的剥离业务到Redux中。从而能够方便的复用到任何前端技术中去。而Vuex很难作到这一点。
  • Redux强调reduce必须是纯函数,纯函数意味着相同的参数会致使相同的结果,也就是结果是能够预知的,从而具备很是好的可测性,这也就知足了咱们对业务进行自动化测试的需求。而Vuex是依托于修改参数引用(mutations)的方式,而且actions是支持异步致使了返回值的不肯定性。

结合以上缘由,一个小程序版本的Redux才是咱们须要的。

4. 如何构建一个小程序版本的Redux

我相信大部分人都阅读过Redux源码,固然我也写过一篇关于Redux源码的文章,我相信原理你们都懂,可是如何去实现一个小程序版本的Redux的难点是咱们如何实现一个相似于react-redux的东西将Redux结合到小程序里面来。

咱们面临如下几个技术问题:

  • store存在哪?
  • 如何暴露接口?
  • 如何将store中的数据与Page中的数据进行响应式?

其实就是作一个发布订阅模式的实现,可是咱们要保证咱们store内部的数据不能被随意修改,这样才能保证咱们的业务稳定性。

我想,一提到小程序内部数据共享,你们确定会想到globalData。可是globalData是依托于全局app对象,而全局变量的影响你们是心知肚明的,不必定哪一个新手给你搞坏了也是说不定的。因此也就致使了状态变化的不可跟踪。

那么如何避免使用全局变量又能解决数据存储的问题呢?答案是 ---- 沙盒模式

沙盒模式,是JS很是广泛的一个设计模式,它经过闭包的原理将数据维持在一个函数做用于中,而经过返回值内的函数引用这个函数包体内的变量的方式,造成闭包,而只有经过该函数的返回函数才能访问和修改该闭包内的数据,从而起来了数据保护的做用。

function initMpState () { // mp-redux初始化函数,在这里造成一个独立做用域
  const reducers = {};  // 该函数做用域内的数据
  const finalState = {};  // 该函数做用域内的数据 
  const listeners = [];  // 该函数做用域内的数据
  let injectMethod = null;  // 该函数做用域内的数

  function getStore() { // 用于访问沙盒内数据的接口
    return finalState;
  }

  function createStore(modules, injectFunc) { // 用于初始化沙盒内数据的接口
    ...
  }

  function dispatch(action) { // 用于操做沙盒内数据的接口
    ...
  }

  function connect(mapStoreToState, component) { // 用于关联小程序Page对象的高级API
    ...
  }

  return {
    createStore,
    dispatch,
    connect,
    getStore
  }
}

module.exports = initMpState();
复制代码

经过沙盒模式,咱们很好的保护了咱们的数据,而且提供了有限的操做手段,安全又可靠的保存了咱们的业务数据模型中的数据。

5. 如何在小程序初始化咱们的store

既然须要暴露接口,又要保持这个函数内的闭包。好复杂呀。可是commonjs在这方面起到了很好的帮助:

当咱们require一个模块的时候,commonjs会维持这个模块在一个独立的做用域中。而且一直存在。典型的应用场景就是Nodejs

因此经过commonjs,咱们使用module.exports将咱们的mp-redux初始化函数返回的api集合暴露给调用方:

// mp-redux/index.js
  function initMpState() {
    ...
  }
  module.exports = initMpState();
复制代码

这样咱们就能够在任何地方视无忌惮的搞事情了(使用api操做store数据).

6. 初始化业务模型

store中的数据是根据业务而来,如何保存业务模型将是咱们的重点。而这些业务模型又会带有不少业务逻辑数据处理。同时,咱们还要保证业务的可测性。

所以reduxreduce方式是咱们需求的绝佳选择,所以每个model必须是一个纯函数,它须要每次操做后都要返回一个纯对象,也就是业务数据模型。

/* modules,这里参考redux,咱们能够拆分不少业务模块,每一个业务模块会有本身的业务模型,所以这里的modules是一个对象,而key就是业务模块的名字value就是处理业务模型的纯函数。 这里提供一injectFunc 主要是由于小程序在系统加载后就初始化, 所以咱们须要劫持特定api来在这个api中同步store中数据到当前显示的页面中。为何不写死成小程序的onShow?主要是之后考虑百度小程序,支付宝小程序。这样更灵活。 */
  function createStore(modules, injectFunc) { 
     if (injectFunc && typeof injectFunc === 'string') {
      injectMethod = injectFunc;
    }
    // 咱们将用户本身定义的业务模型(model)保存到沙盒内的reducers中
    if (modules && typeof modules === 'object') {
      const keys = Object.keys(modules);
      const len = keys.length;

      for (let i = 0; i < len; i++) {
        const key = keys[i];
        if (modules.hasOwnProperty(key) && typeof modules[key] === 'function') {
          reducers[key] = modules[key];
        }
      }
    }
    // 对store进行初始化
    dispatch({type: '@MPSTATE/INIT'});
  }
复制代码

7. 如何关联store的数据到小程序页面中,而且进行响应式处理?

小程序会自动订阅Page参数中的data对象,所以咱们只要在提供一个包裹函数将咱们须要订阅的store中的数据模型反映到小程序Page函数构建须要的参数中便可。而且注入dispatch方法,以及数据映射函数mapStoreToState

由于每一个页面只订阅本身关心的业务数据状态 ,所以咱们不能把整个store都扔给人家。因此咱们须要经过mapStoreToState来仅仅将用户须要的业务数据状态注入到页面中去。

/* *mapStoreToState,用于用户本身将本身关注的业务数据状态订阅到本身的页面中 */
  function connect(mapStoreToState, component) {
    if (!component || typeof component !== 'object') {
      throw new Error('mpState[connect]: Component must be a Object!');
    }

    if (!mapStoreToState || typeof mapStoreToState !== 'function') {
      throw new Error('mpState[connect]: mapStoreToState must be a Function!');
    }
    // 咱们须要将redux相关的函数和状态注入到用户的page定义中
    const newComponent = { ...component };
    // 拿到用户本身在页面定义的data,咱们须要保留原来的状态
    const data = component.data || {};
    // 获取用户订阅的store中的状态
    const extraData = mapStoreToState(finalState);

    if (!extraData || typeof extraData !== 'object') {
      throw new Error('mpState[connect]: mapStoreToState must return a Object!');
    }
    // 合并用户本身页面中的状态,和经过connect注入的store中的状态,这里个人实现有点很差
    let newData = null;

    if (typeof data === 'function') {
      newData = {
        ...data(),
        ...extraData
      }
    } else {
      newData = {
        ...data,
        ...extraData
      }
    }
    // 注入到Page对象中
    if (newData) {
      newComponent.data = newData;
    }
    // 获取须要劫持的生命周期钩子,由于每一个页面不必定都劫持同一个生命周期,所以提供了一个各个页面能够自定义修改劫持钩子的方法
    const injectFunc = component.getInjectMethod;

    const methods = component.methods || {};

    const newLiftMethod = injectFunc && injectFunc() || injectMethod;
    const oldLiftMethod = component[newLiftMethod];
    // 注入dispatch api
    methods.dispatch = dispatch;

    newComponent.methods = methods;
    newComponent.dispatch = dispatch;
    newComponent.mapStoreToState = mapStoreToState;
    //生命周期钩子劫持
    if (newLiftMethod) {
      newComponent[newLiftMethod] = function() {
        if (this) {
          // 在劫持的钩子中同步store的数据到页面
          this.dispatch({});
          oldLiftMethod && oldLiftMethod.call(this, arguments);
        }
      }
    }
    // 返回新的Page对象
    return newComponent;
  }
复制代码
// 使用connect来注入须要订阅的状态,而且mp-redux会在页面对象中自动注入dispatch方法 
  const mpState = require('./../../mp-redux/index.js');
  const util = require('../../utils/util.js');
  const logActions = require('./../../action/logs.js');

  Page(mpState.connect((state) => {
    return {
      userInfo: state.userInfo.userInfo,
      logs: state.logs.logs
    }
  },
  { // 在这里全部的业务数据都保存在store中,因此页面若是只有业务数据的话,是不须要data属性的。
    clearLogs() {
      this.dispatch({ // 经过dispatch方法来发出action,从而更新store中的数据
        type: logActions.clearLogs
      })
    }
  }))
复制代码

8. 如何派发更新store中的数据,而且反应到小程序的页面中来?

由于小程序的状态更新须要经过setData这个api,所以,咱们就须要在dispatch中经过该api来同步store中的数据状态

/* * 这里必定要注意,action是一个原生JS对象,而不是函数,Redux的异步是经过redux-thunk来实现的,可是个人诉求是须要让咱们应用中的业务逻辑更加容易被测试,所以也就没有去提供支持,其实实现起来也很简单。能够参考我作的[vue-with-redux源码](https://github.com/ryouaki/vue-with-redux/blob/master/src/index.js) */
  function dispatch(action) {
    // debugger
    const keys = Object.keys(reducers);
    const len = keys.length;
    // 这个循环用于遍历model来从新计算出新的store
    for (let i = 0; i < len; i++) {
      const key = keys[i];
      const currentReduce = reducers[key];
      const currentState = finalState[key];

      const newState = currentReduce(currentState, action);

      finalState[key] = newState;
    }

    if (this) {
      // 这里是根据组件内部的订阅规则来将新的数据模型经过setData注入到页面中
      const componentState = this.mapStoreToState(finalState) || {};
    // 这里提供了对react和vue的支持,所以也就致使代码多了几行,还在测试中。
      if (this.setData) { // 小程序
        this.setData({ ...componentState })
      } else if (this.setState) { // react什么的吧
        this.setState({ ...componentState })
      } else { // VUE
        const propKeys = Object.keys(componentState);
        for ( let i = 0; i < propKeys.length; i++) {
          this[propKeys[i]] = componentState[propKeys[i]];
        }
      }
    }
  }
复制代码

其实经过上面的代码咱们基本上就完成了一个简单的发布订阅了。

9. actionmodel(我以为modelreduce更容易理解,因此我叫model,哈哈)

不过这里没什么好说的,都和redux同样

const actions = require('./../action/logs.js');

const initState = {
  logs: []
}

module.exports = function (state = initState, action = {}) {
  const newState = { ...state };
  switch (action.type) {
    case actions.addLogs:
      const now = new Date();
      newState.logs.push({
        time: now.getHours() + ":" + now.getMinutes() + ":" + now.getSeconds(),
        value: action.data
      });
      return newState;
    case actions.clearLogs:
      newState.logs = [];
      return newState;
    default:
      return newState;
  }
}
复制代码

最后

经过这个mp-redux,实现了业务逻辑,数据与视图的分离,而业务逻辑与数据都保存在纯js代码中。方便多平台移植,而要作的只是作一个平台数据响应式的适配。

更大的好处是解决了视图与业务层耦合的痛点,而且将数据业务剥离到纯函数中,大大提升了业务代码的可测是性。

因为提供了业务数据的独立测试途径,也下降了总体的测试成本。

另外

给团队招人,途家网,地点国家会议中心,目前前端团队很是年轻,咱们有不少需求是没有既有库可以知足的,因此咱们有不少技术创新的机会。爱折腾的就联系我吧。

另外我在搞前端微服务的实践,并且已经成功,有兴趣的必定要联系我呀。

再者,咱们技术要求不高,我不在意什么Vue源码原理研究多深,也不在意算法多么牛,JS用多溜,我指望那些热爱技术,喜欢专研技术,喜欢经过团队业务开发中痛点挖掘出技术创新点,提升团队总体生产效率的人加入咱们 (这是个人观点,不表明老大是否赞同(-_-!))。