dva 有着阿里巴巴的金字招牌,使用者不乏。下份工作必须上手 dva 了,于是乎作为一个之前主要使用 redux + thunk + promise-middleware 的用户,开始了探究之旅。
吐槽
-
个人认为 dva 想做的事情太多了。dva 涵盖了 redux, react-redux, redux-saga, react-router, react-router-redux, isomorphic-fetch。恨不得一个
import dva from 'dva'
就解决所有事。其中最奇怪的是涵盖react-router
这个决定,因为以我目前看来,dva 并没有任何对react-router
的“改进”,只是原原本本使用了它,既然如此,何必要包含它呢? -
不过优点是,虽然涵盖了很多库,dva 只是很薄的一层,所以哪怕文档没有写,redux,saga 或者是 router 的用法都是可以照搬,学习曲线很平。
本文适合
已经了解 redux 的使用,但还未深入接触 dva 的各位。dva 的文档说实在只能打个 80 分,提供的实例除了最简单单文件 counter 实例外,就是一堆直接整合 umi 的大项目。个人认为上手从 Account System 这个实例开始看比较好。不过这里还是有个gap,所以本篇尝试填补一下。
以实战的角度讨论如何从 redux 快速转型 dva,同时比较两者使用感触上的不同。
第一天的课题
将自己之前写的 todo-list redux 最佳实践 (多文件)改写成 dva 项目。

dva 是什么?
dva 是一个试图简化 React 开发流程,特别是 redux 状态管理流程的轻框架。
从 Counter 了解 api
文档的第一个例子, 熟悉的counter:

import React from "react";
import dva, { connect } from "dva";
// 1. 生成app实例
const app = dva();
// 2. Model 模型
const counter = {
namespace: "count",
state: 0,
reducers: {
add(state, action) {
return state + 1;
},
minus(state, action) {
return state - 1;
}
}
}
app.model(counter);
// 3. UI. 注意 model根据 namespace 和 reducer 自动生成的 type
const App = props => {
console.log(props);
return (
<div>
<h2>{props.count}</h2>
<button
onClick={() => {
props.dispatch({ type: "count/add" });
}}
>
+
</button>
<button
onClick={() => {
props.dispatch({ type: "count/minus" });
}}
>
-
</button>
</div>
);
};
// 4. react-redux 的 connect
const EnhancedApp = connect(({ count }) => ({ count }))(App);
// 5. Router. 提供 history props 给 Router 组件,这里不需要所以照常写
app.router(({ history }) => <EnhancedApp />);
// 6. 运行app, 并挂到 id 为 root 的 div 上(类似于 reactDOM.render)
app.start("#root");
概述
- 1 和 6 是必写模板
- 2 就是写 reducer,同时自动生成了 action 的 type
- 3,4 是 UI,connect 使用方法完全相同
- 5 是根组件,提供
history
props, 一般在这里写路由布局,没有特别规范,只要返回的是组件就行
亮点
- 自动生成了 action 的 type
- 简化了 reducer 的写法
带 payload 的 action 怎么写?
要写一个点 “+”、“-” 增减任意指定数目的 counter 该如何改?
// 首先是 reducer
reducers: {
add(state, action ) {
return state + action.payload;
},
// 也可以再改进,当payload不被指定时,默认1
minus(state, { payload = 1 }) {
return state - payload;
}
}
// 其次是 action
<button
onClick={() => {
props.dispatch({ type: "count/add", payload: 2 });
}}
>
从 dva-cli
学习写项目的基本结构
了解了基本用法,下面探索写项目时如何合理地布局项目结构。
dva 有类似 create-react-app
的脚手架 dva-cli
npm i -g dva-cli
## 建立名为 my-first-dva 的项目
dva new my-first-dva
- 项目结构如下
my-first-dva
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── mock
├── public
└── src
├── assets ## 资源
├── components ## 纯组件
├── models ## 模型
├── routes ## 页面组件
├── index.js ## 起始点
└── router.js ## 路由
- 从入口文件
index.js
看起
import dva from 'dva'
import './index.css'
// 1. 初始
const app = dva()
// 2. 插件,如果有的话使用
// app.use({});
// 3. 模型
app.model(require('./models/example').default)
// 4. 路由
app.router(require('./router').default)
// 5. 启动
app.start('#root')
大致上把model和router部分拆分出去是基本做法。
为啥 index.js
里莫名其妙地使用 require 语法,是个迷。尝试了下,使用正常的 import 语法是没问题的:
import dva from 'dva'
import './index.css'
import routes from './router'
import example from './models/example'
// 1. Initialize
const app = dva()
// 2. Plugins
// app.use({});
// 3. Model
app.model(example)
// 4. Router
app.router(routes)
// 5. Start
app.start('#root')
那么,model不止一个咋办?很简单,多次使用 app.model()
即可。 例如,推荐demo Account System 的 index.js
就如下
import './index.html';
import './index.less';
import dva from 'dva';
import {browserHistory} from 'dva/router';
import router from './router';
import home from './models/home';
import orders from './models/orders';
import storage from './models/storage';
import manage from './models/manage';
import systemUser from './models/systemUser';
import customers from './models/customers';
import products from './models/products';
import suppliers from './models/suppliers';
import settlement from './models/settlement';
import resource from './models/resource';
import customerBills from './models/customerBills';
import supplierBills from './models/supplierBills';
// 1. Initialize
const app = dva({
history: browserHistory
});
// 2. Plugins
//app.use({});
// 3. Model
app.model(home);
app.model(orders);
app.model(storage);
app.model(manage);
app.model(systemUser);
app.model(customers);
app.model(products);
app.model(suppliers);
app.model(settlement);
app.model(resource);
app.model(customerBills);
app.model(supplierBills);
// 4. Router
app.router(router);
// 5. Start
app.start('#root');
- UI组件结构
- 一个总路由文件
router.js
- 每个页面一个组件,放置在 routes 文件夹下
- 复用的UI组件放置在 components 文件夹下
router.js ---> routes 组件 ----> components组件
UI 的大致结构如上。
// router.js
// react-router 怎么写,这儿就咋写
import React from 'react'
import { Router, Route, Switch } from 'dva/router'
import IndexPage from './routes/IndexPage'
const RouterConfig = ({ history }) => (
<Router history={history}>
<Switch>
<Route path="/" exact component={IndexPage} />
</Switch>
</Router>
)
export default RouterConfig
至此,一个正常 dva 项目如何扩展大家应该有个概念。
试写 todo-list
接着用 todo-list redux 最佳实践 练个手。看看如何将一个纯 redux 项目快速改造成 dva 项目。并尝试分析一下其中产生的好处(和坏处?)。大家可以先试试手。我自己写下来,开始感觉最大的思考点是“选择器”,不过后来发现这完全不是问题。
我写的dva实现:todo-list demo
大体思路:
- 改写入口文件
index.js
- 将reducers 改写成 models
- 改写UI组件。由于并不是多页面路由,所以所有的组件都放在了原本的components文件夹下
- 处理其他细节,比如选择器等
1. 如何将一个reducer改成model?
直接上代码了
// redux 添加和toggle一个todo
let nextId = 4;
const todos = (state = [], action) => {
switch (action.type) {
case "ADD_TODO":
return [
...state,
{
id: nextId++,
detail: action.payload.detail,
completed: false
}
];
case "TOGGLE_TODO":
return state.map(t => {
if (t.id === action.payload.id) {
return { ...t, completed: !t.completed };
}
return t;
});
default:
return state;
}
};
export default todos;
改写成
let nextId = 4;
export default {
namespace: "todos",
// redux例子里原本createStore的initialState也直接放进来了。
state: [
{ id: 1, detail: "学习graphQL", completed: false },
{ id: 2, detail: "写博客", completed: false },
{ id: 3, detail: "本周的西部世界", completed: true }
],
// 因为namespace,reducer命名可以更加简洁
reducers: {
add(state, action) {
return [
...state,
{
id: nextId++,
detail: action.payload.detail,
completed: false
}
];
},
toggle(state, action) {
return state.map(todo => {
if (todo.id === action.payload.id) {
return { ...todo, completed: !todo.completed };
}
return todo;
});
}
}
};
2. 如何在UI组件里使用actions?
方案一:上面的 todos
model 对应的 UI 是<List />
组件, 用于展示todo的列表。由于 dva 中 action type 已在书写 model 时自动定义,这里只需要直接使用:
//List.js
import React from "react";
import { connect } from "dva";
import { getFilteredTodos } from "../models";
const List = ({ filteredTodos, dispatch }) => {
// 直接 dispatch action ////////////////
const handleClick = id => {
dispatch({
type: "todos/toggle",
payload: { id }
});
};
//////////////////////////////////////
return (
<ul className="list pl0 pv5">
{filteredTodos.map((t, index) => (
<li
key={t.id}
onClick={() => handleClick(t.id)}
>
{t.completed && <span>✔️ </span>}
{t.detail}
</li>
))}
</ul>
);
};
const mapStateToProps = state => ({ filteredTodos: getFilteredTodos(state) });
const ConnectedList = connect(mapStateToProps)(List);
export default ConnectedList;
方案二:事实上,dva 的 connect 与 react-redux的相同,还可以接收第二个参数 mapDispatchToProps
, 所以另一种使用方式是将handleClick
内dispatch action的部分转移至 connect 内,并利用到 react-redux
的语法糖简写:
import React from "react";
import { connect } from "dva";
import { getFilteredTodos } from "../models";
const List = ({ filteredTodos, toggle }) => (
<ul className="list pl0 pv5">
{filteredTodos.map((t, index) => (
<li
key={t.id}
onClick={() => toggle(t.id)}
>
{t.completed && <span>✔️ </span>}
{t.detail}
</li>
))}
</ul>
);
const ConnectedList = connect(
state => ({ filteredTodos: getFilteredTodos(state) }),
{
toggle: id => ({
type: "todos/toggle",
payload: { id }
})
}
)(List);
export default ConnectedList;
方案三:当然如果将所有的 actionCreator 写在一个文件中,在我看来也不错。
3. 选择器的书写
List.js
里,用到了一个选择器 getFilteredTodos(state)
, 功能是通过 todos 和 filter 来计算此时页面所应该显示的是哪些 todo (例如点击“未完成”,就该只显示未完成的todos)。
写这个demo时,突然意识到选择器只是一个普通的 js 函数,所以在 dva 里照旧正常使用,不需任何修改。我的做法如大家所见,将所有选择器放在 model/index.js
里。
如何拆分非常复杂的reducers
从开始使用时,这就是我最大的关注点,不过看完所有的 demo,似乎并没有得到解答( 很惊讶,大家似乎都觉得两层够用了 )。简单的说,dva 的模型是两层结构,一个总的 model 由很多第二层的小 model 通过 app.model()
的方式聚合组成。但如果需要第三层呢?这方面 redux 使用 combineReducers()
是不受限制的,reducer 套 reducer 可以无限套下去。但目前我没想到啥简单的 dva 处理方式。用代码叙述一下这个问题:
// redux 中, 如上例的todos reducer,如果还需要添加一个 isFetching 状态,那么
import { combineReducers } from 'redux'
const todos = (state = [], action) => { ... }
// 添加新的reducer
const isFetching = (state = false, action) => {
switch(action.type){
case "FETCH":
return !state
default:
return state
}
}
// 合并
export default combineReducers({ todos, isFetching })
在 redux 里很简单的实现,但在 dva 里:
const todos = {
namespace: "todos",
state: [ ... ],
reducers: { ... }
};
// 添加新的 model
const isFetching = {
namespace: "todos",
state: false,
reducers: {
fetch (state, action) {
return !state
}
}
};
// 如何合并两个model成为一个呢?自己写一个 combineModels() 吗 ?
这个问题我没想到怎么办,希望各位大神帮忙解答!
结语
dva 的上手篇,还没涉及到异步以及redux-saga
的部分。目前看来
优点
- 之前写大项目最头痛的 action namespace 的问题在此漂亮的解决了
- 同时 action type 的生成也是自动的
- reducer 的书写简洁了不少
(虽然分了三点说,但差不多是一个事儿)
缺点
似乎无法很好的分解超过两层的复杂状态?(求解)
下一篇,探究 dva 异步的 api 。
我的其他文章列表:传送门