dva 是一个基于 redux 和 redux-saga 的数据流方案,在umi 中也能看到它的身影。
dvajs.com/guide/
简单使用
- 定义路由
javascript
// router.js
function RouterConfig({ history }) {
return (
<Router history={history}>
<Switch>
<Route path="/" exact component={IndexPage} />
<Route path="/user" exact component={UserPage} />
</Switch>
</Router>
);
}
- 定义Model层
javascript
// example
import request from "../utils/request";
export function queryUser() {
// return request('/api/users');
return request("https://randomuser.me/api");
}
////////////////////////////////////////////////////
import { queryUser } from "../services/example";
export default {
namespace: "user",
state: { name: { first: "匿名", age: "10" } },
subscriptions: {
setup({ dispatch, history }) {
// eslint-disable-line
},
},
effects: {
*fetchUser({ payload }, { call, put }) {
const res = yield call(queryUser);
console.log(res);
const user = res.data.results[0];
// eslint-disable-line
yield put({ type: "save", payload: user });
},
},
reducers: {
save(state, action) {
return { ...state, ...action.payload };
},
},
};
- view 层使用
javascript
import { connect } from "dva";
import React from "react";
const UserPage = ({ user, fetchUser }) => {
const refreshUser = () => {
fetchUser();
};
return (
<div>
<h3>用户信息</h3>
<div>姓名:{user.name.first}</div>
<button onClick={refreshUser}>刷新</button>
</div>
);
};
export default connect(
(state) => {
return { user: state.user };
},
{
fetchUser: () => ({ type: "user/fetchUser" }),
}
)(UserPage);

源码解析
那么,这个流程是怎么实现的呢,dva是怎么将react,redux-saga
串起来的呢,让我们来看下源码的实现
github.com/dvajs/dva
首先,我们先来看下入口文件
这个是生成npm包的工具,找到对应的.fatherrc
看下
dva()
函数的作用是
- 返回一个app 对象
- 将路由挂载到对象上
- 功能拆分为
create
router
start
javascript
// packages\dva\src\index.js
export default function(opts = {}) {
// 默认路由为HashRouter
const history = opts.history || createHashHistory();
// 初始化配置
const createOpts = {
initialReducer: {
router: connectRouter(history),
},
setupMiddlewares(middlewares) {
return [routerMiddleware(history), ...middlewares];
},
setupApp(app) {
app._history = patchHistory(history);
},
};
// 返回一个app对象
const app = create(opts, createOpts);
const oldAppStart = app.start;
app.router = router;
app.start = start;
return app;
// 省略...
}
我们先来看create
函数
这个函数的做了几件事
- 获取初始化的参数
- 创建了app对象
- 在
start
函数中使用了saga来处理异步逻辑 - 为app对象添加model、unmodel、replaceModel方法
javascript
export function create(hooksAndOpts = {}, createOpts = {}) {
// 获取参数
const { initialReducer, setupApp = noop } = createOpts
// 加载插件
const plugin = new Plugin()
plugin.use(filterHooks(hooksAndOpts))
const app = {
// 解构 reducers 和 effects
// 注入 model 层
_models: [prefixNamespace({ ...dvaModel })],
_store: null,
_plugin: plugin,
use: plugin.use.bind(plugin),
model,
start
}
return app
/**
* 应用启动前,注册 model
*/
function model(m) {
// 给 model 增加 namespace
// 省略代码
return prefixedModel
}
// 处理异步加载的文件注入,即在app 启动后懒加载
// 如果模型中定义了effects,那么运行这些effects
// 如果模型中定义了subscriptions,那么运行这些subscriptions,并将返回的取消订阅函数存储到unlisteners对象中
function injectModel(createReducer, onError, unlisteners, m) {
// 省略代码
}
/**
* 注销 model
* 移除 model 中的 reducers
* 取消订阅
* 删除 app._models 中的 model
*
*/
function unmodel(createReducer, reducers, unlisteners, namespace) {
// 省略代码
}
/**
* 替换 model 或者添加 model
*/
function replaceModel(createReducer, reducers, unlisteners, onError, m) {
// 省略代码
}
/**
* 启动应用
*/
function start() {
// Global error handler
// 处理全局错误
const onError = (err, extension) => {
// 省略代码
}
// 用于处理异步操作和处理Promise。然后,将sagaMiddleware的run方法绑定到应用程序的store上,以便运行saga
const sagaMiddleware = createSagaMiddleware()
const promiseMiddleware = createPromiseMiddleware(app)
app._getSaga = getSaga.bind(null)
// 遍历Model,为每个模型创建对应的reducer,并将其添加到reducers对象中
// 如果模型有effects,则将其对应的saga添加到sagas数组中
const sagas = []
const reducers = { ...initialReducer }
for (const m of app._models) {
// 省略代码
}
const reducerEnhancer = plugin.get('onReducer')
const extraReducers = plugin.get('extraReducers')
// 检测 extraReducers 是否和 dva 内部的 reducers 冲突
invariant(
Object.keys(extraReducers).every((key) => !(key in reducers)),
`[app.start] extraReducers is conflict with other reducers, reducers list: ${Object.keys(
reducers
).join(', ')}`
)
// Create store
app._store = createStore({
reducers: createReducer(),
initialState: hooksAndOpts.initialState || {},
plugin,
createOpts,
sagaMiddleware,
promiseMiddleware
})
const store = app._store
// 将sagaMiddleware的run方法赋值给store的runSaga属性,将空对象赋值给store的asyncReducers属性。
// 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
sagas.forEach(sagaMiddleware.run)
// Setup app
setupApp(app)
// Run subscriptions
// 运行订阅,遍历应用程序的模型,如果模型有subscriptions,则运行相应的订阅函数。
const unlisteners = {}
for (const model of this._models) {
// 省略代码
}
// Setup app.model and app.unmodel
// 为app对象添加model、unmodel、replaceModel方法
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的作用是将sage(Model层)注入到app中,并挂载到store中。
那么,这个store是如何与React结合并进行注入的呢?
我们回到dva
函数
github.com/dvajs/dva/b...
- 使用connectRouter包裹 root reducer 并且提供我们创建的history对象,获得新的 root reducer
- 在 provider 里可以嵌套 router
可以看到,在start
函数中,在最后一步加载元素时,做了数据拦截,将store和router传进去
这样可以确保所有的子组件都可以通过 context 访问到 Redux 的 store。
总结
经过对核心功能源码的深入分析,我们了解到dva的主要工作是将Model层抽离出来进行数据管理。其内部实现主要通过redux-saga来处理数据和异步action,最后利用connectRouter进行封装,从而实现在全局范围内都能通过context访问数据,大大简化了数据管理流程。
参考: