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 。
個人其餘文章列表:傳送門