dva 有着阿里巴巴的金字招牌,使用者不乏。下份工做必须上手 dva 了,因而乎做为一个以前主要使用 redux + thunk + promise-middleware 的用户,开始了探究之旅。css
我的认为 dva 想作的事情太多了。dva 涵盖了 redux, react-redux, redux-saga, react-router, react-router-redux, isomorphic-fetch。巴不得一个 import dva from 'dva'
就解决全部事。其中最奇怪的是涵盖 react-router
这个决定,由于以我目前看来,dva 并无任何对 react-router
的“改进”,只是原本来本使用了它,既然如此,何须要包含它呢?html
不过优势是,虽然涵盖了不少库,dva 只是很薄的一层,因此哪怕文档没有写,redux,saga 或者是 router 的用法都是能够照搬,学习曲线很平。node
已经了解 redux 的使用,但还未深刻接触 dva 的各位。dva 的文档说实在只能打个 80 分,提供的实例除了最简单单文件 counter 实例外,就是一堆直接整合 umi 的大项目。我的认为上手从 Account System 这个实例开始看比较好。不过这里仍是有个gap,因此本篇尝试填补一下。react
以实战的角度讨论如何从 redux 快速转型 dva,同时比较二者使用感触上的不一样。git
将本身以前写的 todo-list redux 最佳实践 (多文件)改写成 dva 项目。github
dva 是一个试图简化 React 开发流程,特别是 redux 状态管理流程的轻框架。npm
文档的第一个例子, 熟悉的counter: json
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");
复制代码
history
props, 通常在这里写路由布局,没有特别规范,只要返回的是组件就行要写一个点 “+”、“-” 增减任意指定数目的 counter 该如何改?redux
// 首先是 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
api
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');
复制代码
router.js
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 redux 最佳实践 练个手。看看如何将一个纯 redux 项目快速改形成 dva 项目。并尝试分析一下其中产生的好处(和坏处?)。你们能够先试试手。我本身写下来,开始感受最大的思考点是“选择器”,不事后来发现这彻底不是问题。
我写的dva实现:todo-list demo
大致思路:
index.js
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
里。
从开始使用时,这就是我最大的关注点,不过看完全部的 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
的部分。目前看来
(虽然分了三点说,但差很少是一个事儿)
彷佛没法很好的分解超过两层的复杂状态?(求解)
下一篇,探究 dva 异步的 api 。
个人其余文章列表:传送门