React 教程第十四篇 —— Redux 跨元件通訊高階篇之中介軟體

DK_Lan發表於2018-05-24

Redux 複習總結

在前面三篇 Redux 的教程中已詳細提到 Redux 的實現,大概可可以總結以下幾點

  • Redux

    • ActionsReducerStore這三層
    • 通過createStore(reducer)得到store,換名話說store包含了reducer的邏輯實現
    • 通過store.dispath(action)去呼叫reducer,從而改變state
    • 通過store.getState()獲取在reducer改變的state
    • Redux 本身與 React 沒有並沒有半毛線關係
  • React

    • Componentstateprops三大關鍵要素
    • 本身通過setState()改變state從而觸發render,更新component
  • react-redux

    • 這是一個第三方模組,它的作用就是本來沒有半毛錢關係的 Redux 和 React 關聯在一起
    • 它有元件Provier和方法connect
    • connect將 React 的state和 Redux 的actions合併成一個物件props,再將這個物件和component生成一個新的元件
    • Provider負責將 Redux 的store當屬性傳connect的新元件,從面將 React 和 Redux 關聯到了一起
    • 當新元件呼叫action的時候,Provider.store就會對映呼叫reducer從而改變state,當state發生改變,就會觸發新元件的render,重新更新元件。

中介軟體

上面是 React 依賴 Redux 的實現過程,但問題來了,如果專案中有非同步請求,根據 Redux 的規則是:

  • Action 必須返回一個帶有屬性type的物件
  • Reducer 必須是一個純函式,負責改變state

這樣一來,這個非同步請求就變得無處安放了,這個時候的解決方案就是需要一個模組,在這個模組中發起 ajax 請求,然後在請求的回撥函式中去手動呼叫reducer,而這個發起 ajax 請求的模組就稱之為中介軟體

實現

該案例是以中介軟體呼叫 nodejs 的公共介面,實現一個資料列表。

原始碼下載:https://github.com/dk-lan/rea…

效果預覽

原始碼下載後執行下面步驟例可檢視效果

  • npm install
  • npm start

結構

| src
|——| components
|——|——| datagrid
|——|——|——| datagridcomponent.js
|——|——|——| datagridaction.js
|——|——|——| datagridconstants.js
|——|——|——| datagridreducer.js
|——|——| cnode
|——|——|——| cnode.js
|——|——| spinner
|——|——|——| spinner.js
|——|——|——| spinner.scss
|——| redux
|——|——| store.js
|——|——| middleware.js
|——|——| rootReducer.js
|——| utils
|——|——| httpclient.js
|——| app.js

datagridcomponent.js
import React from `react`
import {connect} from `react-redux`

import SpinnerComponent from `../spinner/spinner`

import * as actions from `./datagridaction`

class DatagridComponent extends React.Component{
    getKeys(item){
        let cols = item ? Object.keys(item) : [];
        return this.props.config.cols || cols;
    }
    
    componentWillMount(){
        this.props.refresh(this.props.config)
    }
    render(){
        return (
            <div>
                <table className="table">
                    <thead>
                        <tr>
                            {
                                this.getKeys(this.props.dataset[0]).map((key) => {
                                    return <th key={Math.random()}>{key}</th>
                                })
                            }
                        </tr>
                    </thead>
                    <tbody>
                        {
                            this.props.dataset.map((item) => {
                                return (
                                    <tr key={item.id || item.indexid} onDoubleClick={this.selectTr.bind(this, item)}>
                                        {
                                            this.getKeys(item).map((key) => {
                                                return <td key={Math.random()}>{item[key]}</td>
                                            })
                                        }
                                    </tr>
                                )
                            })
                        }
                        <tr></tr>
                    </tbody>
                </table>
                <SpinnerComponent show={this.props.show}/>
            </div>
        )
    }
}

const mapStateToProps = (state) => {
    return {
        dataset: state.datagrid.dataset || [], 
        show: state.datagrid.show,
        error: state.datagrid.error
    }
}

export default connect(mapStateToProps, actions)(DatagridComponent);

datagridconstants.js

export const Requesting = `Requesting`
export const Requested = `Requested`
export const RequestError = `RequestError`

datagridaction.js

import * as constants from `./datagridconstants`

export function refresh(_config){
    return {
        types: [constants.Requesting, constants.Requested, constants.RequestError],
        url: _config.url,
        method: _config.method || `get`,
        data: _config.data || {},
        name: _config.name
    }
}

datagridreducer.js

import * as constants from `./datagridconstants`

export default function datagrid(state = {}, action){
    let _state = JSON.parse(JSON.stringify(state));
    switch(action.type){
        case constants.Requesting:
            _state.show = true;
            break;
        case constants.Requested:
            _state.show = false;
            if(action.name){
                _state[action.name] = _state[action.name] || {};
                _state[action.name].dataset = action.result;
            } else {
                _state.dataset = action.result;
            }
            break;
        case constants.RequestError:
            _state.show =false;
            _state.error = action.error;
            break
    }
    return _state;
}

spinner.js

import React, {Component} from `react`
import `./spinner.scss`

class SpinnerComponent extends React.Component{
    render(){
        let html = (
            <div>
                <div className="dk-spinner-mask"></div>
                <div className="dk-spinner dk-spinner-three-bounce">
                    <div className="dk-bounce1"></div>
                    <div className="dk-bounce2"></div>
                    <div className="dk-bounce3"></div>
                </div>        
            </div>     
        )
        return this.props.show ? html : null;
    }
}

export default SpinnerComponent

spinner.scss

.dk-spinner-mask {
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    background-color: #fff;
    opacity: .4;
    z-index: 2090;
}

.dk-spinner-three-bounce.dk-spinner {
    position: absolute;
    top: 53%;
    left: 47%;
    background-color: none!important;
    z-index: 2099;
    margin: 0 auto;
    width: 70px;
    text-align: center;    
}

.dk-spinner-three-bounce div {
    width: 18px;
    height: 18px;
    background-color: #1ab394;
    border-radius: 100%;
    display: inline-block;
    -webkit-animation: dk-threeBounceDelay 1.4s infinite ease-in-out;
    animation: dk-threeBounceDelay 1.4s infinite ease-in-out;
    -webkit-animation-fill-mode: both;
    animation-fill-mode: both;
}

.dk-spinner-three-bounce .dk-bounce1 {
    -webkit-animation-delay: -.32s;
    animation-delay: -.32s;
}

.dk-spinner-three-bounce .dk-bounce2 {
    -webkit-animation-delay: -.16s;
    animation-delay: -.16s;
}

@-webkit-keyframes dk-threeBounceDelay{
    0%,
    100%,
    80%{-webkit-transform:scale(0); transform:scale(0)}
    40%{-webkit-transform:scale(1);transform:scale(1)}
}
@keyframes dk-threeBounceDelay{
    0%,
    100%,
    80%{-webkit-transform:scale(0);transform:scale(0)}
    40%{-webkit-transform:scale(1);transform:scale(1)}
}

cnode.js

import React from `react`
import Datagrid from `../../components/datagrid/datagridcomponent`

export default class CNodeComponent extends React.Component{
    static defaultProps = {
        config: {
            url: `https://cnodejs.org/api/v1/topics`,
            data: {page: 1, limit: 10},
            cols: [`title`, `reply_count`, `top`, `create_at`, `last_reply_at`]
        }
    }
    render(){
        return (
            <div>
                <Datagrid config={this.props.config}/>
            </div>
        )
    }
}

rootReducer.js

import React from `react`
import {combineReducers} from `redux`

import datagrid from `../components/datagrid/datagridreducer`

export default combineReducers({
    datagrid
})

middleware.js

import http from `../utils/httpclient`
import * as constants from `../components/datagrid/datagridconstants`

export default function(api){
    return function(dispatch){
        return function(action){
            let {type, types, url, data, method = `get`, name} = action;
            if(!url){
               return dispatch(action)
            }

            dispatch({type: constants.Requesting})

            http[method](url, data).then((res) => {
                let _action = {
                    type: constants.Requested,
                    name,
                    result: res.data
                }
                dispatch(_action)
            }).catch((error) => {
                dispatch({type: constants.RequestError})
            })
        }
    }
}

store.js

import React from `react`
import {createStore, applyMiddleware} from `redux`

import rootReducer from `./rootReducer`

import middleware from `./middleware`

const store = createStore(rootReducer, applyMiddleware(middleware));

export default store;

app.js

import `./libs/bootstrap/css/bootstrap.min.css`
import `./libs/font-awesome/css/font-awesome.min.css`

import React from `react`
import ReactDOM from `react-dom`
import {Provider} from `react-redux`

import store from `./redux/configStore`
import CNodeComponent from `./components/cnode/cnode`

ReactDOM.render(
    <Provider store={store}>
        <CNodeComponent/>
    </Provider>,
    document.getElementById(`app`)
)

小結

  • 元件 datagrid 支援動態配置
  • ajax請求放到了中介軟體執行
  • ajax請求為了統一處理,分為三個狀態,就是constants.js檔案中的三個變數,分別代表請求前,請求中,請求後
  • 請求前顯示載入元件spinner,請求結束後移除載入元件spinner

相關文章