一、介绍
dva 首先是一个基于 redux
和 redux-saga
的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router
和 fetch
,所以也可以理解为一个轻量级的应用框架。
1.1 特性
- 易学易用,仅有 6 个 api,对 redux 用户尤其友好,配合 umi 使用后更是降低为 0 API
- elm 概念,通过 reducers, effects 和 subscriptions 组织 model
- 插件机制,比如 dva-loading 可以自动处理 loading 状态,不用一遍遍地写 showLoading 和 hideLoading
- 支持 HMR,基于 babel-plugin-dva-hmr 实现 components、routes 和 models 的 HMR
二、数据流向
数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过 dispatch
发起一个 action
,如果是同步行为会直接通过 Reducers
改变 State
,如果是异步行为(副作用)会先触发 Effects
然后流向 Reducers
最终改变 State
,所以在 dva
中,数据流向非常清晰简明,并且思路基本跟开源社区保持一致(也是来自于开源社区)。
三、Model
model 是 dva 中最重要的概念。以下是典型的例子:
app.model({
namespace: 'todo',
state: [],
reducers: {
add(state, { payload: todo }) {
// 保存数据到 state
return [...state, todo];
},
},
effects: {
*save({ payload: todo }, { put, call }) {
// 调用 saveTodoToServer,成功后触发 `add` action 保存到 state
yield call(saveTodoToServer, todo);
yield put({ type: 'add', payload: todo });
},
},
subscriptions: {
setup({ history, dispatch }) {
// 监听 history 变化,当进入 `/` 时触发 `load` action
return history.listen(({ pathname }) => {
if (pathname === '/') {
dispatch({ type: 'load' });
}
});
},
},
});
3.1 State
State
表示 Model
的状态数据,通常表现为一个 javascript
对象(当然它可以是任何值);操作的时候每次都要当作不可变数据(immutable data)来对待,保证每次都是全新对象,没有引用关系,这样才能保证 State
的独立性,便于测试和追踪变化。
- 在 dva 中你可以通过 dva 的实例属性
_store
看到顶部的state
数据,但是通常你很少会用到:const app = dva(); console.log(app._store); // 顶部的 state 数据
- 优先级低于传给 dva() 的 opts.initialState。
此时,在 app.start() 后 state.count 为 1const app = dva({ initialState: { count: 1 }, }); app.model({ namespace: 'count', state: 0, });
3.2 Action
type AsyncAction = any
Action
是一个普通 javascript 对象,它是改变 State
的唯一途径。无论是从 UI 事件、网络回调,还是 WebSocket 等数据源所获得的数据,最终都会通过 dispatch
函数调用一个 action
,从而改变对应的数据。action
必须带有 type
属性指明具体的行为,其它字段可以自定义,如果要发起一个 action
需要使用 dispatch
函数;需要注意的是 dispatch
是在组件 connect
Models以后,通过 props
传入的。
dispatch({
type: 'add',
});
3.3 dispatch 函数
type dispatch = (a: Action) => Action
dispatching
function 是一个用于触发 action
的函数,action
是改变 State
的唯一途径,但是它只描述了一个行为,而 dipatch
可以看作是触发这个行为的方式,而 Reducer
则是描述如何改变数据的。
在 dva
中,connect
Model 的组件通过 props
可以访问到 dispatch
,可以调用 Model
中的 Reducer
或者 Effects
,常见的形式如:
dispatch({
type: 'user/add', // 如果在 model 外调用,需要添加 namespace
payload: {}, // 需要传递的信息
});
3.4 Reducer
type Reducer<S, A> = (state: S, action: A) => S
Reducer
(也称为 reducing function)函数接受两个参数:之前已经累积运算的结果和当前要被累积的值,返回的是一个新的累积结果。该函数把一个集合归并成一个单值。
3.5 namespace
model 的命名空间,同时也是他在全局 state 上的属性,只能用字符串,不支持通过 . 的方式创建多层命名空间
3.6 effects
以 key/value 格式定义 effect。用于处理异步操作和业务逻辑,不直接修改 state
。由 action
触发,可以触发 action
,可以和服务器交互,可以获取全局 state
的数据等等。
格式为 *(action, effects) => void
或 [*(action, effects) => void, { type }]
。
-
put:用于触发action
yield put({ type: 'todos/add', payload: 'Learn Dva'});
-
call:用于调用异步逻辑,支持Promise
const result = yield call(fetch, '/todos');
这个call与JS的call用法大概一致,这个call的第一个参数是你要调用的函数,第二个参数开始是你要传递的参数,可一 一传递。
-
select:用于从state里获取数据。
const todos = yield select(state => state.todos);
3.7 subscriptions
以 key/value 格式定义 subscription
。subscription
是订阅,用于订阅一个数据源,然后根据需要 dispatch
相应的 action
。在 app.start()
时被执行,数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。
格式为 ({ dispatch, history }, done) => unlistenFunction
。
/*订阅*/
subscriptions: {
/**
* 付民康 2021/3/1
* desc: 监听history对象的变化,执行回话检查操作
* @params
**/
setup({ dispatch, history }) { // eslint-disable-line
history.listen(({ pathname }) => {
const blackList = ['/login', '/logout'];
if (!blackList.includes(pathname)) {
dispatch({ type: 'checkAuth' }); // 每次访问新路由的时候,检查一下会话
}
});
},
},
注意:如果要使用 app.unmodel()
,subscription 必须返回 unlisten 方法,用于取消数据订阅。
四、输出文件
4.1 dva
默认输出文件。
4.2 dva/router
默认输出 react-router
接口, react-router-redux
的接口通过属性 routerRedux 输出。
比如:
import { Router, Route, routerRedux } from 'dva/router';
跳转1:从props取出并传递history
<button onClick={ () => this.props.history.push('/') }>go back home</button>
跳转2:withRouter, Link
import { withRouter, Link } from 'dva/router'
<button onClick={ () => history.push('/') }>go back home</button>
<Link to='/'>home page</Link>
export default withRouter(Counter);
跳转3:routerRedux
dispatch(routerRedux.push(item.url));
4.3 dva/fetch
异步请求库,输出 isomorphic-fetch
的接口。不和 dva 强绑定,可以选择任意的请求库。
4.4 dva/saga
输出 redux-saga
的接口,主要用于用例的编写。(用例中需要用到 effects)
4.5 dva/dynamic
解决组件动态加载问题的 util 方法。
import dynamic from 'dva/dynamic';
const UserPageComponent = dynamic({
app,
models: () => [
import('./models/users'),
],
component: () => import('./routes/UserPage'),
});
opts 包含:
- app: dva 实例,加载 models 时需要
- models: 返回 Promise 数组的函数,Promise 返回 dva model
- component:返回 Promise 的函数,Promise 返回 React Component
五、dva API
5.1 app = dva(opts)
创建应用,返回 dva 实例。(注:dva 支持多实例)
opts 包含:
- history:指定给路由用的 history,默认是 hashHistory
- initialState:指定初始数据,优先级高于 model 中的 state,默认是 {}
如果要配置 history 为 browserHistory,可以这样:
import createHistory from 'history/createBrowserHistory';
const app = dva({
history: createHistory(),
});
另外,出于易用性的考虑,opts 里也可以配所有的 hooks ,下面包含全部的可配属性:
const app = dva({
history,
initialState,
onError,
onAction,
onStateChange,
onReducer,
onEffect,
onHmr,
extraReducers,
extraEnhancers,
});
5.2 app.use(hooks)
配置 hooks 或者注册插件。(插件最终返回的是 hooks )
比如注册 dva-loading
插件的例子:
import createLoading from 'dva-loading';
...
app.use(createLoading(opts));
hooks 包含:
5.2.1 onError((err, dispatch) => {})
effect
执行错误或 subscription
通过 done
主动抛错时触发,可用于管理全局出错状态。
注意:subscription
并没有加 try...catch
,所以有错误时需通过第二个参数 done
主动抛错。例子:
app.model({
subscriptions: {
setup({ dispatch }, done) {
done(e);
},
},
});
如果我们用 antd,那么最简单的全局错误处理通常会这么做:
import { message } from 'antd';
const app = dva({
onError(e) {
message.error(e.message, /* duration */3);
},
});
5.2.2 onAction(fn | fn[])
在 action 被 dispatch 时触发,用于注册 redux 中间件。支持函数或函数数组格式。
例如我们要通过 redux-logger
打印日志:
import createLogger from 'redux-logger';
const app = dva({
onAction: createLogger(opts),
});
5.2.3 onStateChange(fn)
state
改变时触发,可用于同步 state
到 localStorage,服务器端等。
5.2.4 onReducer(fn)
封装 reducer 执行。比如借助 redux-undo
实现 redo/undo :
import undoable from 'redux-undo';
const app = dva({
onReducer: reducer => {
return (state, action) => {
const undoOpts = {};
const newState = undoable(reducer, undoOpts)(state, action);
// 由于 dva 同步了 routing 数据,所以需要把这部分还原
return { ...newState, routing: newState.present.routing };
},
},
});
5.3 app.model(model)
注册 model,详见 #Model 部分
5.4 app.unmodel(namespace)
取消 model 注册,清理 reducers, effects 和 subscriptions。subscription 如果没有返回 unlisten 函数,使用 app.unmodel 会给予警告⚠️。
5.5 app.replaceModel(model)
只在app.start()之后可用
替换model为新model,清理旧model的reducers, effects 和 subscriptions,但会保留旧的state状态,对于HMR非常有用。subscription 如果没有返回 unlisten 函数,使用 app.unmodel
会给予警告⚠️。
如果原来不存在相同namespace的model,那么执行app.model
操作
5.6 app.router(({ history, app }) => RouterConfig)
注册路由表。
通常是这样的:
import { Router, Route } from 'dva/router';
app.router(({ history }) => {
return (
<Router history={history}>
<Route path="/" component={App} />
</Router>
);
});
推荐把路由信息抽成一个单独的文件,这样结合 babel-plugin-dva-hmr 可实现路由和组件的热加载,比如:
app.router(require('./router'));
而有些场景可能不使用路由,比如多页应用,所以也可以传入返回 JSX 元素的函数。比如:
app.router(() => <App />);
5.7 app.start(selector?)
启动应用。selector 可选,如果没有 selector 参数,会返回一个返回 JSX 元素的函数。
app.start('#root');
那么什么时候不加 selector?常见场景有测试、node 端、react-native 和 i18n 国际化支持。
比如通过 react-intl 支持国际化的例子:
import { IntlProvider } from 'react-intl';
...
const App = app.start();
ReactDOM.render(<IntlProvider><App /></IntlProvider>, htmlElement);