概述
本篇文章使用create-react-app作為腳手架,結(jié)合react技術(shù)棧(react + redux + react-router),構(gòu)建一個(gè)簡(jiǎn)單的單頁(yè)面應(yīng)用demo。文章會(huì)一步步地講解如何構(gòu)建這么一個(gè)單頁(yè)應(yīng)用。文章的最后也會(huì)給出相應(yīng)的demo地址。
本文主要是對(duì)SPA搭建的實(shí)踐過(guò)程講解,在對(duì)react、redux、react-router有了初步了解后,來(lái)運(yùn)用這些技術(shù)構(gòu)建一個(gè)簡(jiǎn)單的單頁(yè)應(yīng)用。這個(gè)應(yīng)用包括了側(cè)邊導(dǎo)航欄與主體內(nèi)容區(qū)域。下面簡(jiǎn)單羅列了將會(huì)用到的一些框架與工具。
- create-react-app:腳手架
- react:負(fù)責(zé)頁(yè)面組件構(gòu)建
- react-router:負(fù)責(zé)單頁(yè)應(yīng)用路由部分的控制
- redux:負(fù)責(zé)管理整個(gè)應(yīng)用的數(shù)據(jù)流
- react-redux:將react與redux這兩部分相結(jié)合
- redux-thunk:redux的一個(gè)中間件??梢允筧ction creator返回一個(gè)
function(而不僅僅是object),并且使得dispatch方法可以接收一個(gè)function作為參數(shù),通過(guò)這種改造使得action支持異步(或延遲)操作 - redux-actions:針對(duì)redux的一個(gè)FSA工具箱,可以相應(yīng)簡(jiǎn)化與標(biāo)準(zhǔn)化action與reducer部分
好了,話不多說(shuō),一起來(lái)構(gòu)建你的單頁(yè)應(yīng)用吧。
使用create-react-app腳手架
create-react-app是Facebook官方出品的腳手架。有了它,你只需要一行指令即可跳過(guò)webpack繁瑣的配置、npm繁多的引入等過(guò)程,迅速構(gòu)建react項(xiàng)目。
首先安裝create-react-app
npm i -g create-react-app
安裝完成后,就可以使用create-react-app指令快速創(chuàng)建一個(gè)基于webpack的react應(yīng)用程序
cd $your_dir
create-react-app react-redux-demo
這時(shí)你可以進(jìn)入react-redux-demo這個(gè)目錄,運(yùn)行npm start既可啟動(dòng)該應(yīng)用。
打開訪問(wèn)localhost:3000看到下方對(duì)應(yīng)的頁(yè)面,就說(shuō)明項(xiàng)目基礎(chǔ)框架創(chuàng)建完畢了。

創(chuàng)建React組件
修改目錄結(jié)構(gòu)
下面在我們的react-redux-demo項(xiàng)目,查看一下相應(yīng)的目錄結(jié)構(gòu)
|--public
|--index.html
|-- ……
|--src
|--App.js
|--index.js
|-- ……
|--node_modules
其中public中存放的內(nèi)容不會(huì)被webpack編譯,所以可以放一些靜態(tài)頁(yè)面或圖片;src中存放的內(nèi)容才會(huì)被webpack打包編譯,我們主要工作的目錄就是在src下。
了解react的同學(xué)肯定知道,在react中我們通過(guò)構(gòu)建各種react component來(lái)實(shí)現(xiàn)一個(gè)新的世界。在我們的項(xiàng)目里,會(huì)基于此,將組件分為通用組件部分與頁(yè)面組件部分。通用組件也就是我們普遍意義上的組件,一些大型項(xiàng)目會(huì)維護(hù)一個(gè)自己的組件庫(kù),其中的組件會(huì)被整個(gè)項(xiàng)目共享;頁(yè)面組件實(shí)際上就是我們項(xiàng)目中所呈現(xiàn)出來(lái)的各個(gè)頁(yè)面。因此,我們的目錄會(huì)變成這樣
|--public
|--index.html
|-- ……
|--src
|--page
|--welcome.js
|--goods.js
|--component
|--nav
|--index.js
|--index.css
|--App.js
|--index.js
|-- ……
|--node_modules
在src目錄下新建了page和component兩個(gè)目錄分別用于存放頁(yè)面組件和通用組件。頁(yè)面組件包括welcome.js和商品列表頁(yè)good.js,通用組件包括了一個(gè)導(dǎo)航欄nav。
兩種組件形式
編寫頁(yè)面或組件,類似于靜態(tài)頁(yè)的開發(fā)。推薦的組件寫法有兩種:
1)純函數(shù)形式:該類組件為無(wú)狀態(tài)組件。由于使用函數(shù)來(lái)定義,因此不能訪問(wèn)this對(duì)象,同時(shí)也沒有生命周期方法,只能訪問(wèn)props。這類組件主要是一些純展示類的小組件,通過(guò)將這些小組件進(jìn)行組合構(gòu)成更為復(fù)雜的組件。例如:
const Title = props => (
<h1>
{props.title} - {props.subtitle}
</h1>
)
2)es6形式的組件:該類組件一般為復(fù)雜的或有狀態(tài)組件。使用es6的class語(yǔ)法進(jìn)行創(chuàng)建。需要注意的是,在頁(yè)面/組件中使用this注意其指向,必要時(shí)需要綁定。綁定方法可以使用bind函數(shù)或箭頭函數(shù)。創(chuàng)建方式如下:
class Title extends Component {
constructor(props) {
super(props);
this.state = {
shown: true
};
}
render() {
let style = {
display: this.state.shown ? 'block' : none
};
return (
<h1 style={style}>
{props.title} - {props.subtitle}
</h1>
);
}
}
下面是這兩種組件之間的對(duì)比:
| Presentational Components | Container Components | |
|---|---|---|
| Purpose | How things look (markup, styles) | How things work (data fetching, state updates) |
| Aware of Redux | No | Yes |
| To read data | Read data from props | Subscribe to Redux state |
| To change data | Invoke callbacks from props | Dispatch Redux actions |
| Are written | By hand | Usually generated by React Redux |
鑒于上面的分析,我們可以將導(dǎo)航欄nav編寫為無(wú)狀態(tài)組件,而page中的部分使用有狀態(tài)的組件。
導(dǎo)航欄組件nav
// component/nav/index.css
.nav {
margin: 30px;
padding: 0;
}
.nav li {
border-left: 5px solid sandybrown;
margin: 15px 0;
padding: 6px 0;
color: #333;
list-style: none;
background: #bbb;
}
// component/nav/index.js
import React from 'react';
import './index.css';
const Nav = props => (
<ul className="nav">
{
props.list.map((ele, idx) => (
<li key={idx}>{ele.text}</li>
))
}
</ul>
);
export default Nav;
修改后的App.js與App.css
// App.css
.App {
text-align: center;
}
.App::after {
clear: both;
}
.nav_bar {
float: left;
width: 300px;
}
.conent {
margin-left: 300px;
padding: 30px;
}
// App.js
import React, { Component } from 'react';
import Nav from './component/nav';
import Welcome from './page/welcome';
import Goods from './page/goods';
import './App.css';
const LIST = [{
text: 'welcome',
url: '/welcome'
}, {
text: 'goods',
url: '/goods'
}];
const GOODS = [{
name: 'iPhone 7',
price: '6,888',
amount: 37
}, {
name: 'iPad',
price: '3,488',
amount: 82
}, {
name: 'MacBook Pro',
price: '11,888',
amount: 15
}];
class App extends Component {
render() {
return (
<div className="App">
<div className="nav_bar">
<Nav list={LIST} />
</div>
<div className="conent">
<Welcome />
<Goods list={GOODS} />
</div>
</div>
);
}
}
export default App;
welcome頁(yè)面
// page/welcome.js
import React from 'react';
const Welcome = props => (
<h1>Welcome!</h1>
);
export default Welcome;
goods頁(yè)面
// page/goods.js
import React, { Component } from 'react';
class Goods extends Component {
render() {
return (
<ul className="goods">
{
this.props.list.map((ele, idx) => (
<li key={idx} style={{marginBottom: 20, listStyle: 'none'}}>
<span>{ele.name}</span> |
<span>¥ {ele.price}</span> |
<span>剩余 {ele.amount} 件</span>
</li>
))
}
</ul>
);
}
}
export default Goods;
現(xiàn)在我們的頁(yè)面是這樣的

使用redux來(lái)管理數(shù)據(jù)流

redux是flux架構(gòu)的一種實(shí)現(xiàn)。圖中展示了,在react+redux框架下,一個(gè)點(diǎn)擊事件是如何進(jìn)行交互的。
然而redux并不是完全依附于react的框架,實(shí)際上redux是可以和任何UI層框架相結(jié)合的。因此,為了更好得結(jié)合redux與react,對(duì)redux-flow中的store有一個(gè)更好的全局性管理,我們還需要使用react-redux。
npm i --save redux
npm i --save react-redux
同時(shí),為了更好地創(chuàng)建action和reducer,我們還會(huì)在項(xiàng)目中引入redux-actions:一個(gè)針對(duì)redux的一個(gè)FSA工具箱,可以相應(yīng)簡(jiǎn)化與標(biāo)準(zhǔn)化action與reducer部分。當(dāng)然,這是可選的
npm i --save redux-actions
下面我們會(huì)以goods頁(yè)面為例,實(shí)現(xiàn)以下場(chǎng)景:goods頁(yè)面組件渲染完成后,發(fā)送請(qǐng)求,獲取商品列表。其中獲取數(shù)據(jù)的方法會(huì)使用mock數(shù)據(jù)。
為了實(shí)現(xiàn)這些功能,我們需要進(jìn)一步調(diào)整目錄結(jié)構(gòu)
|--public
|--index.html
|-- ……
|--src
|--page
|--welcome.js
|--goods.js
|--component
|--nav
|--index.js
|--index.css
|--action
|--goods.js
|--reducer
|--goods.js
|--index.js
|--App.js
|--index.js
|-- ……
|--node_modules
首先,創(chuàng)建action
首先,我們要?jiǎng)?chuàng)建對(duì)應(yīng)的action。
action是一個(gè)object類型,對(duì)于action的結(jié)構(gòu)有Flux有相關(guān)的標(biāo)準(zhǔn)化建議FSA
一個(gè)action必須要包含type屬性,同時(shí)它還有三個(gè)可選屬性error、payload和meta。
- type屬性相當(dāng)于是action的標(biāo)識(shí),通過(guò)它可以區(qū)分不同的action,其類型只能是字符串常量或
Symbol。 - payload屬性是可選的,可以使任何類型。payload可以用來(lái)裝載數(shù)據(jù);在error為true的時(shí)候,payload一般是用來(lái)裝載錯(cuò)誤信息。
- error屬性是可選的,一般當(dāng)出現(xiàn)錯(cuò)誤時(shí)其值為true;如果是其他值,不被理解為出現(xiàn)錯(cuò)誤。
- meta屬性可以使任何類型,它一般會(huì)包括一些不適合在payload中放置的數(shù)據(jù)。
我們可以創(chuàng)建一個(gè)獲取goods信息的action:
// action/goods.js
const getGoods = goods => {
return {
type: 'GET_GOODS',
payload: goods
};
}
這樣,我們就可以得到GET_GOODS這個(gè)action。
在項(xiàng)目中,使用redux-actions對(duì)actions進(jìn)行創(chuàng)建與管理:
createAction(type, payloadCreator = Identity, ?metaCreator)
createAction相當(dāng)于對(duì)action創(chuàng)建器的一個(gè)包裝,會(huì)返回一個(gè)FSA,使用這個(gè)返回的FSA可以創(chuàng)建具體的action。
payloadCreator是一個(gè)function,處理并返回需要的payload;如果空缺,會(huì)使用默認(rèn)方法。如果傳入一個(gè)Error對(duì)象則會(huì)自動(dòng)將action的error屬性設(shè)為true:
example = createAction('EXAMLE', data => data);
// 和下面的使用效果一樣
example = createAction('EXAMLE');
因此上面的方式可以改寫為:
// action/goods.js
import {createAction} from 'redux-actions';
export const getGoods = createAction('GET_GOODS');
* 此外,還可以使用createActions同時(shí)創(chuàng)建多個(gè)action creators。
其次,創(chuàng)建state的處理方法——reducer
針對(duì)不同的action,會(huì)有不同的reducer對(duì)應(yīng)進(jìn)行state處理,它們通過(guò)type的值相互對(duì)應(yīng)。
reducer是一個(gè)處理state的方法(function),該方法接收兩個(gè)參數(shù),當(dāng)前狀態(tài)state和對(duì)應(yīng)的action。根據(jù)state與action,reducer會(huì)進(jìn)行處理并返回一個(gè)新的state(同時(shí)也是一個(gè)新的object,而不去修改原state)??梢酝ㄟ^(guò)簡(jiǎn)單的switch操作來(lái)實(shí)現(xiàn):
// reducer/goods.js
const goods = (state, action) => {
switch (action.type) {
case 'GET_GOODS':
return {
...state,
data: action.payload
};
// 其他action處理……
}
}
對(duì)應(yīng)createAction,redux-actions也有相應(yīng)的reducer方式:
handleAction(type, reducer | reducerMap = Identity, defaultState)
type可以是字符串,也可以是createAction返回的action創(chuàng)建器:
handleAction('GET_GOODS', {
next(state, action) {...},
throw(state, action) {...}
}, defaultState);
//或者可以是
handleAction(getGoods, {
next(state, action) {...},
throw(state, action) {...}
}, defaultState);
此外,有時(shí)候一些操作的一系列action可以在語(yǔ)義和業(yè)務(wù)邏輯上是有一定聯(lián)系的,我們希望將他們放在一起便于維護(hù)??梢酝ㄟ^(guò)handleActions方法將多個(gè)相關(guān)的reducer寫在一起,以便于后期維護(hù):
handleActions(reducerMap, defaultState)
因此,我們使用redux-actions來(lái)改寫我們之前寫的reducer
// reducer/goods.js
import {handleActions} from 'redux-actions';
export const goods = handleActions({
GET_GOODS: (state, action) => ({
...state,
data: action.payload
})
}, {
data: []
});
然后,對(duì)reducer進(jìn)行合并
因?yàn)樵趓edux中會(huì)統(tǒng)一管理一個(gè)store,因此,需要將不用的reducer所處理的state進(jìn)行合并。
redux為我們提供了combineReducers方法。當(dāng)業(yè)務(wù)邏輯過(guò)多時(shí),我們可以將多個(gè)reducer進(jìn)行組合,生成一個(gè)統(tǒng)一的reducer。雖然現(xiàn)在我們只有一個(gè)reducer,但是為了拓展性和示范性,在這里還是創(chuàng)建了一個(gè)reducer/index.js文件來(lái)進(jìn)行reducer的合并,生成一個(gè)rootReducer。
// reducer/index.js
import {combineReducers} from 'redux';
import {goods} from './goods';
export const rootReducer = combineReducers({
goods
});
之后,將頁(yè)面組件與數(shù)據(jù)流相結(jié)合
上面的部分已經(jīng)將redux中的action與reducer創(chuàng)建完畢了,然而,現(xiàn)在的數(shù)據(jù)流和我們的組件仍然是處于分離狀態(tài)的,我們需要讓全局的state,即store,的變化能夠驅(qū)動(dòng)頁(yè)面組件的變化,才能完成redux-flow中的最后一環(huán)。這就需要將store中的各部分state映射到組件的props上。
解決這個(gè)問(wèn)題就要用到我們之前提到的react-redux工具了。
首先,我們需要基于rootReducer創(chuàng)建一個(gè)全局的store。在src目錄下新建一個(gè)store.js文件,調(diào)用redux的createStore方法:
// store.js
import {createStore} from 'redux';
import {rootReducer} from './reducer';
export const store = createStore(rootReducer);
然后,我們需要讓所有的組件都能訪問(wèn)到store。最簡(jiǎn)單的方式就是使用react-redux
提供的Provider對(duì)整個(gè)應(yīng)用進(jìn)行包裝。這樣就可以使所有的子頁(yè)面、子組件能訪問(wèn)到store。因此需要改寫index.js:
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import {Provider} from 'react-redux';
import {store} from './store';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'));
最后,才是進(jìn)行組件與狀態(tài)的連接。將store中需要映射的部分connect到我們的組件上。使用其connect方法可以做到這一點(diǎn):
connect(mapStateToProps)(component);
redux中存在一個(gè)全局的store,其中存儲(chǔ)了整個(gè)應(yīng)用的狀態(tài),對(duì)其進(jìn)行統(tǒng)一管理。connect可以將這個(gè)狀態(tài)中的數(shù)據(jù)連接到頁(yè)面組件上。其中,mapStateToProps是store中狀態(tài)到該組件屬性的一個(gè)映射方式,component是需要連接的頁(yè)面組件。通過(guò)connect方法,一旦store發(fā)生變化,組件也就會(huì)相應(yīng)更新。
我們需要修改原先page/goods.js
import React, { Component } from 'react';
import {connect} from 'react-redux';
class Goods extends Component {
render() {
return (
<ul className="goods">
{
this.props.list.map((ele, idx) => (
<li key={idx} style={{marginBottom: 20, listStyle: 'none'}}>
<span>{ele.name}</span> |
<span>¥ {ele.price}</span> |
<span>剩余 {ele.amount} 件</span>
</li>
))
}
</ul>
);
}
}
const mapStateToProps = (state, ownProps) => ({
goods: state.goods.data
});
// -export default Goods;
export default connect(mapStateToProps)(Goods);
此外,也可以為組件中相應(yīng)的方法映射對(duì)應(yīng)的action的觸發(fā):
const mapDispatchToProps = dispatch => ({
onShownClick: () => dispatch($yourAction)
});
最后,在組件渲染完成后觸發(fā)整個(gè)flow
如果產(chǎn)生了一個(gè)需要狀態(tài)更新的交互,可以通過(guò)在組件中相應(yīng)部分觸發(fā)action來(lái)實(shí)現(xiàn)狀態(tài)更新-->組件更新。觸發(fā)方式:
dispatch($your_action)
connect后的組件,其props里會(huì)有一個(gè)dispatch的屬性,就是個(gè)dispatch方法:
let dispatch = this.props.dispatch;
因此,最終的page/goods.js組件如下:
import React, { Component } from 'react';
import {connect} from 'react-redux';
import * as actions from '../action/goods';
const GOODS = [{
name: 'iPhone 7',
price: '6,888',
amount: 37
}, {
name: 'iPad',
price: '3,488',
amount: 82
}, {
name: 'MacBook Pro',
price: '11,888',
amount: 15
}];
class Goods extends Component {
componentDidMount() {
let dispatch = this.props.dispatch;
dispatch(actions.getGoods(GOODS));
}
render() {
return (
<ul className="goods">
{
this.props.goods.map((ele, idx) => (
<li key={idx} style={{marginBottom: 20, listStyle: 'none'}}>
<span>{ele.name}</span> |
<span>¥ {ele.price}</span> |
<span>剩余 {ele.amount} 件</span>
</li>
))
}
</ul>
);
}
}
const mapStateToProps = (state, ownProps) => ({
goods: state.goods.data
});
export default connect(mapStateToProps)(Goods);
注意到,組件中數(shù)據(jù)不再是由App.js中寫入的了,而是經(jīng)過(guò)了完整的redux-flow的過(guò)程獲取并渲染的。注意同時(shí)修改App.js
import React, { Component } from 'react';
import Nav from './component/nav';
import Welcome from './page/welcome';
import Goods from './page/goods';
import './App.css';
const LIST = [{
text: 'welcome',
url: '/'
}, {
text: 'goods',
url: '/goods'
}];
class App extends Component {
render() {
return (
<div className="App">
<div className="nav_bar">
<Nav list={LIST} />
</div>
<div className="conent">
<Welcome />
<Goods />
</div>
</div>
);
}
}
export default App;
現(xiàn)在訪問(wèn)頁(yè)面,雖然效果和之前一致,但是其內(nèi)部構(gòu)造和原理已經(jīng)大不相同了。
最后一部分:添加路由系統(tǒng)
單頁(yè)應(yīng)用中的重要部分,就是路由系統(tǒng)。由于不同普通的頁(yè)面跳轉(zhuǎn)刷新,因此單頁(yè)應(yīng)用會(huì)有一套自己的路由系統(tǒng)需要維護(hù)。
我們當(dāng)然可以手寫一個(gè)路由系統(tǒng),但是,為了快速有效地創(chuàng)建于管理我們的應(yīng)用,我們可以選擇一個(gè)好用的路由系統(tǒng)。本文選擇了react-router 4。這里需要注意,在v4版本里,react-router將WEB部分的路由系統(tǒng)拆分至了react-router-dom,因此需要npmreact-router-dom
npm i --save react-router-dom
本例中我們使用react-router中的BrowserRouter組件包裹整個(gè)App應(yīng)用,在其中使用Route組件用于匹配不同的路由時(shí)加載不同的頁(yè)面組件。(也可以使用HashRouter,顧名思義,是使用hash來(lái)作為路徑)react-router推薦使用BrowserRouter,BrowserRouter需要history相關(guān)的API支持。
首先,需要在App.js中添加BrowserRouter組件,并將Route組件放在BrowserRouter內(nèi)。其中Route組件接收兩個(gè)屬性:path和component,分別是匹配的路徑與加載渲染的組件
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import {Provider} from 'react-redux';
import {store} from './store';
import {BrowserRouter, Route} from 'react-router-dom';
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<Route path='/' component={App}/>
</BrowserRouter>
</Provider>,
document.getElementById('root'));
此時(shí)我們啟動(dòng)服務(wù)器的效果和之前一直。因?yàn)榇藭r(shí)路由匹配到了path='/',因此加載了App組件。
還記得我們?cè)谧铋_始部分創(chuàng)建的Nav導(dǎo)航欄組件么?現(xiàn)在,我們就要實(shí)現(xiàn)導(dǎo)航功能:點(diǎn)擊對(duì)應(yīng)的導(dǎo)航欄鏈接,右側(cè)顯示不同的區(qū)域內(nèi)容。這需要改造index.js中的content部分:我們?yōu)槠涮砑觾蓚€(gè)Route組件,分別在不同的路徑下加載不同的頁(yè)面組件(welcome與goods)
// App.js
import React, { Component } from 'react';
import Nav from './component/nav';
import Welcome from './page/welcome';
import Goods from './page/goods';
import './App.css';
import {Route} from 'react-router-dom';
const LIST = [{
text: 'welcome',
url: '/welcome'
}, {
text: 'goods',
url: '/goods'
}];
class App extends Component {
render() {
return (
<div className="App">
<div className="nav_bar">
<Nav list={LIST} />
</div>
<div className="conent">
<Route path='/welcome' component={Welcome} />
<Route path='/goods' component={Goods} />
</div>
</div>
);
}
}
export default App;
現(xiàn)在,可以嘗試在地址欄輸入http://localhost:3000、http://localhost:3000/welcome和http://localhost:3000/goods來(lái)查看效果。
當(dāng)然,實(shí)際項(xiàng)目里不可能是通過(guò)手動(dòng)修改地址欄來(lái)“跳轉(zhuǎn)”頁(yè)面。所以需要用到Link這個(gè)組件。通過(guò)其中的to這個(gè)屬性來(lái)指明“跳轉(zhuǎn)”的地址。這個(gè)Link組件我們會(huì)添加到Nav組件中
// component/nav/index.js
import React from 'react';
import './index.css';
import {Link} from 'react-router-dom';
const Nav = props => (
<ul className="nav">
{
props.list.map((ele, idx) => (
<Link to={ele.url} key={idx}>
<li>{ele.text}</li>
</Link>
))
}
</ul>
);
export default Nav;
最終頁(yè)面效果如下:


現(xiàn)在在這個(gè)demo里,我們點(diǎn)擊左側(cè)的導(dǎo)航,右側(cè)內(nèi)容發(fā)生變化,瀏覽器不會(huì)刷新?;赗eact+Redux+React-router,我們實(shí)現(xiàn)了一個(gè)最基礎(chǔ)版的SPA(單頁(yè)應(yīng)用)。
額外的部分,異步請(qǐng)求
如果你還記得在redux數(shù)據(jù)流部分,是怎么給goods頁(yè)面?zhèn)魅霐?shù)據(jù)的:dispatch(actions.getGoods(GOODS)),我們直接給getGoods這個(gè)action構(gòu)造器傳入GOODS列表,作為加載的數(shù)據(jù)。但是,在實(shí)際的應(yīng)用場(chǎng)景中,往往,我們會(huì)在action中發(fā)送ajax請(qǐng)求,從后端獲取數(shù)據(jù);在等待數(shù)據(jù)獲取的過(guò)程中,可能還會(huì)有一個(gè)loading效果;最后收到了response響應(yīng),再渲染響應(yīng)頁(yè)面。
基于以上的場(chǎng)景,重新整理一下我們的action內(nèi)的思路:
- component渲染完成后,觸發(fā)一個(gè)action,
dispatch(actions.getGoods())。這個(gè)action并不會(huì)帶列表的參數(shù),而是向后端請(qǐng)求結(jié)果。 - 在
getGoods()這個(gè)方法里,主要會(huì)做這三件數(shù):首先,觸發(fā)一個(gè)requestGoods的action,用于表示現(xiàn)在正在請(qǐng)求數(shù)據(jù);其次,會(huì)調(diào)用一個(gè)叫fetchData()的方法,這個(gè)就是向后端請(qǐng)求數(shù)據(jù)的方法;最后,在拿到數(shù)據(jù)后,再觸發(fā)一個(gè)receiveGoods的action,用于標(biāo)識(shí)請(qǐng)求完成并帶上渲染的數(shù)據(jù)。 - 其他部分與之前類似。
這里就有一個(gè)問(wèn)題,基于上面的討論,我們需要actions.getGoods()這個(gè)方法返回一個(gè)function來(lái)實(shí)現(xiàn)我們?cè)诓襟E2里所說(shuō)的三個(gè)功能;然而,目前項(xiàng)目中的dispatch()方法只能接受一個(gè)object類型作為參數(shù)。所以,我們需要改造dispatch()方法。
改造的手段就是使用redux-thunk這個(gè)中間件。可以使action creator返回一個(gè)function(而不僅僅是object),并且使得dispatch方法可以接收一個(gè)function作為參數(shù),通過(guò)這種改造使得action支持異步(或延遲)操作。
那么如何來(lái)改造呢?首先為redux加入redux-thunk這個(gè)中間件
npm i --save redux-thunk
然后修改store.js
// store.js
import {createStore, applyMiddleware, compose} from 'redux';
import {rootReducer} from './reducer';
import thunk from 'redux-thunk';
const middleware = [thunk];
export const store = createStore(rootReducer, compose(
applyMiddleware(...middleware)
));
然后,基于之前的思路,整理action中的代碼。在這里,我們使用setTimeout來(lái)模擬向后端請(qǐng)求數(shù)據(jù):
// action/goods.js
import {createAction} from 'redux-actions';
const GOODS = [{
name: 'iPhone 7',
price: '6,888',
amount: 37
}, {
name: 'iPad',
price: '3,488',
amount: 82
}, {
name: 'MacBook Pro',
price: '11,888',
amount: 15
}];
const requestGoods = createAction('REQUEST_GOODS');
const receiveGoods = createAction('RECEIVE_GOODS');
const fetchData = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(GOODS);
}, 1500);
});
};
export const getGoods = () => async dispatch => {
dispatch(requestGoods());
let goods = await fetchData();
dispatch(receiveGoods(goods));
};
相應(yīng)地修改reducer中的代碼
// reducer/goods.js
import {handleActions} from 'redux-actions';
export const goods = handleActions({
REQUEST_GOODS: (state, action) => ({
...state,
isFetching: true
}),
RECEIVE_GOODS: (state, action) => ({
...state,
isFetching: false,
data: action.payload
})
}, {
isFetching: false,
data: []
});
可以看到,我們添加了一個(gè)isFetching的狀態(tài)來(lái)表示數(shù)據(jù)是否加載完畢。
最后,還需要更新UI component層
// page/goods.js
import React, { Component } from 'react';
import {connect} from 'react-redux';
import * as actions from '../action/goods';
class Goods extends Component {
componentDidMount() {
let dispatch = this.props.dispatch;
dispatch(actions.getGoods());
}
render() {
return this.props.isFetching ? (<h1>Loading…</h1>) : (
<ul className="goods">
{
this.props.goods.map((ele, idx) => (
<li key={idx} style={{marginBottom: 20, listStyle: 'none'}}>
<span>{ele.name}</span> |
<span>¥ {ele.price}</span> |
<span>剩余 {ele.amount} 件</span>
</li>
))
}
</ul>
);
}
}
const mapStateToProps = (state, ownProps) => ({
isFetching: state.goods.isFetching,
goods: state.goods.data
});
export default connect(mapStateToProps)(Goods);
最終,訪問(wèn)http://localhost:3000/goods頁(yè)面會(huì)有一個(gè)大約1.5s的loading效果,然后等“后端”數(shù)據(jù)返回后渲染出列表。


最后的最后,如果你還沒有走開
再介紹一個(gè)redux調(diào)試神器——redux-devTools,可以在chrome插件中可以找到

在開發(fā)者工具中使用,可以很方便的進(jìn)行redux的調(diào)試


當(dāng)然,需要在代碼中進(jìn)行簡(jiǎn)單的配置。對(duì)store.js進(jìn)行一些小修改
import {createStore, applyMiddleware, compose} from 'redux';
import {rootReducer} from './reducer';
import thunk from 'redux-thunk';
const middleware = [thunk];
// export const store = createStore(rootReducer, compose(
// applyMiddleware(...middleware)
// ));
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
export const store = createStore(rootReducer, composeEnhancers(
applyMiddleware(...middleware)
));
以上。
現(xiàn)在,你可以愉快地進(jìn)行SPA的開發(fā)啦!本文中的demo可以點(diǎn)擊這里獲取。