dva

一.目标定位
想解决什么问题?打算怎么做?

简言之:dva想提供一个基于业界react&redux最佳实践的业务框架,以解决用裸redux全家桶作为前端数据层带来的种种问题

编辑成本高,需要在reducer, saga, action之间来回切换

不便于组织业务模型(或者叫domain model)。比如我们写了一个userlist之后,要写一个productlist,需要复制很多文件。

saga书写太复杂,每监听一个action都需要走fork -> watcher -> worker的流程

redux entry书写麻烦,要完成store创建,中间件配置,路由初始化,Provider的store的绑定,saga的初始化

例如:


+ src
  + sagas
    - user.js
  + reducers
    - user.js
  + actions
    - user.js
  + service
    - user.js

二.核心实现
怎么做了?

依赖关系


dva
  react
  react-dom
  dva-core
    redux
    redux-saga
  history
  react-redux
  react-router-redux

实现思路
他最核心的是提供了app.model方法,用于把reducer, initialState, action, saga封装到一起


const model = {
    // 用作顶层state key,以及action前缀
    namespace
    // module级初始state
    state
    // 订阅其它数据源,如router change,window resize, key down/up...
    subscriptions
    // redux-saga里的sagas
    effects
    // redux里的reducer
    reducers
};

dva-core实际所作的主要工作是从model配置得到reducers,worker sagas, states后,屏蔽接下来的一系列繁琐工作:

接redux(组合state,组合reducer)

接redux-saga(完成redux-saga的fork -> watcher -> worker,并做好错误捕获)

除了core里最重要的两部分外,dva还做了一些事情:

内置react-router-redux, history负责路由管理

粘上react-redux的connect,isomorphic-fetch等常用的东西

subscriptions锦上添花,给监听场外因素的代码提供一个容身之处

和react连接起来(用store连接react和redux,靠redux中间件机制把redux-saga拉进来一起玩)

到这里差不多封装好了,那么,下面开一些口子增加一点灵活性:

递出一堆钩子(effect/reducer/action/state级hook),让内部状态可读

提供全局错误处理方式,解决异步错误不可控的痛点

增强model管理(允许动态增删model)

猜测整个实现过程是这样:

配置化

在技术上实现固化,把灵活性限制起来,让业务写法更统一,满足工程化的需要

面向通用场景扩展

只开必要的口子,放出能满足大多数业务场景需要的最小灵活性集合

面向特定需要增强

应对业务呼声,考虑是否放出/提供更多一些的灵活性,在灵活性与工程化(可控程度)之间权衡取舍

三.设计理念
遵从什么思想,想要怎么样?

借鉴自elm和choo,包括elm的subscription和choo的设计理念

elm的subscription
通过订阅一些消息来从其它数据源取数据,比如websocket connection of server, keyboard input, geolocation change, history router change等等

例如:


subscriptions: {
  setupHistory ({ dispatch, history }) {
    history.listen((location) => {
      dispatch({
        type: 'updateState',
        payload: {
          locationPathname: location.pathname,
          locationQuery: queryString.parse(location.search),
        },
      })
    })
  },
  setup ({ dispatch }) {
    dispatch({ type: 'query' })
    let tid
    window.onresize = () => {
      clearTimeout(tid)
      tid = setTimeout(() => {
        dispatch({ type: 'changeNavbar' })
      }, 300)
    }
  }
}

提供这种机制来接入其它数据源,并集中到model里统一管理

choo的设计理念
choo的理念是尽量精简,尽量降低选择/切换成本:


We believe frameworks should be disposable, and components recyclable. We don’t want a web where walled gardens jealously compete with one another. By making the DOM the lowest common denominator, switching from one framework to another becomes frictionless. choo is modest in its design; we don’t believe it will be top of the class forever, so we’ve made it as easy to toss out as it is to pick up.

We don’t believe that bigger is better. Big APIs, large complexities, long files – we see them as omens of impending userland complexity. We want everyone on a team, no matter the size, to fully understand how an application is laid out. And once an application is built, we want it to be small, performant and easy to reason about. All of which makes for easy to debug code, better results and super smiley faces.

大意是说框架不应该发展成堡垒,应该随时可用可不用(低成本切换),API及设计应该保持最小化,不要丢给用户一坨“知识”,这样你好他(同事)也好

P.S.当然,这段话拿到哪里都是对的,至于dva甚至choo自身有没有做到就不好说了(从choo的实现上没看出来有什么拆除堡垒的有效措施)

在API设计上,dva-core差不多保持最小化了:

一份model仅4个配置项

API屈指可数

hook差不多都是必须的(onHmr与extraReducers是后来面向特定需要的增强)

不过话说回来,dva-core实际做的只把redux和redux-saga通过model配置整合起来,并增强一些控制(错误处理等),引入的唯一外来概念是subscription,还挂在model上,即便用力设计API,也复杂不到哪去

四.优缺点
有什么缺点,带来的收益是什么?

优点:

框架限制有利于工程化,砖块一样的代码最好了

简化繁琐的样板代码(boilerplate code),仪式一样的action/reducer/saga/api…

解决多文件导致关注点分散的问题,逻辑分离是好事,但文件隔离就有点难受了

缺点:

限制了灵活性(比如combineReducers问题)

性能负担(getSaga部分的实现,看着就不快,做了不少额外的事情来达到控制的目的)

五.实现技巧
外置参数检查
invariant是源码出现最多的基本套路:


function start(container) {
  // 允许 container 是字符串,然后用 querySelector 找元素
  if (isString(container)) {
    container = document.querySelector(container);
    invariant(
      container,
      `[app.start] container ${container} not found`,
    );
  }

  // 并且是 HTMLElement
  invariant(
    !container || isHTMLElement(container),
    `[app.start] container should be HTMLElement`,
  );

  // 路由必须提前注册
  invariant(
    app._router,
    `[app.start] router must be registered before app.start()`,
  );

  oldAppStart.call(app);
  //...
}

invariant用来保证强条件(不满足条件直接throw,生产环境也throw),warning用来保证弱条件(开发环境log error并无干扰throw,生产环境不throw,换成空函数)

invariant无差别throw可以用,但warning不建议使用,因为含warning的release代码不如编译替换干净(还会执行空函数)

另一个技巧是包一层函数,在外面做参数检查,比如示例中的:


function start(container) {
  //...参数检查
  oldAppStart.call(app);
}

这样做的好处是把参数检查拿出去了,可读性会更好一些,但有多一层函数调用的性能开销,而且不如if-else控制度高(只能通过throw阻断后续流程)

切面Hook
先看这部分源码:


// 把每一个effect都包一遍,为了实现effect级的控制
const sagaWithOnEffect = applyOnEffect(onEffect, sagaWithCatch, model, key);
function applyOnEffect(fns, effect, model, key) {
  for (const fn of fns) {
    effect = fn(effect, sagaEffects, model, key);
  }
  return effect;
}

然后用法是这样的(传入的onEffect Hook):


function onEffect(effect, { put }, model, actionType) {
  const { namespace } = model;
  return function*(...args) {
      yield put({ type: SHOW, payload: { namespace, actionType } });
      yield effect(...args);
      yield put({ type: HIDE, payload: { namespace, actionType } });
  };
}

(摘自dva-loading

这不就是环绕增强(AOP里的Around Advice)吗?

围绕一个连接点的增强,如方法调用。这是最强大的一种增强类型。环绕增强可以在方法调用前后完成自定义的行为。它也负责选择是继续执行连接点,还是直接返回它们自己的返回值或者抛出异常来结束执行

(摘自AOP(Aspect-Oriented Programming))

这里的实际作用是onEffect把saga包一层,把saga的执行权交出去,允许外部(通过onEfect hook)注入逻辑。把自己交给hook,不是什么了不起的技巧,但用法上很有意思,利用iterator可展开的特性,实现了装饰者的效果(交出去一个saga,拿回来一个增强过的saga,类型没变不影响流程)

上一篇:Dva 源码解析


下一篇:umi + ts + dva中 model的动态使用