概述
本篇文章使用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í)踐過程講解美浦,在對(duì)react输莺、redux戚哎、react-router有了初步了解后,來運(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è)中間件嘱函「食可以使action creator返回一個(gè)
function
(而不僅僅是object
),并且使得dispatch方法可以接收一個(gè)function
作為參數(shù)实夹,通過這種改造使得action支持異步(或延遲)操作 - redux-actions:針對(duì)redux的一個(gè)FSA工具箱橄浓,可以相應(yīng)簡(jiǎn)化與標(biāo)準(zhǔn)化action與reducer部分
好了,話不多說亮航,一起來構(gòu)建你的單頁(yè)應(yīng)用吧荸实。
使用create-react-app腳手架
create-react-app是Facebook官方出品的腳手架。有了它缴淋,你只需要一行指令即可跳過webpack繁瑣的配置准给、npm繁多的引入等過程,迅速構(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)用钟沛。
打開訪問localhost:3000
看到下方對(duì)應(yīng)的頁(yè)面畔规,就說明項(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中我們通過構(gòu)建各種react component
來實(shí)現(xiàn)一個(gè)新的世界悠鞍。在我們的項(xiàng)目里对室,會(huì)基于此,將組件分為通用組件部分與頁(yè)面組件部分。通用組件也就是我們普遍意義上的組件掩宜,一些大型項(xiàng)目會(huì)維護(hù)一個(gè)自己的組件庫(kù)蔫骂,其中的組件會(huì)被整個(gè)項(xiàng)目共享;頁(yè)面組件實(shí)際上就是我們項(xiàng)目中所呈現(xiàn)出來的各個(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ù)形式:該類組件為無狀態(tài)組件。由于使用函數(shù)來定義伐坏,因此不能訪問this
對(duì)象怔匣,同時(shí)也沒有生命周期方法,只能訪問props
桦沉。這類組件主要是一些純展示類的小組件每瞒,通過將這些小組件進(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
編寫為無狀態(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來管理數(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í)碑宴,通過它可以區(qū)分不同的action软啼,其類型只能是字符串常量或
Symbol
。 - payload屬性是可選的延柠,可以使任何類型祸挪。payload可以用來裝載數(shù)據(jù);在error為true的時(shí)候捕仔,payload一般是用來裝載錯(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處理佳晶,它們通過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
)晒他。可以通過簡(jiǎn)單的switch操作來實(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ù)〈バ欤可以通過handleActions
方法將多個(gè)相關(guān)的reducer寫在一起咪鲜,以便于后期維護(hù):
handleActions(reducerMap, defaultState)
因此,我們使用redux-actions
來改寫我們之前寫的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ù)邏輯過多時(shí),我們可以將多個(gè)reducer進(jìn)行組合孝鹊,生成一個(gè)統(tǒng)一的reducer炊琉。雖然現(xiàn)在我們只有一個(gè)reducer,但是為了拓展性和示范性又活,在這里還是創(chuàng)建了一個(gè)reducer/index.js
文件來進(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è)問題就要用到我們之前提到的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);
然后评汰,我們需要讓所有的組件都能訪問到store
纷捞。最簡(jiǎn)單的方式就是使用react-redux
提供的Provider
對(duì)整個(gè)應(yīng)用進(jìn)行包裝。這樣就可以使所有的子頁(yè)面被去、子組件能訪問到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è)面組件耍攘。通過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)更新的交互示损,可以通過在組件中相應(yīng)部分觸發(fā)action來實(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)過了完整的redux-flow的過程獲取并渲染的医清。注意同時(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)在訪問頁(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來作為路徑)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
來查看效果胧卤。
當(dāng)然,實(shí)際項(xiàng)目里不可能是通過手動(dòng)修改地址欄來“跳轉(zhuǎn)”頁(yè)面拼岳。所以需要用到Link
這個(gè)組件枝誊。通過其中的to
這個(gè)屬性來指明“跳轉(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ì)刷新§艄唬基于React+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ù)獲取的過程中,可能還會(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è)問題稠诲,基于上面的討論侦鹏,我們需要actions.getGoods()
這個(gè)方法返回一個(gè)function
來實(shí)現(xiàn)我們?cè)诓襟E2里所說的三個(gè)功能诡曙;然而,目前項(xiàng)目中的dispatch()
方法只能接受一個(gè)object
類型作為參數(shù)略水。所以价卤,我們需要改造dispatch()
方法。
改造的手段就是使用redux-thunk這個(gè)中間件渊涝∩麒担可以使action creator返回一個(gè)function
(而不僅僅是object
),并且使得dispatch方法可以接收一個(gè)function
作為參數(shù)跨释,通過這種改造使得action支持異步(或延遲)操作胸私。
那么如何來改造呢?首先為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來模擬向后端請(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)來表示數(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);
最終暖侨,訪問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)擊這里獲取兽肤。