dva 是基于 redux、redux-saga 和 react-router 的轻量级前端框架,概念来自 elm,支持 side effects、热替换、动态加载、react-native、SSR 等,已在生产环境广泛应用。
最早了解到 dva 是在使用蚂蚁金服出品的 antd 组件库时,官方文档上介绍了如何使用 dva 和 antd 创建一个应用。不过当时并没有尝试使用 dav,而是在 create-react-app
中使用了 antd 组件库。
年前克隆了 ant-design-pro 的代码,想要学习一下它的应用架构。ant-design-pro 基于 ES2015+、React、dva、g2 和 antd,虽说学习应用架构并不是特别需要了解底层技术,但却激发我对 dva 的学习欲望。
在学习过程中,我首先阅读了 README 中提及到了基本概念和 API,然后跟着教程做了一个 demo。关于 dva 的基本概念、API 等这里就不介绍了,看官方的文档会更好,我主要记录下我在跟着教程完成 demo 中遇到的问题和个人的总结。
预备知识:redux-saga
dva 基于 redux、redux-saga 和 react-router,redux 和 react-router 我有一定的了解,但是之前 redux 异步操作都是通过 redux-thunk 来完成,因此对于 redux-sage 不是很清楚,所以先花了一些时间简单学习一下 redux-saga。
redux-saga 是一个用于管理 Redux 应用异步操作(又称异步 action)的中间件。 redux-saga 通过创建 Sagas 将所有的异步操作逻辑收集在一个地方集中处理,可以用来代替 redux-thunk 中间件。其中,Sagas 是通过 Generator 函数来创建的,它监听发起的 action,然后决定基于这个 action 来做什么:是发起一个异步调用(比如一个 Ajax 请求),还是发起其他的 action 到 Store,甚至是调用其他的 Sagas。
在具体用法上,我理解 redux-saga 主要使用四个 API:
- takeEvery:监听 action,并调用对应的函数;
- takeLast:当有多个 action 触发时,只处理最后一个;
- call:执行异步操作;
- put:发起 action。
一个比较完成的 saga.js
如下:
import { takeEvery, takeLatest } from 'redux-saga'
import { call, put } from 'redux-saga/effects'
import Api from '...'
function* fetchUser(action) {
try {
const user = yield call(Api.fetchUser, action.payload.userId);
yield put({type: "USER_FETCH_SUCCEEDED", user: user});
} catch (e) {
yield put({type: "USER_FETCH_FAILED", message: e.message});
}
}
function* mySaga() {
yield* takeLatest("USER_FETCH_REQUESTED", fetchUser);
}
更详细的介绍可以参考此文档。
初始化应用
dva 为我们提供了命令行工具,可以方便的构建应用,我使用的是最新版的 dva2.1.0 和 antd3.2.0。
首先通过 npm i dva-cli -g
命令全局安装 dva 命令行工具,该工具提供了三个命令:
- init:在当前文件夹下初始化应用。
- new:创建一个新的应用。
- generate(简写 g):生成代码,如:model、route。不过由于时间原因,当前版本(0.9.2)并不支持。
命令行工具安装完成后,直接使用 dva new <project-name>
创建应用,应用的目录结构如下所示:
.
├── mock # 模拟的数据文件
├── node_modules # 项目依赖包(执行 npm install 下载 )
├── public # 公共资源目录,构建时会被自动复制到输出目录
│ └── index.html # 入口 html
├── src # 源码目录
│ ├── assets # 静态资源
│ ├── components # 组件
│ ├── models # 数据模型
│ ├── routes # 路由页面
│ ├── services # 服务,主要供 models 使用,提供一些 API 等
│ ├── utils # 存放各种工具方法,如:封装的 request 模块
│ ├── index.css # 项目主 css
│ ├── index.js # 入口文件
│ └── router.js # 路由
├── .editorconfig # 编辑器配置
├── .eslintrc # eslint 配置文件
├── .gitignore # 配置 git 忽略的目录和文件
├── .roadhogrc.mock.js # mock 数据配置
├── .webpackrc # webpack 配置
└── package.json # 项目配置文件
使用命令行工具创建完项目之后,最好先了解一下他的 配置方式。
接着我们配置一下 antd 和 babel-plugin-import,其中 babel-plugin-import 用于按需引入 antd 的 JavaScript 和 CSS,这样打包出来的文件不至于太大。执行下面两个命令安装:
$ npm i -S antd
$ npm i -D babel-plugin-import
安装完成后编辑 .webpackrc
,添加如下配置:
{
"extraBabelPlugins": [
[
"import",
{
"libraryName": "antd",
"libraryDirectory": "es",
"style": "css"
}
]
]
}
最后,我们配置一下代理,能通过 RESTFul 的方式访问 http://localhost:8000/api/users ,在 .webpackrc
里加上如下配置:
{
"proxy": {
"/api": {
"target": "http://jsonplaceholder.typicode.com/",
"changeOrigin": true,
"pathRewrite": {
"^/api": ""
}
}
}
}
完成了上述的工作后,我们就可以通过 npm start
命令启动应用。访问 http://localhost:8000/api/users ,就能访问到 http://jsonplaceholder.typicode.com/users 的数据。
项目开发
完成了项目创建和一些配置工作后,就可以正式进行项目开发了。开发过程大致可以分为四个步骤:
- 添加路由:可以通过
dva g route <route-name>
生成(目前不支持),这一命令完成了在routes
文件下创建对应的路由页面和 css 文件以及在routes.js
添加页面的配置的工作。 - 添加 model:可以通过
dva g model <model-name>
生成(目前不支持)。 - 添加 service:在
services
目录下添加对应 model 所需要的服务,例如:请求数据。 - 添加界面:可以通过
dva g component <path/name>
生成组件(目前不支持)。
路由、service 以及组件的编写这里就不介绍了,就是正常的 React 应用开发模式,这里主要说明一下 model,一个 model 文件大概长这样:
export default {
namespace: 'example',
state: {},
subscriptions: {
setup ({dispatch, history}) {
}
},
effects: {
* fetch ({payload}, {call, put}) {
yield put({type: 'save'})
},
takeLatest: [function * ({payload}, {call, put}) {
}, {type: 'takeLatest'}]
},
reducers: {
save (state, action) {
return {...state, ...action.payload}
}
}
}
可以看到,model 包含了五个属性,各个属性的含义如下:
- namespace:命名空间,同时也是他在全局 state 上的属性名。
- state:初始 state。
- subscriptions:订阅数据源,然后根据需要 dispatch 相应的 action。
- effects:用于处理异步操作和业务逻辑,不直接修改 state。可以指定 type,写法参考上文 takeLatest
- reducers:同 redux 中的 redur 一样,用于处理同步操作,唯一可以修改 state 的地方。
model 创建好后,需要在 src/index.js
中注册,如下所示:
app.model(require('./models/example').default);
插件
dva 有一个管理 effects 执行的 hook,并基于此封装了 dva-loading 插件。通过这个插件,我们可以不必一遍遍地写 showLoading 和 hideLoading,当发起请求时,插件会自动设置数据里的 loading 状态为 true 或 false 。然后我们在渲染 components 时绑定并根据这个数据进行渲染。
先安装 dva-loading :
$ npm i -S dva-loading
修改 src/index.js
加载插件,在合适的地方加入下面两句:
+ import createLoading from 'dva-loading';
+ app.use(createLoading());
然后在组件中里绑定 loading 数据:
+ loading: state.loading.models.users
这样当发起请求时,组件 loading 属性就会发生相应的变化。除此之外,dva 还包含其他的 hooks,可以用来实现相应的功能。
总结
dva 确实如官网介绍所说,十分的轻量、易学易用,花 20 分钟不到的时间就能入门。它只是基于现有的 React 应用架构进行了封装,没有引入什么新的概念。它明确的说明了每个部件应该如何写,减少了以必要的纠结。同时,提供了 app.model
方法,把 reducer, initialState, action, saga 封装到了一起,避免我们在多个文件中来回切换。
在学习 dva 的基本用法的过程中,我还了解到了一些其他知识:
- HTTP 请求中 PATCH 方法,用于更新资源的部分内容。
- 使用 target="_blank" 时不添加 rel="noopener noreferrer" 会导致安全问题