dva 首先是一个基于redux[1]和redux-saga[2]的数据流方案,然后为了简化开发体验,dva 还额外内置了react-router[3]和fetch[4],所以也可以理解为一个轻量级的应用框架。
经过一段时间的自学或培训,大家应该都能理解 redux 的概念,并认可这种数据流的控制可以让应用更可控,以及让逻辑更清晰。但随之而来通常会有这样的疑问:概念太多,并且 reducer, saga, action 都是分离的(分文件)。
import dva from 'dva';
const App = () => <div>Hello dva</div>;
// 创建应用
const app = dva();
// 注册视图
app.router(() => <App />);
// 启动应用
app.start('#root');
// 创建应用
const app = dva();
app.use(createLoading()) // 使用插件
// 注册 Model
app.model({
namespace: 'count',
state: 0,
reducers: {
add(state) { return state + 1 },
},
effects: {
*addAfter1Second(action, { call, put }) {
yield call(delay, 1000);
yield put({ type: 'add' });
},
},
});
// 注册视图
app.router(() => <ConnectedApp />);
// 启动应用
app.start('#root');
dva 做了三件比较重要的事情:
// dva/src/index.js
export default function (opts = {}) {
// 1. 使用 connect-react-router 和 history 初始化 router 和 history
// 通过添加 redux 的中间件 react-redux-router,强化了 history 对象的功能
const history = opts.history || createHashHistory();
const createOpts = {
initialReducer: {
router: connectRouter(history),
},
setupMiddlewares(middlewares) {
return [routerMiddleware(history), ...middlewares];
},
setupApp(app) {
app._history = patchHistory(history);
},
};
// 2. 调用 dva-core 里的 create 方法 ,函数内实例化一个 app 对象。
const app = create(opts, createOpts);
const oldAppStart = app.start;
// 3. 用自定义的 router 和 start 方法代理
app.router = router;
app.start = start;
return app;
// 3.1 绑定用户传递的 router 到 app._router
function router(router) {
invariant(
isFunction(router),
`[app.router] router should be function, but got ${typeof router}`,
);
app._router = router;
}
// 3.2 调用 dva-core 的 start 方法,并渲染视图
function start(container) {
// 对 container 做一系列检查,并根据 container 找到对应的DOM节点
if (!app._store) {
oldAppStart.call(app);
}
const store = app._store;
// 为HMR暴露_getProvider接口
// ref: https://github.com/dvajs/dva/issues/469
app._getProvider = getProvider.bind(null, store, app);
// 渲染视图
if (container) {
render(container, store, app, app._router);
app._plugin.apply('onHmr')(render.bind(null, container, store, app));
} else {
return getProvider(store, this, this._router);
}
}
}
function getProvider(store, app, router) {
const DvaRoot = extraProps => (
<Provider store={store}>{router({ app, history: app._history, ...extraProps })}</Provider>
);
return DvaRoot;
}
function render(container, store, app, router) {
const ReactDOM = require('react-dom'); // eslint-disable-line
ReactDOM.render(React.createElement(getProvider(store, app, router)), container);
}
我们同时可以发现 app 是通过 create(opts, createOpts)进行初始化的,其中 opts 是暴露给使用者的配置,createOpts 是暴露给开发者的配置,真实的 create 方法在 dva-core 中实现
dva-core 则完成了核心功能:
dva-core create
作用: 完成 app 实例的构造,并暴露 use、model 和 start 三个接口
// dva-core/src/index.js
const dvaModel = {
namespace: '@@dva',
state: 0,
reducers: {
UPDATE(state) {
return state + 1;
},
},
};
export function create(hooksAndOpts = {}, createOpts = {}) {
const { initialReducer, setupApp = noop } = createOpts; // 在dva/index.js中构造了createOpts对象
const plugin = new Plugin(); // dva-core中的插件机制,每个实例化的dva对象都包含一个plugin对象
plugin.use(filterHooks(hooksAndOpts)); // 将dva(opts)构造参数opts上与hooks相关的属性转换成一个插件
const app = {
_models: [prefixNamespace({ ...dvaModel })],
_store: null,
_plugin: plugin,
use: plugin.use.bind(plugin), // 暴露的use方法,方便编写自定义插件
model, // 暴露的model方法,用于注册model
start, // 原本的start方法,在应用渲染到DOM节点时通过oldStart调用
};
return app;
}
dva-core start
作用:
function start() {
const sagaMiddleware = createSagaMiddleware();
const promiseMiddleware = createPromiseMiddleware(app);
app._getSaga = getSaga.bind(null);
const sagas = [];
const reducers = { ...initialReducer };
for (const m of app._models) {
// 把每个 model 合并为一个reducer,key 是 namespace 的值,value 是 reducer 函数
reducers[m.namespace] = getReducer(m.reducers, m.state, plugin._handleActions);
if (m.effects) {
// 收集每个 effects 到 sagas 数组
sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect'), hooksAndOpts));
}
}
// 初始化 Store
app._store = createStore({
reducers: createReducer(),
initialState: hooksAndOpts.initialState || {},
plugin,
createOpts,
sagaMiddleware,
promiseMiddleware,
});
const store = app._store;
// Extend store
store.runSaga = sagaMiddleware.run;
store.asyncReducers = {};
// Execute listeners when state is changed
const listeners = plugin.get('onStateChange');
for (const listener of listeners) {
store.subscribe(() => {
listener(store.getState());
});
}
// Run sagas, 调用 Redux-Saga 的 createSagaMiddleware 创建 saga中间件,调用中间件的 run 方法所有收集起来的异步方法
// run方法监听每一个副作用action,当action发生的时候,执行对应的 saga
sagas.forEach(sagaMiddleware.run);
// Setup app
setupApp(app);
// 运行 subscriptions
const unlisteners = {};
for (const model of this._models) {
if (model.subscriptions) {
unlisteners[model.namespace] = runSubscription(model.subscriptions, model, app, onError);
}
}
// 暴露三个 Model 相关的接口,Setup app.model and app.unmodel
app.model = injectModel.bind(app, createReducer, onError, unlisteners);
app.unmodel = unmodel.bind(app, createReducer, reducers, unlisteners);
app.replaceModel = replaceModel.bind(app, createReducer, reducers, unlisteners, onError);
/**
* Create global reducer for redux.
*
* @returns {Object}
*/
function createReducer() {
return reducerEnhancer(
combineReducers({
...reducers,
...extraReducers,
...(app._store ? app._store.asyncReducers : {}),
}),
);
}
}
}
在前面的 dva.start 方法中我们看到了 createOpts,并了解到在 dva-core 的 start 中的不同时机调用了对应方法。
import * as routerRedux from 'connected-react-router';
const { connectRouter, routerMiddleware } = routerRedux;
const createOpts = {
initialReducer: {
router: connectRouter(history),
},
setupMiddlewares(middlewares) {
return [routerMiddleware(history), ...middlewares];
},
setupApp(app) {
app._history = patchHistory(history);
},
};
其中 initialReducer 和 setupMiddlewares 在初始化 store 时调用,然后才调用 setupApp
可以看见针对 router 相关的 reducer 和中间件配置,其中 connectRouter 和 routerMiddleware 均使用了 connected-react-router 这个库,其主要思路是:把路由跳转也当做了一种特殊的 action。
按照 React 官方指导意见, 如果多个 Component 之间要发生交互, 那么状态(即: 数据)就维护在这些 Component 的最小公约父节点上, 也即是
以及 本身不维持任何 state, 完全由父节点 传入 props 以决定其展现, 是一个纯函数的存在形式, 即: Pure Component
与上图相比, 几个明显的改进点:
这样一来, 各个部分各司其职, 耦合度更低, 复用度更高, 扩展性更好。
因为我们可以使用 Middleware 拦截 action, 这样一来异步的网络操作也就很方便了, 做成一个 Middleware 就行了, 这里使用 redux-saga 这个类库, 举个栗子:
有了前面三步的铺垫, Dva 的出现也就水到渠成了, 正如 Dva 官网所言, Dva 是基于 React + Redux + Saga 的最佳实践, 对于提升编码体验有三点贡献:
约定大于配置
app.model({
namespace: 'count',
state: {
record: 0,
current: 0,
},
reducers: {
add(state) {
const newCurrent = state.current + 1;
return { ...state,
record: newCurrent > state.record ? newCurrent : state.record,
current: newCurrent,
};
},
minus(state) {
return { ...state, current: state.current - 1};
},
},
effects: {
*add(action, { call, put }) {
yield call(delay, 1000);
yield put({ type: 'minus' });
},
},
subscriptions: {
keyboardWatcher({ dispatch }) {
key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
},
},
});
Dva 的 api 参考了choo[17],概念来自于 elm。
We believe programming should be fun and light, not stern and stressful. It's cool to be cute; using serious words without explaining them doesn't make for better results - if anything it scares people off. We don't want to be scary, we want to be nice and fun, and thencasually_be the best choice around._Real casually.
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.
[1]
redux: https://github.com/reduxjs/redux
[2]
redux-saga: https://github.com/redux-saga/redux-saga
[3]
react-router: https://github.com/ReactTraining/react-router
[4]
fetch: https://github.com/github/fetch
[5]
redux entry: https://github.com/ant-design/antd-init/blob/master/boilerplates/redux/src/entries/index.js
[6]
配合 umi 使用: https://umijs.org/guide/with-dva.html
[7]
dva-loading: https://github.com/dvajs/dva/tree/master/packages/dva-loading
[8]
babel-plugin-dva-hmr: https://github.com/dvajs/babel-plugin-dva-hmr
[9]
dva@3 前年提出计划后,官方几乎不再维护: https://github.com/dvajs/dva/issues/2208
[10]
redux-sagas: http://superraytin.github.io/redux-saga-in-chinese
[11]
generator 的相关概念: http://www.ruanyifeng.com/blog/2015/04/generator.html
[12]
elm: https://elm-lang.org/news/farewell-to-frp
[13]
react-router: https://github.com/reactjs/react-router
[14]
组件设计方法: https://github.com/dvajs/dva-docs/blob/master/v1/zh-cn/tutorial/04-%E7%BB%84%E4%BB%B6%E8%AE%BE%E8%AE%A1%E6%96%B9%E6%B3%95.md
[15]
dva: https://github.com/dvajs/dva/blob/master/packages/dva/src/index.js
[16]
dva-core: https://github.com/dvajs/dva/blob/master/packages/dva-core/src/index.js
[17]
choo: https://github.com/choojs/choo
[18]
Why dva and what's dva: https://github.com/dvajs/dva/issues/1
[19]
支付宝前端应用架构的发展和选择: https://www.github.com/sorrycc/blog/issues/6
[20]
React + Redux 最佳实践: https://github.com/sorrycc/blog/issues/1
[21]
Dva 概念: https://dvajs.com/guide/concepts.html#%E6%95%B0%E6%8D%AE%E6%B5%81%E5%90%91
[22]
Dva 入门课: https://dvajs.com/guide/introduce-class.html#react-%E6%B2%A1%E6%9C%89%E8%A7%A3%E5%86%B3%E7%9A%84%E9%97%AE%E9%A2%98
[23]
Dva 源码解析: https://dvajs.com/guide/source-code-explore.html#%E9%9A%90%E8%97%8F%E5%9C%A8-package-json-%E9%87%8C%E7%9A%84%E7%A7%98%E5%AF%86
[24]
Dva 源码实现: https://www.yuque.com/lulongwen/react/ixolgo
[25]
Dva 源码分析: https://www.shymean.com/article/dva%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90