基于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í)踐過(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)建完畢了。

啟動(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中我們通過(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目錄下新建了pagecomponent兩個(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.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來(lái)管理數(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í),通過(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ù)stateaction,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)createActionredux-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è)屬性: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:3000http://localhost:3000/welcomehttp://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è)面效果如下:

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


點(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ù)獲取的過(guò)程中,可能還會(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è)問(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ù)返回后渲染出列表。

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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容