Redux教程2:連結React

玄農發表於2016-01-07

通過前面的教程,我們有了簡單的環境,並且可以執行 Redux 的程式,也對 如何編寫Redux示例 有了初步的印象;

掌握了 使用Redux控制狀態轉移,繼而驅動 React 元件發生改變,這才是學習Redux的初衷。

本篇我們將 Redux 和 React 聯合起來,著重講解 redux-react 模組的使用;

1、編寫紅綠燈React元件

在原有的基礎上,我們編寫紅綠燈元件:

touch components/light/index.js components/light/index.less

在 components/light/index.js 中寫React程式碼,其結構非常簡單:

import React, { PropTypes, Component } from `react`
import { render } from `react-dom`
import classnames from `classnames`
import `./index.less`

class Light extends Component{
    render(){
        let color = this.props.light.color;
        return(
            <div className="traffic-light">
                <span className={classnames(`light`,color)} />
            </div>
        )
    }
}

Light.propTypes = {
    light: PropTypes.object.isRequired
}

Light.defaultProps = {
    light : {color:`red`,time:`4`}
}

export default Light

根據更改樣式類名(`red`、`green`、`yellow`),從而移動 sprite圖 產生燈變換的效果:

.traffic-light{
  .light{
    display: inline-block;
    background: url(//lh3.googleusercontent.com/-YWLqWZXDYHU/VmWC7GHoAuI/AAAAAAAACgk/nXvEmSWAhQU/s800/light.png) no-repeat 0 0;
    background-size: auto 100%;
    overflow: hidden;
    width:140px / 2;
    height:328px / 2;

    &.red{
      background-position: 0,0;
    }
    &.yellow{
      background-position: -78px , 0;
    }
    &.green{
      background-position: -156px , 0;
    }
  }
}

修改 components/light/demo.js 檔案程式碼為:

import React, {Component, PropTypes} from `react`
import {render} from `react-dom`
import Light from `./index`

var color = `red`;

render(
    <div id="traffic">
        <Light color={color}/>
    </div>,
    document.getElementById(`demo`)
)

這樣就能通過 http://localhost:3000/light/demo 預覽這個元件了;

demo light

2、連結React和redux

有了React和之前的Redux,現在就要將兩者連結起來了。我們的目標是讓紅綠燈執行起來,就好比平時在十字路口看到的那樣;

2.1、建立示例檔案

再建立一個示例檔案,就不叫demo了,叫做redux好了:

touch components/light/redux.js

之所以示例檔名稱為 demo.js 或 redux.js ,是因為我在 webpack.config.js 中配置了,如果想用其他的檔名,只要依樣畫葫蘆就可以;

首先在 components/light/redux.js 中輸入最基本的腳手架程式碼,引入所需要的元件或模組:

import React, {Component, PropTypes} from `react`
import {render} from `react-dom`
import { Provider, connect } from `react-redux`
import { bindActionCreators } from `redux`
import * as LightActions from `../../actions/light/`
import lightStore from `../../stores/light/`
import Light from `./index`

// 宣告store
let store = lightStore();

2.2、建立容器React

繼而建立一個 App React類 ,作為總的容器,將上述的 Light 元件放入其中:

import React, {Component, PropTypes} from `react`
import {render} from `react-dom`
import { Provider, connect } from `react-redux`
import { bindActionCreators } from `redux`
import * as LightActions from `../../actions/light/`
import lightStore from `../../stores/light/`
import Light from `./index`

// 宣告store
let store = lightStore();

class App extends Component{
    _bind(...methods){
        methods.forEach((method)=>this[method] = this[method].bind(this));
    }
    constructor(){
        super();
        this._bind(`autoChange`,`handleClick`);
        this.state = {
            count : 0,
            timeId : null
        }
    }

    autoChange(){ // 自動更改紅綠燈
        var _self = this;

        // 這裡放置邏輯程式碼

        this.state.timeId = setTimeout(function(){
            // 遞迴呼叫,實現 setInterval 方法
            _self.autoChange();
        },1000);
    }
    handleClick(e){  // 用點選模擬紅路燈

        if(this.state.timeId){
            clearTimeout(this.state.timeId);
            this.state.timeId = null;
        } else {
            this.autoChange();
        }

    }
    render(){
        // 通過connect 注入 redux 的 dispatch 方法
        return (
            <div id="traffic" onClick={this.handleClick}>
                <Light light={`yellow`}/>
            </div>
        )
    }
}

上面的程式碼還是個半成品,看不到效果;簡單描述一下上面的程式碼做了什麼:

  • 定義 App 容器,將 Light 元件放在其 render 方法中
  • constructor 方法引用了 _bind方法,方便一次性繫結 this 上下文,該方法來自文章 Refactoring React Components to ES6 Classes
  • handleClick 方法是純粹是為了演示,當使用者點選紅綠燈的時候,紅綠燈呼叫 autoChange方法 開始自動變換,使用者再次點選的時候就停止變換;
  • autoChange 方法用於紅綠燈狀態自動轉換的,這裡佔位;本質是使用 setTimeout 代替 setInterval 實現;

2.3、連結React元件和Redux類

這是最為關鍵的一個步驟,


  ...


class App extends Component{

    ...
}

// 宣告 connect 連線
// 將 redux 中的 state傳給 App
function mapStateToProps(state){
    return{
        light:state
    }
}

function mapDispatchToProps(dispatch){
    return{
        actions : bindActionCreators(LightActions,dispatch)
    }
}

// 宣告 connect 連線
App = connect(mapStateToProps,mapDispatchToProps)(App);

// 真正的連線
render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById(`demo`)
)

這裡使用 react-redux 提供 connect 的方法 連結React元件和Redux類

// 宣告 connect 連線
App = connect(mapStateToProps,mapDispatchToProps)(App);
  • connect 方法不會改變原來的元件類,反而返回一個新的 已與 Redux store 連線的 元件類。注意這裡並沒有注入store 物件,真正 store 物件的注入靠最後的 <Provider store>元件;(更多說明請參考 [react-redux 的 API][1])
  • 傳入 connect 的 mapStateToProps方法 ,正如其名,是將 Redux 的狀態 對映到 React元件的props屬性。任何時候,**只要 Redux store 發生改變,mapStateToProps 函式就會被呼叫**。這裡返回物件是 {light:state},這樣確保 Redux 中的 state 發生改變時,元件的 props.light 都是最新的 Redux state。
  • mapDispatchToProps方法 則是將 Store 中的 dispatch方法 直接封裝成物件的一個屬性,一般會用到 Redux 的輔助函式 bindActionCreators();這裡將 dispatch 繫結到 action屬性,這樣在紅綠燈元件內讓其變成紅燈的時候,不需要 dispatch(changeRed()) 這麼呼叫,直接使用 actions.changeRed(),語義化更好;(更多說明請參考 [react-redux 的 API][1])
  • 最後的 <Provider store> 使元件層級中的 connect() 方法都能夠獲得 Redux store,這裡才真正注入store變數,之前的只是宣告而已(之前的好比store是個形參,到了這一步store就是實參了)。(更多說明請參考 [react-redux 的 API][1])

經過上面的語句,Redux就將 state屬性 、 (**store** 的) dispatch方法 與 R
eact 元件的 props 繫結在一起,凡是更改 redux 的 states,就會更新所連線元件的 props 屬性。

react-redux 中的 connect 方法就算是HOC(High Order Component,高階元件)了,具體原理可參考文章 初識React中的High Order Component,這是因為如果使用ES6 寫React元件的話,mixin是不支援的
,因此使用High Order Component代替;

2.4、利用redux驅動react

理解了最為困難的部分,之後的事情就水到渠成了;

現在,只要記住 在App中可以直接使用Redux中的一切了 就行了

我們回過頭來,完善 App 元件的程式碼,完善 autoChange方法:


...

class App extends Component{
    _bind(...methods){
        methods.forEach((method)=>this[method] = this[method].bind(this));
    }
    constructor(){
        super();
        this._bind(`changeColor`,`handleClick`,`autoChange`);
        this.state = {
            count : 0,
            timeId : null
        }
    }
    changeColor(light,actions){ // 紅路燈變換規則
        switch(light.color){
            case `red`:
                actions.changeGreen();
                break;
            case `green`:
                actions.changeYellow();
                break;
            case `yellow`:
                actions.changeRed();
                break;
            default:
                actions.changeRed();
        }       
    }
    autoChange(){ // 自動更改紅綠燈
        const { light, actions } = this.props;
        let _self = this;

        let curCount = ++this.state.count;

        // console.log(`xx,`,curCount);
        if(this.state.count > +light.time){
            curCount = 0;
            this.changeColor(light,actions);
        }
        // 自動更改
        this.state.timeId = setTimeout(function(){
            _self.setState({count:curCount});
            _self.autoChange();
        },1000);

    }
    handleClick(e){  // 用點選模擬紅路燈

        if(this.state.timeId){
            clearTimeout(this.state.timeId);
        } else {
            this.autoChange();
        }

    }
    render(){
        // 通過connect 注入 redux 的 dispatch 方法
        const { light, actions } = this.props;
        return (
            <div id="traffic" onClick={this.handleClick.bind(this)}>
                <Light light={light}/>
            </div>
        )
    }
}

...

至此已經完成本節示例,通過 npm start 開啟服務, 在 http://localhost:3000/light/redux 中檢視。

在這個示例裡,通過點選紅綠燈,每隔若干秒紅綠燈就會變換顏色,這說明兩者已經連結起來;

demo light

(這個是gif圖,如果沒動畫請點選在新視窗開啟)

在後一篇文章,將示例如何處理多個Redux、React的情形;

[1] http://camsong.github.io/redux-in-chinese/docs/react-redux/api.html


相關文章