一、開發(fā)環(huán)境
首先,請安裝 NodeJS。NodeJS 是一個 JS 執(zhí)行環(huán)境,umi 基于 JS 編寫,并且需要在你的開發(fā)機上運行,所以依賴于它。
安裝完成后,執(zhí)行下面的命令確認是否安裝成功。
node -v
npm -v
在 umi 中我們采用了一些 NodeJS 的新特性,請確保你的 NodeJS 版本大于等于 8.5.0。
在國內(nèi),你可以安裝cnpm獲得更快速、更安全的包管理體驗。使用如下命令安裝:
npm install -g cnpm --registry=https://registry.npm.taobao.org
然后你可以通過如下的命令確認是否成功:
cnpm -v
通過 cnpm 你可以很方便的安裝和管理一些第三方的包。比如 umi 就將通過它來安裝到你的項目。
二、安裝 umi 依賴
首先,新建一個空的文件夾,用來存放本課程后續(xù)所有的代碼。
mkdir antd-course
cd antd-course
然后,調(diào)用 cnpm init 來初始化 package.json,它是 NodeJS 約定的用來存放項目的信息和配置等信息的文件。
cnpm init -y
上面命令中,參數(shù) -y 表示對 npm 要求提供的信息,都自動按下回車鍵,表示接受默認值。
接著,安裝 umi 的依賴。
cnpm install umi --save-dev
安裝完成之后你會發(fā)現(xiàn) package.json 中多出了一項 devDependencies 的配置。這是由于在上面命令中,參數(shù) --save 可以讓依賴信息保存到 package.json 中,這樣其它開發(fā)者下載代碼后就只需要執(zhí)行 cnpm install 后就會自動安裝項目依賴的包。
另外項目中還多出了一個 node_modules 的文件夾,它包含了大量的內(nèi)容。里面存放的是項目依賴的包,umi 的代碼也是被下載并安裝到其中的。如果你想要了解更多,可以參考 npm 的文檔。
三、初始化 umi 的配置
接下來,讓我們創(chuàng)建我們的第一個頁面。
在創(chuàng)建第一個頁面之前,我們需要先初始化 umi 的配置。
在 umi 中,大量的使用了配置和約定來幫助你快速開發(fā)代碼。
1.首先,我們先來創(chuàng)建配置文件,配置文件被約定為 config/config.js。

- 為了讓后面的開發(fā)更加高效,我們推薦你下載一款適合你的編輯器或者 IDE 來創(chuàng)建和編寫代碼。本課程中我們推薦你使用 VS Code。
- 在 umi 中,你也可以簡單的使用
.umirc.js來作為配置文件。當然它和config/config.js是二選一的。更多說明可以參考 umi 的文檔。 -
config/config.js中初始化的內(nèi)容如下:
export default {};
一開始它只是 export 了一個默認的空的對象 {},并沒有什么作用,但是在后面我們會用到。
我們所說的頁面是指由一個獨立路由對應的 UI 界面,關于路由你后續(xù)可以在《路由配置》中了解更多。在這一章節(jié)中你只需要知道對于這個例子,就是指我們期望通過瀏覽器訪問 /helloworld 的時候可以得到一個顯示 hello world 的頁面。
2.我們新建一個 src 目錄,它用來存放項目的除了配置以及單測以外的主要代碼。
在 umi 中,約定的存放頁面代碼的文件夾是 pages,是復數(shù),不過如果你喜歡單數(shù)的話你配置項中你可以添加 singular 為 true 來讓 page 變?yōu)榧s定的文件夾。在本課程中我們使用單數(shù)來作為約定目錄。所以你需要修改配置文件為:
export default {
singular: true,
}
接下來讓我們創(chuàng)建第一個頁面組件,新建 src/page/HelloWorld.js 文件,代碼如下:
export default () => {
return <div>hello world</div>;
}
這樣第一個頁面就創(chuàng)建完成了,代碼的具體含義我們會在后面的章節(jié)介紹。接下來你就可以通過 umi 來啟動你的代碼了。首先你需要在 package.json 中的 scripts 里面添加兩個命令:
{
"scripts": {
+ "dev": "umi dev",
+ "build": "umi build"
}
}
scripts 中定義的命令,可以在項目根目錄中通過 cnpm run [scriptname] 來運行。接下來請執(zhí)行:
cnpm run dev
修改 package.json 的時候要注意它是一個標準的 JSON 格式的文件,如果失敗請檢查是不是逗號或者引號的問題。如果順利,項目將會運行起來。你將會在命令行中看到如下的日志:

復制日志中的地址,比如 http://localhost:8000/(這里的端口可能會因為被占用或者其他原因而不同,請參考你的機器中實際打印出來的地址)。并在后面加上 helloworld 的路徑后(比如 http://localhost:8000/helloworld)在瀏覽器中打開,然后你將會看到:

在 umi 中,你可以使用約定式的路由,在 page 下面的 JS 文件都會按照文件名映射到一個路由,比如上面這個例子,訪問 /helloworld 會對應到 HelloWorld.js。
除了約定式的路由,你也可以使用配置式的路由。至于使用哪種路由取決于你的喜好,這不是本課程的重點。在本課程中為了讓開發(fā)者更好的理解路由組件嵌套,我們會使用配置式的路由。
要使用配置式的路由,你需要在配置文件 config/config.js 中添加如下配置:
export default {
routes: [{
path: '/',
component: './HelloWorld',
}],
}
其中 component 是一個字符串,它是相對于 page 目錄的相對路徑。在上面的配置中我們將路由的路徑配置成為了 /,這樣你訪問 http://localhost:8000/ 首頁就能看到 hello world 了。
當有了 routes 的配置之后 umi 就不會再執(zhí)行約定式對應的路由邏輯了,而是直接使用你通過配置聲明的路由。關于路由的更多信息你可以參考《路由配置》這一章節(jié)的說明。
添加 umi-plugin-react 插件
umi 是一個可插拔的企業(yè)級 react 應用框架,它的很多功能都是通過插件實現(xiàn)。尤其是 umi 官方的 umi-plugin-react 這個插件集成了常用的一些進階的功能,為了后面的課程需要,我們需要添加該插件集到項目中。
首先通過 cnpm install umi-plugin-react --save-dev 來安裝該插件集。然后在配置文件 config/config.js 中引入該插件:
export default {
plugins: [
['umi-plugin-react', {
// 這里暫時還沒有添加配置,該插件還不會有作用,我們會在后面的課程按照需求打開相應的配置
}],
],
routes: [{
path: '/',
component: './HelloWorld',
}],
}
.gitignore
cnpm 安裝的依賴會被默認安裝到項目的 node_modules 目錄下。這個目錄通常是不需要提交到代碼倉庫中的。如果你使用的是 git 來作為代碼的管理工具,那么你可以添加 .gitignore 文件到項目根目錄中,避免將不必要的代碼提交到 git 倉庫中。
.gitignore 如下:
node_modules
dist
.umi
其中 .umi 是 umi 在開發(fā)過程中產(chǎn)生的臨時入口文件,便于開發(fā)調(diào)試,同樣也不需要提交到代碼倉庫中。dist 是構建出來的產(chǎn)物,通常也不需要提交。
我們建議你可以在本地通過 git 管理起你的代碼,方便在后面的課程中更好的操作你的代碼。
git init
git add -A
git commit -m 'init'
構建和部署
你可以通過 cnpm run build 來構建出最終的產(chǎn)物,執(zhí)行該命令后會生成最終的 HTML、CSS 和 JS 到 dist 目錄下。它們是瀏覽器可以直接識別并運行的代碼,這樣你就可以將它們部署到你想要的服務器上了。
三、使用 Ant Design 組件
1.引入 antd
Ant Design 是一個服務于企業(yè)級產(chǎn)品的設計體系,組件庫是它的 React 實現(xiàn),antd 被發(fā)布為一個 npm 包方便開發(fā)者安裝并使用。
在 umi 中,你可以通過在插件集 umi-plugin-react 中配置 antd 打開 antd 插件,antd 插件會幫你引入 antd 并實現(xiàn)按需編譯。
在目錄 config/config.js 下:
export default {
plugins: [
['umi-plugin-react', {
antd: true
}],
],
// ...
}
如果使用我們的腳手架,Ant Design 已經(jīng)自帶了,否則你需要自己安裝。
# 腳手架所在的目錄
$ cnpm install --save antd
四、基本布局
1.第一步:添加基本布局
在 src 目錄下創(chuàng)建 layout 文件目錄,然后創(chuàng)建 index.js 文件,在 index.js 中我們寫入:
import { Component } from 'react';
import { Layout } from 'antd';
// Header, Footer, Sider, Content組件在Layout組件模塊下
const { Header, Footer, Sider, Content } = Layout;
class BasicLayout extends Component {
render() {
return (
<Layout>
<Sider>Sider</Sider>
<Layout>
<Header>Header</Header>
<Content>Content</Content>
<Footer>Footer</Footer>
</Layout>
</Layout>
)
}
}
export default BasicLayout;
上面代碼中,我們創(chuàng)建了一個三部分的基本布局:Header 、Content 、Footer。然后我們將 Content 替換成 { this.props.children },這樣之后我們設置的路由會通過替換 children 變量實現(xiàn)內(nèi)容的切換。
<Content>{ this.props.children }</Content>
2.第二步:添加樣式
上面我們定義了導航的結構,下面我們添加一些樣式,讓這個布局看上去更美觀一些:
import { Component } from 'react';
import { Layout } from 'antd';
const { Header, Footer, Sider, Content } = Layout;
export default class BasicLayout extends Component {
render() {
return (
<Layout>
<Sider width={256} style={{ minHeight: '100vh', color: 'white' }}>
Sider
</Sider>
<Layout >
<Header style={{ background: '#fff', textAlign: 'center', padding: 0 }}>Header</Header>
<Content style={{ margin: '24px 16px 0' }}>
<div style={{ padding: 24, background: '#fff', minHeight: 360 }}>
{this.props.children}
</div>
</Content>
<Footer style={{ textAlign: 'center' }}>Ant Design ?2018 Created by Ant UED</Footer>
</Layout>
</Layout>
)
}
}
五、配置路由
定義好容器組件之后,我們就可以通過路由配置的方式把路由對應的組件渲染到容器組件中去了,具體路由如何配置我們放在下面的章節(jié)中講述。這里我們直接在 config/config.js 添加路由配置:
routes: [{
path: '/',
component: '../layout',
routes: [
{
path: 'helloworld',
component: './HelloWorld'
},
]
}],
基本
exports.routes 需要是一個數(shù)組,數(shù)組中的每一個對象是一個路由信息,比如:
exports.routes = [
{
path: '/',
component: 'App',
},
{
path: '/user',
component: 'User',
}
];
其中,path 表示頁面訪問路徑,component 表示 page 下的文件名,比如 App, User 分別表示 page/App,page/User。這樣,訪問 http://localhost:7001/ 和 http://localhost:7001/user 則會有展示 App, User 中的內(nèi)容。
routes
當需要有一個 layout 作為展示,可以設置 routes:
exports.routes = [
{
path: '/',
component: 'App',
routes: [{
path: 'list'
component: 'List',
}, {
path: 'admin'
component: 'Admin',
}]
},
{
path: '/user',
component: 'User',
}
];
在 page/App 中:
export default (props) => <div style={{padding: 20}}>
{this.props.children}
</div>
這樣訪問 /list 跟 /admin 將會都有這個 layout。
更多配置信息和路由使用方式請參考 umi 官方文檔。
六、使用 model
軟件分層
首先,我們需要簡單了解一下軟件架構的「分層」理念。
一個完整的軟件,往往會被拆分成多個不同的層次,每一個層次聚焦于完成特定的功能。

上圖中,左側(cè)是服務端代碼的層次結構,由 Controller、Service、Data Access 三層組成服務端系統(tǒng):
- Controller 層負責與用戶直接打交道,渲染頁面、提供接口等,側(cè)重于
展示型邏輯。 - Service 層負責處理業(yè)務邏輯,供 Controller 層調(diào)用。
- Data Access 層顧名思義,負責與數(shù)據(jù)源對接,進行純粹的數(shù)據(jù)讀寫,供 Service 層調(diào)用。
上圖的右側(cè)是前端代碼的結構,同樣需要進行必要的分層:
- Page 負責與用戶直接打交道:渲染頁面、接受用戶的操作輸入,側(cè)重于
展示型交互性邏輯。 - Model 負責處理業(yè)務邏輯,為 Page 做數(shù)據(jù)、狀態(tài)的讀寫、變換、暫存等。
- Service 負責與 HTTP 接口對接,進行純粹的數(shù)據(jù)讀寫。
引入 DVA
在 umi 中,你可以通過在插件集 umi-plugin-react 中配置 dva 打開 dva 插件。
export default {
plugins: [
['umi-plugin-react', {
antd: true,
dva: true,
}],
],
// ...
}
Model 是前端分層中的腰部力量,承上啟下,負責管理數(shù)據(jù)(狀態(tài))。業(yè)界主流的狀態(tài)管理類庫有 redux、mobx,等。在我們的教程中,則使用 DVA 框架承擔這一角色。
DVA 是基于 redux、redux-saga 和 react-router 的輕量級前端框架及最佳實踐沉淀。其中,model 是 DVA 中最重要的概念,一個簡單的 model 示例如下:
app.model({
namespace: 'todoList',
state: [],
effects: {
*query({ _ }, { put, call }) {
const rsp = yield call(queryTodoListFromServer);
const todoList = rsp.data;
yield put({ type: 'save', payload: todoList });
},
},
reducers: {
save(state, { payload: todoList }) {
return [...state, todoList];
},
},
});
DVA 的 model 對象有幾個基本的屬性,需要大家了解。
-
namespace:model 的命名空間,只能用字符串。一個大型應用可能包含多個 model,通過namespace區(qū)分。 -
state:當前 model 狀態(tài)的初始值,表示當前狀態(tài)。 -
reducers:用于處理同步操作,可以修改state,由action觸發(fā)。reducer 是一個純函數(shù),它接受當前的 state 及一個數(shù)據(jù)體(payload)作為入?yún)?,返回一個新的 state。 -
effects:用于處理異步操作(例如:與服務端交互)和業(yè)務邏輯,也是由 action 觸發(fā)。但是,它不可以修改 state,要通過觸發(fā) action 調(diào)用 reducer 實現(xiàn)對 state 的間接操作。 -
action:是 reducers 及 effects 的觸發(fā)器,一般是一個對象,形如{ type: 'add', payload: todo },通過 type 屬性可以匹配到具體某個 reducer 或者 effect,payload 屬性則是數(shù)據(jù)體,用于傳送給 reducer 或 effect。
上面這些概念,初學者會覺得比較抽象,這是正常的。大家可以通過后面的例子,慢慢體會。DVA 的文檔非常優(yōu)秀,建議大家直接學習,參考鏈接:
最簡單的卡片列表頁
我們先顯示一個最簡單的卡片列表頁,只有卡片,不做添加操作。src/page 目錄下建立頁面文件 puzzlecards.js,并把它加入到路由。
首先,建立頁面文件。
import React, { Component } from 'react';
import { Card } from 'antd';
export default class PuzzleCardsPage extends Component {
constructor(props) {
super(props);
this.state = {
cardList: [
{
id: 1,
setup: 'Did you hear about the two silk worms in a race?',
punchline: 'It ended in a tie',
},
{
id: 2,
setup: 'What happens to a frog\'s car when it breaks down?',
punchline: 'It gets toad away',
},
],
}
}
render() {
return (
<div>
{
this.state.cardList.map(card => {
return (
<Card key={card.id}>
<div>Q: {card.setup}</div>
<div>
<strong>A: {card.punchline}</strong>
</div>
</Card>
);
})
}
</div>
);
}
}
其次,配置文件 config/config.js 內(nèi)增加一條路由規(guī)則。
export default {
routes: [
{
path: '/',
component: '../layout',
routes: [
+ { path: 'puzzlecards', component: './puzzlecards' },
]
}
],
};
啟動應用,看到如下頁面:

"添加卡片" 按鈕
?在上文的基礎上,我們添加一個按鈕,并在上面綁定一個處理點擊事件的回調(diào)函數(shù)。思路是在回調(diào)函數(shù)中向 cardList 中添加一個新卡片數(shù)據(jù)。
最終我們的頁面文件變成如下樣子:
import React, { Component } from 'react';
import { Card, Button } from 'antd';
export default class PuzzleCardsPage extends Component {
constructor(props) {
super(props);
this.counter = 100;
this.state = {
cardList: [
{
id: 1,
setup: 'Did you hear about the two silk worms in a race?',
punchline: 'It ended in a tie',
},
{
id: 2,
setup: 'What happens to a frog\'s car when it breaks down?',
punchline: 'It gets toad away',
},
],
}
}
addNewCard = () => {
this.setState(prevState => {
const prevCardList = prevState.cardList;
this.counter += 1;
const card = {
id: this.counter,
setup: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit,',
punchline: 'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
};
return {
cardList: prevCardList.concat(card),
};
});
}
render() {
return (
<div>
{
this.state.cardList.map(card => {
return (
<Card key={card.id}>
<div>Q: {card.setup}</div>
<div>
<strong>A: {card.punchline}</strong>
</div>
</Card>
);
})
}
<div>
<Button onClick={this.addNewCard}> 添加卡片 </Button>
</div>
</div>
);
}
}
雖然每次添加的卡片內(nèi)容都相同,但是不要緊,這里只是演示用法,但是注意唯獨 id 不能相同。為了產(chǎn)生唯一的 id,我們在組件中新加了一個 counter 成員,它只是為了產(chǎn)生唯一 id,并不是真的為了計數(shù),所以初始值不重要(我們這里給了 100)。
新的頁面如下:

到這里我們其實已經(jīng)完成了想要的頁面,也并沒有用 dva,那 dva 到底有什么用 ?
這里陳述幾個需求:
- 在實際的前端開發(fā)中,像 cardList 中包含的那些數(shù)據(jù),一般都是通過發(fā)起異步 http 請求從后端服務中獲得。
- 我們希望把數(shù)據(jù)邏輯(cardList 相關邏輯)和視圖邏輯(PuzzleCardsPage)分開管理在不同的模塊中,「關注分離」使得代碼更加健壯,同時易于調(diào)試。
- 我們希望這些數(shù)據(jù)在需要的時候,可以提供給不同的組件使用:也即數(shù)據(jù)共享。
而 dva 就是用來滿足這些需求的:
- 通過把狀態(tài)上提到 dva model 中,我們把數(shù)據(jù)邏輯從頁面中抽離出來。
- 通過 effect 優(yōu)雅地處理數(shù)據(jù)生成過程中的副作用,副作用中最常見的就是異步邏輯。
- dva model 中的數(shù)據(jù)可以注入給任意組件。
- 另外,dva 允許把數(shù)據(jù)邏輯再拆分(「頁面」常常就是分隔的標志),以 namespace 區(qū)分。當你覺得有必要時,不同的 namespace 之間的 state 是可以互相訪問的。
如果你熟悉 React 中最基本的兩個概念 props 和 state,一定知道 props 和 state 對于一個組件來講都是數(shù)據(jù)的來源,而 state 又可以通過 props 傳遞給子組件,這像是一個雞生蛋蛋生雞的問題:到底誰是數(shù)據(jù)的源頭 ?答案是 state,而且是廣義的 state:它可以是 react 組件樹中各級組件的 state,也可以是 react 組件樹外部由其他 js 數(shù)據(jù)結構表示的 state,而 dva 管理的就是 react 組件樹之外的 state: Redux。歸根結底,props 是用來傳導數(shù)據(jù)的,而 state 是數(shù)據(jù)改變的源泉。
基于 dva 的?簡單卡片列表頁:使用 connect 對接靜態(tài)的 dva model
如果你已經(jīng)對 React 開發(fā)比較熟悉,就會知道子組件的 state 可以上提 (state hoisting),由父組件來管理:
- 子組件間接回調(diào)到父組件的 setState 的方法來改變父組件的 state;
- 新的 state 通過 props 的形式把再次被子組件獲悉。
而 dva 可以幫助我們把 state 上提到 所有 React 組件之上,過程是相似的:
- 頁面通過調(diào)用 dispatch 函數(shù)來驅(qū)動 dva model state 的改變;
- 改變后的 dva model state通過 connect 方法注入頁面。
所謂「注入」從本質(zhì)上是 控制反轉(zhuǎn) 的一種實現(xiàn),這種思想在許多的語言框架中都有體現(xiàn),最著名的莫過于基于 Java 語言的 Spring。組件不再負責管理數(shù)據(jù),組件只是通過 connect 向 dva 聲明所需數(shù)據(jù)。
本節(jié)中我們只做狀態(tài)上提,我們只需要定義一個基本的 dva model 和使用 connect。首先,我們在 src/model 目錄下創(chuàng)建一個 dva model 文件:puzzlecards.js。
export default {
namespace: 'puzzlecards',
state: [
{ id: 1,
setup: 'Did you hear about the two silk worms in a race?',
punchline: 'It ended in a tie',
},
{
id: 2,
setup: 'What happens to a frog\'s car when it breaks down?',
punchline: 'It gets toad away',
},
],
};
其次,修改之前的頁面文件:
import React, { Component } from 'react';
import { Card /* ,Button */ } from 'antd';
import { connect } from 'dva';
const namespace = 'puzzlecards';
const mapStateToProps = (state) => {
const cardList = state[namespace];
return {
cardList,
};
};
@connect(mapStateToProps)
export default class PuzzleCardsPage extends Component {
render() {
return (
<div>
{
this.props.cardList.map(card => {
return (
<Card key={card.id}>
<div>Q: {card.setup}</div>
<div>
<strong>A: {card.punchline}</strong>
</div>
</Card>
);
})
}
{/* <div>
<Button onClick={this.addNewCard}> 添加卡片 </Button>
</div> */}
</div>
);
}
}
首先,注意 dva model 的定義。一個基本的 dva model 最少具備兩個成員:namespace 和 state。namespace 來作為一個 model 的唯一標識,state 中就是該 model 管理的數(shù)據(jù)。
其次,看頁面文件的變化:我們刪除了組件本身的 state,同時添加了 @connect(mapStateToProps)。connect 是連接 dva 和 React 兩個平行世界的關鍵,一定要理解。
- connect 讓組件獲取到兩樣東西:1. model 中的數(shù)據(jù);2. 驅(qū)動 model 改變的方法。
- connect 本質(zhì)上只是一個 javascript 函數(shù),通過
@裝飾器語法使用,放置在組件定義的上方; - connect 既然是函數(shù),就可以接受入?yún)?,第一個入?yún)⑹亲畛S玫模枰且粋€函數(shù),我們習慣給它命名叫做 mapStateToProps,顧名思義就是把 dva model 中的 state 通過組件的 props 注入給組件。通過實現(xiàn)這個函數(shù),我們就能實現(xiàn)把 dva model 的 state 注入給組件。
mapStateToProps 這個函數(shù)的入?yún)?state 其實是 dva 中所有 state 的總合。對于初學 js 的人可能會很疑惑:這個入?yún)⑹钦l給傳入的呢?其實你不用關心,你只需知道 dva 框架會適時調(diào)用 mapStateToProps,并傳入 dva model state 作為入?yún)?,我們再次提醒:傳入?state 是個 "完全體",包含了 所有 namespace 下的 state!我們自己定義的 dva model state 就是以 namespace 為 key 的 state 成員。所以 const namespace = 'puzzlecards' 中的 puzzlecards 必須和 model 中的定義完全一致。dva 期待 mapStateToProps 函數(shù)返回一個 對象,這個對象會被 dva 并入到 props 中,在上面的例子中我們?nèi)〉綌?shù)據(jù)后,把它改名為 cardList 并返回( 注意返回的不是 cardList 本身,而是一個包含了 cardList 的對象! ),cardList 就可以在子組件中通過 props 被訪問到了。
注意 render 函數(shù)中通過 this.props.cardList 取到了數(shù)據(jù),數(shù)據(jù)已經(jīng)不再由組件自己管理,我們得到了第一步中的頁面樣子:

這里我們同時利用 Redux DevTools 展示了 Dva 中 state 的內(nèi)容,證明了我們定義的 model 確實生效了。如果想時刻洞察 model 中的內(nèi)容,強烈建議安裝 Redux DevTools

"添加卡片" 按鈕:使用 dispatch 和 reducer 改變 dva model
我們上面的例子中只是移植了 state,但是沒有移植按鈕和按鈕上的行為。React 有一個基本的哲學:數(shù)據(jù)映射到視圖。無論什么途徑,我們點擊按鈕后,本質(zhì)上都是去觸發(fā) state 的改變,state 的改變再映射回視圖。所以我們這里的目標就是使得每次點擊按鈕,觸發(fā) dva model 的中卡片數(shù)據(jù)再添加一條。而在 dva 的語境中,是統(tǒng)一通過 dispatch 函數(shù)來做這件事情。
修改 model 文件,加入 reducers 部分:
export default {
namespace: 'puzzlecards',
state: {
data: [
{
id: 1,
setup: 'Did you hear about the two silk worms in a race?',
punchline: 'It ended in a tie',
},
{
id: 2,
setup: 'What happens to a frog\'s car when it breaks down?',
punchline: 'It gets toad away',
},
],
counter: 100,
},
reducers: {
addNewCard(state, { payload: newCard }) {
const nextCounter = state.counter + 1;
const newCardWithId = { ...newCard, id: nextCounter };
const nextData = state.data.concat(newCardWithId);
return {
data: nextData,
counter: nextCounter,
};
}
},
};
修改頁面文件,注入新的方法:
import React, { Component } from 'react';
import { Card ,Button } from 'antd';
import { connect } from 'dva';
const namespace = 'puzzlecards';
const mapStateToProps = (state) => {
const cardList = state[namespace].data;
return {
cardList,
};
};
const mapDispatchToProps = (dispatch) => {
return {
onClickAdd: (newCard) => {
const action = {
type: `${namespace}/addNewCard`,
payload: newCard,
};
dispatch(action);
},
};
};
@connect(mapStateToProps, mapDispatchToProps)
export default class PuzzleCardsPage extends Component {
render() {
return (
<div>
{
this.props.cardList.map(card => {
return (
<Card key={card.id}>
<div>Q: {card.setup}</div>
<div>
<strong>A: {card.punchline}</strong>
</div>
</Card>
);
})
}
<div>
<Button onClick={() => this.props.onClickAdd({
setup: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
punchline: 'here we use dva',
})}> 添加卡片 </Button>
</div>
</div>
);
}
}
于是得到新的頁面,

接下來我們解釋一下都干了什么事情。
使用 mapDispatchToProps 和 dispatch
通過使用這兩者,我們可以給組件注入方法,組件使用這些方法能給 dva model 發(fā)「消息」。this.props.onClickAdd 就是被注入的方法。
() => this.props.onClickAdd({
setup: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
punchline: 'here we use dva',
})
注意不要寫成
this.props.onClickAdd({
setup: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit',
punchline: 'here we use dva',
})
區(qū)別是上面定義了一個 click 事件的回調(diào)函數(shù),而下面是直接調(diào)用函數(shù)?;卣{(diào)函數(shù)在點擊時被調(diào)用,又立刻調(diào)用 onClickAdd。如果直接寫成 this.props.onClickAdd({}),就變成 render 函數(shù)執(zhí)行到此處時直接調(diào)用 onClickAdd 函數(shù)了。
onClickAdd 是怎么被注入的呢 ?答案就在于我們給 connect 傳入了第二個函數(shù):mapDispatchToProps。我們習慣用這個名字是因為它精煉地說明了這個函數(shù)的作用:以 dispatch 為入?yún)?,返回一個掛著函數(shù)的對象,這個對象上的函數(shù)會被 dva 并入 props,注入給組件使用。
我們在 onClickAdd 函數(shù)中調(diào)用 dispatch 派發(fā)了一個 action,action 包含 onClickAdd 傳遞過來的內(nèi)容 { setup, punchline } 作為 payload,action 的 type 是 puzzlecards/addNewCard。addNewCard 在這個例子中是 reducer 的名字,這個我們下面會講到。dispatch 函數(shù)就是和 dva model 打交道的唯一途徑。 dispatch 函數(shù)接受一個 對象 作為入?yún)?,在概念上我們稱它為 action,唯一強制要包含的是 type 字段,string 類型,用來告訴 dva 我們想要干什么。我們可以選擇給 action 附著其他字段,這里約定用 payload字段表示額外信息。
我們把想做的事情通過 action 描述出來,并通過 dispatch 告訴 dva model,而對這個消息的處理就是 dva 的事情了。如果深入了解 React 的讀者,一定覺得這句話似曾相識。是的,dva 和 React 哲學一脈相承,React 也是遵循這個原理工作的,在組件中總要寫一個 render 函數(shù),這個函數(shù)就是向 React 描述你想要渲染出的內(nèi)容,作為開發(fā)者你并不會去直接操作 DOM,而 React 負責把 render 函數(shù)的返回值轉(zhuǎn)化成 DOM 元素,并由 React 最終決定渲染 DOM 的時機和流程(React 渲染引擎的執(zhí)行是個異步的過程)。
接下來的問題是:派發(fā)出的 action 怎樣被 dva 識別并執(zhí)行 "添加卡片"? 的邏輯呢 ?
定義 reducer
dva model 中可以定義一個叫做 reducers 的成員用來響應 action 并修改 state。每一個 reducer 都是一個 function,action 派發(fā)后,通過 action.type 被唯一地匹配到,隨后執(zhí)行函數(shù)體邏輯,返回值被 dva 使用作為新的 state。state 的改變隨后會被 connect 注入到組件中,觸發(fā)視圖改變。
reducer 的樣子大概是:
someReducer(state /* old state */, { payload }) {
// ... do calculation
return {
// ... build a new object as next state and return it
};
}
reducer 應該是一個 "純函數(shù)",它的返回值作為新的 state。dva 會注入舊的 state 和 action 中的 payload,是否使用完全根據(jù)需要決定;返回值必須是一個新構造對象,絕不能把舊 state 的引用返回!
reducer 干的事情和 React 中
setState(prevState => { ... })很像,都要返回一個新構造的對象,但區(qū)別是:reducer 的返回值會 整個取代 (Replace) 老的 state,而 setState 中回調(diào)函數(shù)的返回值是會 融合(Merge) 到老的 state 中去。
下圖標示了由 dva 驅(qū)動的整個過程,

參考資料
https://www.yuque.com/ant-design/course/wybhm9#%E6%9E%84%E5%BB%BA%E5%92%8C%E9%83%A8%E7%BD%B2