dva学习总结
Closed this issue · 0 comments
dva 初探
前言: 最近正在学习 dva ,整理出一些学习笔记,笔者默认阅读此文的读者有一定的react , redux , redux-saga 基础,如果没有,可先自行了解这些技术,本文不再赘述。
什么是 dva
dva是基于现有应用框架(redux
+react-router
+redux-saga
等)封装的一个框架(不是库),基本上没有引入新概念,也没有创建新语法,对于熟悉前言中涉及的技术栈的童鞋来说会非常容易上手。详细介绍可移步dva介绍
为什么会有 dva
在处理复杂异步请求的业务中,一开始我们是使用 redux-thunk + async/await 结合使用,比如在异步登录的逻辑中,使用 redux-thunk 处理如下:
// action/auth.js
import request from 'axios';
import { loadUserData } from './user';
export const login = (user, pass) => async (dispatch) => {
try {
dispatch({ type: LOGIN_REQUEST });
let { data } = await request.post('/login', { user, pass });
await dispatch(loadUserData(data.uid));
dispatch({ type: LOGIN_SUCCESS, data });
} catch(error) {
dispatch({ type: LOGIN_ERROR, error });
}
}
这种处理之后,组件调用的是dispatch(action creator)
,此时的 action 被赋予了太多的逻辑功能,不再是一个 pure action 。为了保持 action 的简洁性,继而引入 redux-saga ,它提供了一个 saga 文件用来存放异步逻辑,引入 redux-saga 之后,上面的验证用户登录逻辑就变成如下:
// sagas/index.js
import { take, call, put } from 'redux-saga/effects'
import Api from '...'
export function* login(user, pass) {
try {
const data = yield call(Api.authorize, user, pass)
yield put({type: 'LOGIN_SUCCESS', data.uid})
} catch(error) {
yield put({type: 'LOGIN_ERROR', error})
}
}
使用 redux-saga 之后,action 又回归其纯粹性。并且将异步操作全部抽离在 sagas 中一层进行处理,这样方便我们进行多种异步处理操作。
redux-saga 虽然在处理较为复杂的异步逻辑时提供了比较好的解决方案,但是当业务变复杂时,随着模块的逐渐增加,由于项目通常要分 reducer, action, saga, component 等等,所以项目中的文件个数也会变得很多,如下:
+ src
+ actions
- user.js
- detail.js
+ reducers
- user.js
- detail.js
+ sagas
- user.js
- detail.js
+ components
这样在项目开发过程中,就需要不断地切换文件目录,大大影响开发效率。于是 dva 应运而生,dva 的主要解决的项目开发中的痛点:
- reducer, saga, action 之间来回切换的开发成本
- saga 创建麻烦
- 主文件中的入口逻辑变得很复杂
上面的例子使用 dva 来实现如下:
// models/login.js
import Api from '...'
export default {
namespace: 'login',
state: {
user: null
},
effects: {
*login(){
const data = yield call(Api.authorize, user, pass)
yield put({type: 'LOGIN_SUCCESS', data.uid})
},
},
reducers: {}
}
其中,reducers 可以看成是同步的请求逻辑,effects 可以看成是异步的请求逻辑,所有的逻辑都放在了 models 目录下的文件中,省去了文件之间的切换成本,让开发人员可以专注于业务逻辑。
具体可以参考支付宝前端应用架构的发展和选择
dva 的相关知识点
dva中只有5个 API,8个新的概念,其中所有的 API 如下:
app = dva(Opts)
创建应用,返回 dva 实例app.use(Hooks)
配置 hooks 或者注册插件app.model(ModelObject)
注册 modelapp.router(Function)
注册路由表app.start([HTMLElement], opts)
启动应用
具体的使用可以移步这里
8个概念如下所示:
- State 表示应用的所有数据层,其中全局的 state 由所有 model的 state 组成
- Action 表示应用的所有事件,包括同步的和异步的,格式如下:
{
type: String,
payload: Any?,
error? Error,
}
调用的时候有如下两种方式:
dispatch(Action);
dispatch({ type: 'todos/add', payload: 'todo content' });
- Model 用于将数据相关的逻辑进行聚合
- Reducer 和 redux 中的 reducer 概念相同,接受 state,action 作为参数,返回新的state
- Effect 用来处理异步逻辑,使用 generator实现
- Subscription 表示订阅,用于订阅一个数据源,然后按需 dispatch action。
- Router 路由的配置信息
- RouteComponent 表示 Router 里匹配路径的 Component,通常会绑定 model 的数据
dva 的使用
如何基于 dva 开发一个项目,dva 的作者给出了一个一步步开发 dva 项目的教程, 笔者仿照该教程,并且基于 dva2.0, 做出了一个 demo,该 demo 类似于 dva中的范例,只是初步体验一下 dva 的开发。
深入 dva
借用描述 dva 数据流动的一张图,如下所示:
如图所示:用户在浏览器中访问某个 URL,由此渲染一个页面,该页面可能包含多个 Components, 当用户在页面进行操作的时候,由此 dispatch 某个 action,同步的 action 逻辑放在 Reducer 中,异步的 action 逻辑存放在 Effect 中。通过 model 中的数据处理,将新的 state 传入页面中,从而触发页面数据的更新。
dva 源码解读
这次的解读主要是针对 dva@2.1 和 dva-core@1.1。
首先是 dva 中的入口文件所暴露出来的方法,主要是const app = dva();
这行代码的作用,返回一个 app实例。该方法如下:
export default function (opts = {}) {
const history = opts.history || createHashHistory(); //history默认是HashHistory
const createOpts = {
initialReducer: {
routing,
},
setupMiddlewares(middlewares) {
return [
routerMiddleware(history),
...middlewares,
];
},
setupApp(app) {
app._history = patchHistory(history);
},
};
const app = core.create(opts, createOpts);
const oldAppStart = app.start;
app.router = router;
app.start = start;
return app;
}
// 此处略去一些方法的定义
这个函数很简单,主要是调用了 dva-core 里面的 create 方法,并且返回了一个包含如下方法的 app 对像:
var app = {
_models: [(0, _prefixNamespace2.default)((0, _extends3.default)({}, dvaModel))],
_store: null,
_plugin: plugin,
use: plugin.use.bind(plugin),
model: model,
start: start
};
对 app 的初始化定义在 dva-core/lib/index.js
文件中。在这个文件中,实现了 app 对象的所有方法。接下来一个一个进行分析:
- model()
这个方法比较简单,只是将传进来的 model push 进 _models 这个属性中。这就意味着每次我们注册 model 时,只能单个进行传递,不能以数组的形式进行传递,例如:
app.model(Model1); app.model(Model2);
//而不是
app.model([Model1,Model2])
其实 app.model 在调用 app.start 之后会变成 injectModel()
, 它的源码如下:
function injectModel(createReducer, onError, unlisteners, m) {
model(m);
var store = app._store;
if (m.reducers) {
store.asyncReducers[m.namespace] = (0, _getReducer2.default)(m.reducers, m.state);
store.replaceReducer(createReducer(store.asyncReducers));
}
if (m.effects) {
store.runSaga(app._getSaga(m.effects, m, onError, plugin.get('onEffect')));
}
if (m.subscriptions) {
unlisteners[m.namespace] = (0, _subscription.run)(m.subscriptions, m, app, onError);
}
}
这个函数里面调用了上面的 model() ,除此之外,该函数还将 model 定义的 reducers,effects, subscriptions 进行分别处理。
- reducers 分支 是调用 redux 的原生 api 对 model 中的 reducers 进行处理
- effects 分支是调用 redux-saga 中的
sagaMiddleware.run()
来执行管理一部 action,在这之前,先调用了app._getSaga()
方法:
export default function getSaga(resolve, reject, effects, model, onError, onEffect) {
return function *() {
for (const key in effects) {
if (Object.prototype.hasOwnProperty.call(effects, key)) {
const watcher = getWatcher(resolve, reject, key, effects[key], model, onError, onEffect);
const task = yield sagaEffects.fork(watcher);
yield sagaEffects.fork(function *() {
yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`);
yield sagaEffects.cancel(task);
});
}
}
};
}
这个方法主要实现了 saga 那一套的watch/worker
(监听->执行) 的工作形式。
- subscriptions 分支调用了同级目录下 subscription.js 中的 run():