dva 首先是一个基于 redux和 redux-saga的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router和 fetch,所以也可以理解为一个轻量级的应用框架。
dva 是基于现有应用架构 (redux + react-router + redux-saga 等)的一层轻量封装。dva 是 react 和 redux 的最佳实践。最核心的是提供了 app.model 方法,用于把 reducer, initialState, action, saga 封装到一起。
创建一个dva项目很简单,dva为我们提供了dva-cli,用来快速创建dva项目。
通过 npm 安装 dva-cli 并确保版本是 0.9.1 或以上。
$ npm install dva-cli -g
$ dva -v
dva-cli version 0.10.1
dva version 2.4.1
roadhog version 2.5.0-beta.4
$ dva new dva-quickstart
运行
$ cd dva-quickstart
$ npm start
Compiled successfully!
You can now view Your App in the browser.
Local: http://localhost:8000/
On Your Network: http://192.168.43.29:8000/
Note that the development build is not optimized.
To create a production build, use npm run build.
运行成功后,会自动打开浏览器,默认为4000端口,可以看到下面的界面
│ .editorconfig # 编辑器配置
│ .eslintrc # eslint配置
│ .gitignore # 配置git忽略目录或文件
│ .roadhogrc.mock.js# mock数据配置
│ .webpackrc # webpack配置文件
│ package.json # 项目配置文件
├─mock # 模拟数据文件
├─public # 公共资源
└─src
│ index.css # 主样式
│ index.js # 入口文件
│ router.js # 路由配置文件
├─assets # 放置静态资源
├─components # 组件
├─models # dva最核心的内容
├─routes # 路由页面
├─services # 请求后台数据,调用后台API接口
└─utils # 工具类,默认放置了封装fetch的方法
数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State ,如果是异步行为(副作用)会先触发 Effects 然后流向 Reducers 最终改变 State,所以在 dva 中,数据流向非常清晰简明,并且思路基本跟开源社区保持一致(也是来自于开源社区)。
通信问题
组件会发生三种通信。
React 只提供了一种通信手段:传参。对于大应用,很不方便。
数据流问题
目前流行的数据流方案有:
最流行的社区 React 应用架构方案如下:
那么dva所做的事情就是把他们进行了整合与封装,只暴露出几个简单的API,使得react项目的开发变得方便快捷。
在index.js中,可以看到dva执行的步骤
import dva from 'dva';
import './index.css';
// 1. Initialize
const app = dva({});
// 2. Plugins
// app.use({});
// 3. Model
app.model(require('./models/example').default);
// 4. Router
app.router(require('./router').default);
// 5. Start
app.start('#root');
首先看一下Model,这是dva的核心模块,组件之前的数据状态管理全都要靠它,通过Model可以解决react各组件间的传值问题。
一个Model就是一个对象,它包含以下属性:
reducers中包含多个Action 处理器,它接受两个参数,一个是当前的state,另一个是传入的参数,返回处理后的state来更新当前的state。
reducers: {
delete(state, { payload: id }) {
return state.filter(item => item.id !== id);
},
},
Effect 是一个generator函数,内部使用yield关键字,标识每一步的操作(不管是同步还是异步),dva 提供多个 effect 函数内部的处理函数,比较常用的是 call 和 put。 call 用来执行异步操作, put用来发出一个Action,类似于 dispatch。就像下面这样:
effects:{
*deleteItem(action, { call, put}){
yield new Promise(resolve => {
setTimeout(() => {
resolve()
}, 1000);
})
yield put({
type: 'delete',
payload: action.payload
})
}
}
State只是一个存储数据的地方,而数据的表现还是要在UI层进行呈现,还要可以实现用户操作UI层改变State的目的,State改变后也会引起View的改变。这就需要将State与View关联起来。
dva提供了一个叫connect的方法,connect 方法传入的第一个参数是 mapStateToProps 函数,mapStateToProps 函数会返回一个对象,用于建立 State 到 Props 的映射关系。它返回的也是一个 React 组件,通常称为容器组件。
const Products = ({ dispatch, products }) => {
function handleDelete(id) {
dispatch({
type: 'products/deleteItem',
payload: id,
});
}
return (
<div>
<h2>List of Products</h2>
<ProductList onDelete={handleDelete} products={products} />
</div>
);
};
export default connect(({ products }) => ({
products,
}))(Products);
在handleDelete中有一个dispatch方法,被 connect 的 Component 会自动在 props 中拥有 dispatch 方法,它就是用来改变state的方法。其中的type的值是以“namespace/effect或reducer”的形式,payload为传入的额外参数。
定义路由,默认输出 react-router接口, react-router-redux的接口通过属性 routerRedux 输出。
import { Router, Route, routerRedux } from 'dva/router';
异步请求库,输出 isomorphic-fetch的接口。不和 dva 强绑定,可以选择任意的请求库。
import fetch from 'dva/fetch';
function parseJSON(response) {
return response.json();
}
function checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response;
}
const error = new Error(response.statusText);
error.response = response;
throw error;
}
export default function request(url, options) {
return fetch(url, options)
.then(checkStatus)
.then(parseJSON)
.then(data => ({ data }))
.catch(err => ({ err }));
}
输出 redux-saga的接口,主要用于用例的编写。(用例中需要用到 effects)
解决组件动态加载问题的 util 方法。
在实际项目开发中,我们的model数量是很多的,每个model一般都只是在对应的几个模块中会用到,但在dva的初始化过程中,我们就必须把所有要用的model加载进来,这显然对性能方面是很不友好的。如果能在对应的路由下只加载所用到的model,这将会得到很大的提升。
import dynamic from 'dva/dynamic';
const UserPageComponent = dynamic({
app,
models: () => [
import('./models/users'),
],
component: () => import('./routes/UserPage'),
});
dva中可以灵活的配置 hooks 或者注册插件,如dva-loading 可以自动处理 loading 状态,不用一遍遍地写 showLoading 和 hideLoading
import createLoading from 'dva-loading';
...
app.use(createLoading(opts));
app = dva(opts)
app = dva(opts)
opts中可以设置 history 和初始化 state 数据,这里的 state 优先级要高于model中的state。
const app = dva({
history: createHistory(),
initialState: { //初始数据
products: [
{ name: 'dva', id: 1 },
{ name: 'antd', id: 2 },
],
},
});
还可以在这里面设置以下hooks:
onError((err, dispatch) => {})
effect 执行错误或 subscription 通过 done 主动抛错时触发,可用于管理全局出错状态。如果我们用 antd,那么最简单的全局错误处理通常会这么做:
import { message } from 'antd';
const app = dva({
onError(e) {
message.error(e.message, /* duration */3);
},
});
onAction(fn | fn[])
在 action 被 dispatch 时触发,用于注册 redux 中间件。支持函数或函数数组格式。例如我们要通过 redux-logger打印日志:
import createLogger from 'redux-logger';
const app = dva({
onAction: createLogger(opts),
});
onStateChange(fn)
state 改变时触发,可用于同步 state 到 localStorage,服务器端等。
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 };
},
},
});
onEffect(fn)
封装 effect 执行。比如 dva-loading基于此实现了自动处理 loading 状态。
onHmr(fn)
热替换相关,目前用于 babel-plugin-dva-hmr。
extraReducers
指定额外的 reducer,比如 redux-form需要指定额外的 form reducer:
import { reducer as formReducer } from 'redux-form'
const app = dva({
extraReducers: {
form: formReducer,
},
});
extraEnhancers
指定额外的 StoreEnhancer,比如结合 redux-persist的使用:
import { persistStore, autoRehydrate } from 'redux-persist';
const app = dva({
extraEnhancers: [autoRehydrate()],
});
persistStore(app._store);
欢迎访问我的个人网站:www.dengzhanyong.com
喜欢的话可以关注我的公众号:【前端筱园】
一起交流,共同成长