基于react技術(shù)棧的單頁(yè)應(yīng)用(SPA)搭建_快速入門實(shí)踐

概述

本篇文章使用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)建完畢了。

啟動(dòng)頁(yè)面

創(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目錄下新建了pagecomponent兩個(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.jsApp.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數(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秸应、payloadmeta

  • 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ù)stateaction,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推薦使用BrowserRouterBrowserRouter需要history相關(guān)的API支持讥巡。

首先掀亩,需要在App.js中添加BrowserRouter組件,并將Route組件放在BrowserRouter內(nèi)欢顷。其中Route組件接收兩個(gè)屬性:pathcomponent,分別是匹配的路徑與加載渲染的組件

// 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è)面組件(welcomegoods

// 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/welcomehttp://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è)面效果如下:

最終效果圖welcome頁(yè)面
最終效果圖goods頁(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)用)。


點(diǎn)擊這里可以下載這個(gè)demo古瓤。


額外的部分止剖,異步請(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)的思路:

  1. component渲染完成后将宪,觸發(fā)一個(gè)action,dispatch(actions.getGoods())橡庞。這個(gè)action并不會(huì)帶列表的參數(shù)较坛,而是向后端請(qǐng)求結(jié)果。
  2. 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ù)秉剑。
  3. 其他部分與之前類似。

這里就有一個(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ù)返回后渲染出列表崇渗。

loading效果
列表加載完畢

最后的最后字逗,如果你還沒有走開

再介紹一個(gè)redux調(diào)試神器——redux-devTools尚揣,可以在chrome插件中可以找到

redux-devTools extension

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

redux-devTools調(diào)試界面
redux-devTools調(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)擊這里獲取兽肤。


Happy Coding!


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末绪抛,一起剝皮案震驚了整個(gè)濱河市资铡,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌幢码,老刑警劉巖笤休,帶你破解...
    沈念sama閱讀 222,183評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異症副,居然都是意外死亡店雅,警方通過查閱死者的電腦和手機(jī)政基,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來闹啦,“玉大人沮明,你說我怎么就攤上這事∏戏埽” “怎么了荐健?”我有些...
    開封第一講書人閱讀 168,766評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)琳袄。 經(jīng)常有香客問我江场,道長(zhǎng),這世上最難降的妖魔是什么窖逗? 我笑而不...
    開封第一講書人閱讀 59,854評(píng)論 1 299
  • 正文 為了忘掉前任址否,我火速辦了婚禮,結(jié)果婚禮上滑负,老公的妹妹穿的比我還像新娘在张。我一直安慰自己,他們只是感情好矮慕,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,871評(píng)論 6 398
  • 文/花漫 我一把揭開白布帮匾。 她就那樣靜靜地躺著,像睡著了一般痴鳄。 火紅的嫁衣襯著肌膚如雪瘟斜。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,457評(píng)論 1 311
  • 那天痪寻,我揣著相機(jī)與錄音螺句,去河邊找鬼。 笑死橡类,一個(gè)胖子當(dāng)著我的面吹牛蛇尚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播顾画,決...
    沈念sama閱讀 40,999評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼取劫,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了研侣?” 一聲冷哼從身側(cè)響起谱邪,我...
    開封第一講書人閱讀 39,914評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎庶诡,沒想到半個(gè)月后惦银,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,543評(píng)論 3 342
  • 正文 我和宋清朗相戀三年扯俱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了书蚪。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,675評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蘸吓,死狀恐怖善炫,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情库继,我是刑警寧澤箩艺,帶...
    沈念sama閱讀 36,354評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站宪萄,受9級(jí)特大地震影響艺谆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜拜英,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,029評(píng)論 3 335
  • 文/蒙蒙 一静汤、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧居凶,春花似錦虫给、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至弄兜,卻和暖如春药蜻,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背替饿。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工语泽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人视卢。 一個(gè)月前我還...
    沈念sama閱讀 49,091評(píng)論 3 378
  • 正文 我出身青樓踱卵,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親据过。 傳聞我的和親對(duì)象是個(gè)殘疾皇子颊埃,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,685評(píng)論 2 360

推薦閱讀更多精彩內(nèi)容