一.目标定位
想解决什么问题?打算怎么作?前端
简言之:dva想提供一个基于业界react&redux最佳实践的业务框架,以解决用裸redux全家桶做为前端数据层带来的种种问题react
编辑成本高,须要在reducer, saga, action之间来回切换web
不便于组织业务模型(或者叫domain model)。好比咱们写了一个userlist以后,要写一个productlist,须要复制不少文件。redux
saga书写太复杂,每监听一个action都须要走fork -> watcher -> worker的流程api
redux entry书写麻烦,要完成store建立,中间件配置,路由初始化,Provider的store的绑定,saga的初始化websocket
例如:react-router
+ src + sagas - user.js + reducers - user.js + actions - user.js + service - user.js
二.核心实现
怎么作了?app
依赖关系框架
dva react react-dom dva-core redux redux-saga history react-redux react-router-redux
实现思路
他最核心的是提供了app.model方法,用于把reducer, initialState, action, saga封装到一块儿less
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,类型没变不影响流程)