深入理解React 高階元件

舞動乾坤發表於2017-10-21

React 中的五種元件形式

目前的前端開發主流技術都已經往元件化方向發展了,而每學一種新的框架的時候,最基礎的部分一定是學習其元件的編寫方式。這就好像學習一門新的程式語言的時候,總是要從hello world開始一樣。而在React中,我們常用的元件編寫方式又有哪些呢?或者說各種不同的元件又可以分為幾類呢?

無狀態元件

無狀態元件(Stateless Component)是最基礎的元件形式,由於沒有狀態的影響所以就是純靜態展示的作用。一般來說,各種UI庫裡也是最開始會開發的元件類別。如按鈕、標籤、輸入框等。它的基本組成結構就是屬性(props)加上一個渲染函式(render)。由於不涉及到狀態的更新,所以這種元件的複用性也最強。

const PureComponent = (props) => (
    <div>
        //use props
    </div>
)複製程式碼

無狀態元件的寫法十分簡單,比起使用傳統的元件定義方式,我通常就直接使用ES6語法中提供的箭頭函式來宣告這種元件形式。當然,如果碰到稍微複雜點的,可能還會帶有生命週期的hook函式。這時候就需要用到Class Component的寫法了。

有狀態元件

在無狀態元件的基礎上,如果元件內部包含狀態(state)且狀態隨著事件或者外部的訊息而發生改變的時候,這就構成了有狀態元件(Stateful Component)。有狀態元件通常會帶有生命週期(lifecycle),用以在不同的時刻觸發狀態的更新。這種元件也是通常在寫業務邏輯中最經常使用到的,根據不同的業務場景元件的狀態數量以及生命週期機制也不盡相同。

class StatefulComponent extends Component {

    constructor(props) {
        super(props);
        this.state = {
            //定義狀態
        }
    }

    componentWillMount() {
        //do something
    }

    componentDidMount() {
        //do something
    }
    ... //其他生命週期

    render() {
        return (
            //render
        );
    }
}複製程式碼

容器元件

在具體的專案實踐中,我們通常的前端資料都是通過Ajax請求獲取的,而且獲取的後端資料也需要進一步的做處理。為了使元件的職責更加單一,引入了容器元件(Container Component)的概念。我們將資料獲取以及處理的邏輯放在容器元件中,使得元件的耦合性進一步地降低。

var UserListContainer = React.createClass({
  getInitialState: function() {
    return {
      users: []
    }
  },

  componentDidMount: function() {
    var _this = this;
    axios.get('/path/to/user-api').then(function(response) {
      _this.setState({users: response.data});
    });
  },

  render: function() {
    return (<UserList users={this.state.users} />);
  }
});複製程式碼

如上面這個容器元件,就是負責獲取使用者資料,然後以props的形式傳遞給UserList元件來渲染。容器元件也不會在頁面中渲染出具體的DOM節點,因此,它通常就充當資料來源的角色。目前很多常用的框架,也都採用這種元件形式。如:React Redux的connect(), Relay的createContainer(), Flux Utils的Container.create()等。

高階元件

其實對於一般的中小專案來說,你只需要用到以上的這三種元件方式就可以很好地構造出所需的應用了。但是當面對複雜的需求的時候,我們往往可以利用高階元件(Higher-Order Component)編寫出可重用性更強的元件。那麼什麼是高階元件呢?其實它和高階函式的概念類似,就是一個會返回元件的元件。或者更確切地說,它其實是一個會返回元件的函式。就像這樣:

const HigherOrderComponent = (WrappedComponent) => {
  return class WrapperComponent extends Component {
    render() {
      //do something with WrappedComponent
    }
  }
}複製程式碼

做為一個高階元件,可以在原有元件的基礎上,對其增加新的功能和行為。我們一般希望編寫的元件儘量純淨或者說其中的業務邏輯儘量單一。但是如果各種元件間又需要增加新功能,如列印日誌,獲取資料和校驗資料等和展示無關的邏輯的時候,這些公共的程式碼就會被重複寫很多遍。因此,我們可以抽象出一個高階元件,用以給基礎的元件增加這些功能,類似於外掛的效果。

一個比較常見的例子是表單的校驗。

//檢驗規則,表格元件
const FormValidator = (WrappedComponent, validator, trigger) => {

   getTrigger(trigger, validator) {
      var originTrigger = this.props[trigger];

      return function(event) {
          //觸發驗證機制,更新狀態
          // do something ...
          originTrigger(event);
      }
  }

  var newProps = {
    ...this.props,
    [trigger]:   this.getTrigger(trigger, validator) //觸發時機,重新繫結原有觸發機制
  };

  return <WrappedComponent  {...newProps} />
}複製程式碼

值得提一句,同樣是給元件增加新功能的方法,相比於使用mixins這種方式高階元件則更加簡潔和職責更加單一。你如果使用過多個mixins的時候,狀態汙染就十分容易發生,以及你很難從元件的定義上看出隱含在mixins中的邏輯。而高階元件的處理方式則更加容易維護。

另一方面,ES7中新的語法Decorator也可以用來實現和上面寫法一樣的效果。

function LogDecorator(msg) {
  return (WrappedComponent) => {
    return class LogHoc extends Component {
      render() {
        // do something with this component
        console.log(msg);
        <WrappedComponent {...this.props} />
      }
    }
  }
}

@LogDecorator('hello world')
class HelloComponent extends Component {

  render() {
    //...
  }
}複製程式碼

Render Callback元件

還有一種元件模式是在元件中使用渲染回撥的方式,將元件中的渲染邏輯委託給其子元件。就像這樣:

import { Component } from "react";

class RenderCallbackCmp extends Component {
  constructor(props) {
    super(props);
    this.state = {
      msg: "hello"
    };
  }

  render() {
    return this.props.children(this.state.msg);
  }
}

const ParentComponent = () =>
  (<RenderCallbackCmp>
    {msg =>
      //use the msg
      <div>
        {msg}
      </div>}
  </RenderCallbackCmp>);複製程式碼

父元件獲取了內部的渲染邏輯,因此在需要控制渲染機制時可以使用這種元件形式。







1. 基本概念

高階元件是React 中一個很重要且較複雜的概念,高階元件在很多第三方庫(如Redux)中都被經常使用,即使你開發的是普通的業務專案,用好高階元件也能顯著提高你的程式碼質量。

高階元件的定義是類比於高階函式的定義。高階函式接收函式作為引數,並且返回值也是一個函式。類似的,高階元件接收React元件作為引數,並且返回一個新的React元件。高階元件本質上也是一個函式,並不是一個元件,這一點一定要注意。

2. 應用場景

為什麼React引入高階元件的概念?它到底有何威力?讓我們先通過一個簡單的例子說明一下。

假設我有一個元件,需要從LocalStorage中獲取資料,然後渲染出來。於是我們可以這樣寫元件程式碼:

import React, { Component } from 'react'

class MyComponent extends Component {

  componentWillMount() {
      let data = localStorage.getItem('data');
      this.setState({data});
  }

  render() {
    return <div>{this.state.data}</div>
  }
}複製程式碼

程式碼很簡單,但當我有其他元件也需要從LocalStorage中獲取同樣的資料展示出來時,我需要在每個元件都重複componentWillMount中的程式碼,這顯然是很冗餘的。下面讓我們來看看使用高階元件可以怎麼改寫這部分程式碼。

import React, { Component } from 'react'

function withPersistentData(WrappedComponent) {
  return class extends Component {
    componentWillMount() {
      let data = localStorage.getItem('data');
        this.setState({data});
    }

    render() {
      // 通過{...this.props} 把傳遞給當前元件的屬性繼續傳遞給被包裝的元件WrappedComponent
      return <WrappedComponent data={this.state.data} {...this.props} />
    }
  }
}

class MyComponent2 extends Component {  
  render() {
    return <div>{this.props.data}</div>
  }
}

const MyComponentWithPersistentData = withPersistentData(MyComponent2)複製程式碼

withPersistentData就是一個高階元件,它返回一個新的元件,在新元件的componentWillMount中統一處理從LocalStorage中獲取資料的邏輯,然後將獲取到的資料以屬性的方式傳遞給被包裝的元件WrappedComponent,這樣在WrappedComponent中就可以直接使用this.props.data獲取需要展示的資料了,如MyComponent2所示。當有其他的元件也需要這段邏輯時,繼續使用withPersistentData這個高階元件包裝這些元件就可以了。

通過這個例子,可以看出高階元件的主要功能是封裝並分離元件的通用邏輯,讓通用邏輯在元件間更好地被複用。高階元件的這種實現方式,本質上是一個裝飾者設計模式。

高階元件的引數並非只能是一個元件,它還可以接收其他引數。例如,元件MyComponent3需要從LocalStorage中獲取key為name的資料,而不是上面例子中寫死的key為data的資料,withPersistentData這個高階元件就不滿足我們的需求了。我們可以讓它接收額外的一個引數,來決定從LocalStorage中獲取哪個資料:

import React, { Component } from 'react'

function withPersistentData(WrappedComponent, key) {
  return class extends Component {
    componentWillMount() {
      let data = localStorage.getItem(key);
        this.setState({data});
    }

    render() {
      // 通過{...this.props} 把傳遞給當前元件的屬性繼續傳遞給被包裝的元件WrappedComponent
      return <WrappedComponent data={this.state.data} {...this.props} />
    }
  }
}

class MyComponent2 extends Component {  
  render() {
    return <div>{this.props.data}</div>
  }

  //省略其他邏輯...
}

class MyComponent3 extends Component {  
  render() {
    return <div>{this.props.data}</div>
  }

  //省略其他邏輯...
}

const MyComponent2WithPersistentData = withPersistentData(MyComponent2, 'data');
const MyComponent3WithPersistentData = withPersistentData(MyComponent3, 'name');複製程式碼

新版本的withPersistentData就滿足我們獲取不同key值的需求了。高階元件中的引數當然也可以有函式,我們將在下一節進一步說明。

3. 進階用法

高階元件最常見的函式簽名形式是這樣的:

HOC([param])([WrappedComponent])

用這種形式改寫withPersistentData,如下:

import React, { Component } from 'react'

function withPersistentData = (key) => (WrappedComponent) => {
  return class extends Component {
    componentWillMount() {
      let data = localStorage.getItem(key);
        this.setState({data});
    }

    render() {
      // 通過{...this.props} 把傳遞給當前元件的屬性繼續傳遞給被包裝的元件WrappedComponent
      return <WrappedComponent data={this.state.data} {...this.props} />
    }
  }
}

class MyComponent2 extends Component {  
  render() {
    return <div>{this.props.data}</div>
  }

  //省略其他邏輯...
}

class MyComponent3 extends Component {  
  render() {
    return <div>{this.props.data}</div>
  }

  //省略其他邏輯...
}

const MyComponent2WithPersistentData = withPersistentData('data')(MyComponent2);
const MyComponent3WithPersistentData = withPersistentData('name')(MyComponent3);複製程式碼

實際上,此時的withPersistentData和我們最初對高階元件的定義已經不同。它已經變成了一個高階函式,但這個高階函式的返回值是一個高階元件。我們可以把它看成高階元件的變種形式。這種形式的高階元件大量出現在第三方庫中。如react-redux中的connect就是一個典型。connect的定義如下:

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])複製程式碼

這個函式會將一個React元件連線到Redux 的 store。在連線的過程中,connect通過函式引數mapStateToProps,從全域性store中取出當前元件需要的state,並把state轉化成當前元件的props;同時通過函式引數mapDispatchToProps,把當前元件用到的Redux的action creator,以props的方式傳遞給當前元件。connect並不會修改傳遞進去的元件的定義,而是它會返回一個新的元件。

例如,我們把元件ComponentA連線到Redux上的寫法類似於:

const ConnectedComponentA = connect(componentASelector, componentAActions)(ComponentA);複製程式碼

我們可以把它拆分來看:

// connect 是一個函式,返回值enhance也是一個函式
const enhance = connect(componentASelector, componentAActions);
// enhance是一個高階元件
const ConnectedComponentA = enhance(ComponentA);複製程式碼

當多個函式的輸出和它的輸入型別相同時,這些函式是很容易組合到一起使用的。例如,有f,g,h三個高階元件,都只接受一個元件作為引數,於是我們可以很方便的巢狀使用它們:f( g( h(WrappedComponent) ) )。這裡可以有一個例外,即最內層的高階元件h可以有多個引數,但其他高階元件必須只能接收一個引數,只有這樣才能保證內層的函式返回值和外層的函式引數數量一致(都只有1個)。

例如我們將connect和另一個列印日誌的高階元件withLog聯合使用:

const ConnectedComponentA = connect(componentASelector)(withLog(ComponentA));複製程式碼

這裡我們定義一個工具函式:compose(...functions),呼叫compose(f, g, h)等價於 (...args) => f(g(h(...args)))。用compose函式我們可以把高階元件巢狀的寫法打平:

const enhance = compose(
  connect(componentASelector),
  withLog
);
const ConnectedComponentA = enhance(ComponentA);複製程式碼

像Redux等很多第三方庫都提供了compose的實現,compose結合高階元件使用,可以顯著提高程式碼的可讀性和邏輯的清晰度。

4.與父元件區別

有些同學可能會覺得高階元件有些類似父元件的使用。例如,我們完全可以把高階元件中的邏輯放到一個父元件中去執行,執行完成的結果再傳遞給子元件。從邏輯的執行流程上來看,高階元件確實和父元件比較相像,但是高階元件強調的是邏輯的抽象。高階元件是一個函式,函式關注的是邏輯;父元件是一個元件,元件主要關注的是UI/DOM。如果邏輯是與DOM直接相關的,那麼這部分邏輯適合放到父元件中實現;如果邏輯是與DOM不直接相關的,那麼這部分邏輯適合使用高階元件抽象,如資料校驗、請求傳送等。

5. 注意事項

1)不要在元件的render方法中使用高階元件,儘量也不要在元件的其他生命週期方法中使用高階元件。因為高階元件每次都會返回一個新的元件,在render中使用會導致每次渲染出來的元件都不相等(===),於是每次render,元件都會解除安裝(unmount),然後重新掛載(mount),既影響了效率,又丟失了元件及其子元件的狀態。高階元件最適合使用的地方是在元件定義的外部,這樣就不會受到元件生命週期的影響了。

2)如果需要使用被包裝元件的靜態方法,那麼必須手動拷貝這些靜態方法。因為高階元件返回的新元件,是不包含被包裝元件的靜態方法。hoist-non-react-statics可以幫助我們方便的拷貝元件所有的自定義靜態方法。有興趣的同學可以自行了解。

3)Refs不會被傳遞給被包裝元件。儘管在定義高階元件時,我們會把所有的屬性都傳遞給被包裝元件,但是ref並不會傳遞給被包裝元件,因為ref根本不屬於React元件的屬性。如果你在高階元件的返回元件中定義了ref,那麼它指向的是這個返回的新元件,而不是內部被包裝的元件。如果你希望獲取被包裝元件的引用,你可以把ref的回撥函式定義成一個普通屬性(給它一個ref以外的名字)。下面的例子就用inputRef這個屬性名代替了常規的ref命名:

function FocusInput({ inputRef, ...rest }) {
  return <input ref={inputRef} {...rest} />;
}

//enhance 是一個高階元件
const EnhanceInput = enhance(FocusInput);

// 在一個元件的render方法中...
return (<EnhanceInput 
  inputRef={(input) => {
    this.input = input
  }
}>)

// 讓FocusInput自動獲取焦點
this.input.focus();複製程式碼




- 首先我們來看看登陸的 Reducer

export const auth = (state = initialState, action = {}) => {
  switch (action.type) {
    case LOGIN_USER:
      return state.merge({
        'user': action.data,
        'error': null,
        'token': null,
      });
    case LOGIN_USER_SUCCESS:
      return state.merge({
        'token': action.data,
        'error': null
      });
    case LOGIN_USER_FAILURE:
      return state.merge({
        'token': null,
        'error': action.data
      });
    default:
      return state
  }
};
複製程式碼

Sagas 監聽發起的 action,然後決定基於這個 action 來做什麼:是發起一個非同步呼叫(比如一個 Ajax 請求),還是發起其他的 action 到 Store,甚至是呼叫其他的 Sagas。

具體到這個登陸功能就是我們在登陸彈窗點選登陸時會發出一個 LOGIN_USER action,Sagas 監聽到 LOGIN_USER action,發起一個 Ajax 請求到後臺,根據結果決定發起 LOGIN_USER_SUCCESSaction 還是LOGIN_USER_FAILUREaction

接下來,我們來實現這個流程

  • 建立 Saga middleware 連線至 Redux store

在 package.json 中新增 redux-saga 依賴

"redux-saga": "^0.15.4"

修改 src/redux/store/store.js

/**
 * Created by Yuicon on 2017/6/27.
 */
import {createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga'
import reducer from '../reducer/reducer';

import rootSaga from '../sagas/sagas';

const sagaMiddleware = createSagaMiddleware();

const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
);

sagaMiddleware.run(rootSaga);

export default store;

複製程式碼

Redux-saga 使用 Generator 函式實現

  • 監聽 action

建立 src/redux/sagas/sagas.js

/**
 * Created by Yuicon on 2017/6/28.
 */
import { takeLatest } from 'redux-saga/effects';
import {registerUserAsync, loginUserAsync} from './users';
import {REGISTER_USER, LOGIN_USER} from '../action/users';

export default function* rootSaga() {
  yield [
    takeLatest(REGISTER_USER, registerUserAsync),
    takeLatest(LOGIN_USER, loginUserAsync)
  ];
}
複製程式碼

我們可以看到在 rootSaga 中監聽了兩個 action 登陸和註冊 。

在上面的例子中,takeLatest 只允許執行一個 loginUserAsync 任務。並且這個任務是最後被啟動的那個。 如果之前已經有一個任務在執行,那之前的這個任務會自動被取消。

如果我們允許多個 loginUserAsync 例項同時啟動。在某個特定時刻,我們可以啟動一個新 loginUserAsync 任務, 儘管之前還有一個或多個 loginUserAsync 尚未結束。我們可以使用 takeEvery 輔助函式。

  • 發起一個 Ajax 請求
  • 獲取 Store state 上的資料

selectors.js

/**
 * Created by Yuicon on 2017/6/28.
 */
export const getAuth = state => state.auth;
複製程式碼
  • api

api.js

/**
 * Created by Yuicon on 2017/7/4.
 * https://github.com/Yuicon
 */

/**
 * 這是我自己的後臺伺服器,用 Java 實現
 * 專案地址:https://github.com/DigAg/digag-server
 * 文件:http://139.224.135.86:8080/swagger-ui.html#/
 */
const getURL = (url) => `http://139.224.135.86:8080/${url}`;

export const login = (user) => {
  return fetch(getURL("auth/login"), {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(user)
  }).then(response => response.json())
    .then(json => {
      return json;
    })
    .catch(ex => console.log('parsing failed', ex));
};

複製程式碼
  • 建立 src/redux/sagas/users.js
/**
 * Created by Yuicon on 2017/6/30.
 */
import {select, put, call} from 'redux-saga/effects';
import {getAuth, getUsers} from './selectors';
import {loginSuccessAction, loginFailureAction, registerSuccessAction, registerFailureAction} from '../action/users';
import {login, register} from './api';
import 'whatwg-fetch';

export function* loginUserAsync() {
  // 獲取Store state 上的資料
  const auth = yield select(getAuth);
  const user = auth.get('user');
  // 發起 ajax 請求
  const json = yield call(login.bind(this, user), 'login');
  if (json.success) {
    localStorage.setItem('token', json.data);
    // 發起 loginSuccessAction
    yield put(loginSuccessAction(json.data));
  } else {
    // 發起 loginFailureAction
    yield put(loginFailureAction(json.error));
  }
}
複製程式碼

select(selector, ...args) 用於獲取Store state 上的資料
put(action) 發起一個 action 到 Store
call(fn, ...args) 呼叫 fn 函式並以 args 為引數,如果結果是一個 Promise,middleware 會暫停直到這個 Promise 被 resolve,resolve 後 Generator 會繼續執行。 或者直到 Promise 被 reject 了,如果是這種情況,將在 Generator 中丟擲一個錯誤。

Redux-saga 詳細api文件

  • 結語

我在工作時用的是 Redux-Thunk, Redux-Thunk 相對來說更容易實現和維護。但是對於複雜的操作,尤其是面對複雜非同步操作時,Redux-saga 更有優勢。到此我們完成了一個 Redux-saga 的入門教程,Redux-saga 還有很多奇妙的地方,大家可以自行探索。

上回說到用React寫了一個帶Header的首頁,我們這次實踐就使用Redux進行狀態管理

深入理解React 高階元件

Rudex

應用中所有的 state 都以一個物件樹的形式儲存在一個單一的 store 中。
惟一改變 state 的辦法是觸發 action,一個描述發生什麼的物件。
為了描述 action 如何改變 state 樹,你需要編寫 reducers。

我們接下來開始開始進行登陸與註冊的狀態管理

首先在 src 目錄下建立 redux 資料夾,目錄如下

digag
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── public
│   └── favicon.ico
│   └── index.html
│   └── manifest.json
└── src
    └── components
        └── Index
            └── Header.js
            └── LoginDialog.js
            └── RegisterDialog.js
    └── containers
        └── App
            └── App.js
            └── App.css
    └── redux
        └── action
            └── users.js
        └── reducer
            └── auth.js
            └── users.js
        └── sagas
            └── api.js
            └── sagas.js
            └── selectors.js.js
            └── users.js
        └── store
            └── store.js
    └── App.test.js
    └── index.css
    └── index.js
    └── logo.svg
    └── registerServiceWorker.js
複製程式碼

程式碼可從此獲取

記得在 package.json 中更新依賴

接下來我會開始解釋關鍵程式碼

  • action
    action/users.js
/*
 * action 型別
 */
export const REGISTER_USER = 'REGISTER_USER';
// 省略其他action 型別

/*
 * action 建立函式
 */
export const registerAction = (newUser) => {
  return{
    type:REGISTER_USER,
    data: newUser,
  }
};
// 省略其他 action 建立函式
複製程式碼
  • reducer
    reducer/users.js
//Immutable Data 就是一旦建立,就不能再被更改的資料。
//對 Immutable 物件的任何修改或新增刪除操作都會返回一個新的 Immutable 物件。
import Immutable from 'immutable';
//從 action 匯入需要的 action 型別
import {REGISTER_USER, REGISTER_USER_SUCCESS, REGISTER_USER_FAILURE} from '../action/users';

// 初始化狀態
const initialState = Immutable.fromJS({
  newUser: null,
  error: null,
  saveSuccess: false,
});

//  reducer 就是一個純函式,接收舊的 state 和 action,返回新的 state。
export const users = (state = initialState, action = {}) => {
  switch (action.type) { // 判斷 action 型別
    case REGISTER_USER:  
      return state.merge({   // 更新狀態
        'newUser': action.data,
        'saveSuccess': false,
        'error': null,
      });
    case REGISTER_USER_SUCCESS:
      return state.set('saveSuccess', action.data);
    case REGISTER_USER_FAILURE:
      return state.set('error', action.data);
    default:
      return state
  }
};
複製程式碼
  • store
    store/store.js
import {createStore, combineReducers, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga'
import * as reducer from '../reducer/users';

import rootSaga from '../sagas/sagas';

const sagaMiddleware = createSagaMiddleware();

const store = createStore(
  combineReducers(reducer),
  applyMiddleware(sagaMiddleware)
);

sagaMiddleware.run(rootSaga);

export default store;
複製程式碼

然後在入口檔案使用 store

src/index.js

import {Provider} from 'react-redux';
import store from './redux/store/store';
// 省略其他

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>, document.getElementById('root')
);
複製程式碼

在 App.js 中獲取 action 和 狀態

import {registerAction, loginAction} from '../../redux/action/users';
import {connect} from "react-redux";
import {bindActionCreators} from "redux";
 //省略其他

class App extends Component {

  render(){
    return(
      <div className="App">
        //省略
      </div>
    )
  }

}

export default connect(
  (state) => {
// 獲取狀態   state.users  是指 reducer/users.js 檔案中匯出的 users
// 可以 `console.log(state);` 檢視狀態樹
  return { users: state.users }
},
  (dispatch) => {
  return {
// 建立action
    registerActions: bindActionCreators(registerAction, dispatch),
    loginActions: bindActionCreators(loginAction, dispatch),
  }
})(App);
// 在App 元件的props裡就有 this.props.users  this.props.registerActions this.props.loginActions 了
// 需要注意的是這裡this.props.users是Immutable 物件,取值需要用this.props.users.get('newUser') 
// 也可在 reducer 裡改用 js 普通物件
複製程式碼

裝飾器版本:
需要在Babel中開啟裝飾器
裝飾器外掛babel-plugin-transform-decorators-legacy

@connect(
  (state) => {
    console.log(state);
    return ({
      users: state.users,
    });
  },
  {registerActions: registerAction, loginActions: loginAction}
)
複製程式碼

最後把 registerActions 傳給RegisterDialog子元件,

src/components/Index/RegisterDialog.js

// 省略其他程式碼
 handleSubmit = (e) => {
    e.preventDefault();
    // 驗證表單資料
    this.refs.user.validate((valid) => {
      if (valid) {
        // this.state.user 為表單收集的 使用者註冊資料
        this.props.registerActions(this.state.user);
        this.setState({loading: true});
      }
    });
  };

複製程式碼

流程是:

  • 呼叫 action
    this.props.registerActions(this.state.user);
    返回action 為
{
    type:REGISTER_USER,
    data: this.state.user,
}
複製程式碼
  • reducer 根據action型別更新狀態
switch (action.type) {
    case REGISTER_USER:
      return state.merge({
        'newUser': action.data,
        'saveSuccess': false,
        'error': null,
      });
//省略其他程式碼
複製程式碼

這時我們的store裡的狀態 newUser就被更新為 註冊彈窗裡收集的資料
到這裡都還是同步的action,而註冊是一個非同步的操作。
下篇文章會介紹如何使用 redux-saga 進行非同步操作。
redux-saga 已經在使用了,有興趣的可以自行檢視程式碼理解。

- 首先我們來看看登陸的 Reducer

export const auth = (state = initialState, action = {}) => {
  switch (action.type) {
    case LOGIN_USER:
      return state.merge({
        'user': action.data,
        'error': null,
        'token': null,
      });
    case LOGIN_USER_SUCCESS:
      return state.merge({
        'token': action.data,
        'error': null
      });
    case LOGIN_USER_FAILURE:
      return state.merge({
        'token': null,
        'error': action.data
      });
    default:
      return state
  }
};
複製程式碼

Sagas 監聽發起的 action,然後決定基於這個 action 來做什麼:是發起一個非同步呼叫(比如一個 Ajax 請求),還是發起其他的 action 到 Store,甚至是呼叫其他的 Sagas。

具體到這個登陸功能就是我們在登陸彈窗點選登陸時會發出一個 LOGIN_USER action,Sagas 監聽到 LOGIN_USER action,發起一個 Ajax 請求到後臺,根據結果決定發起 LOGIN_USER_SUCCESSaction 還是LOGIN_USER_FAILUREaction

接下來,我們來實現這個流程

  • 建立 Saga middleware 連線至 Redux store

在 package.json 中新增 redux-saga 依賴

"redux-saga": "^0.15.4"

修改 src/redux/store/store.js

/**
 * Created by Yuicon on 2017/6/27.
 */
import {createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga'
import reducer from '../reducer/reducer';

import rootSaga from '../sagas/sagas';

const sagaMiddleware = createSagaMiddleware();

const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
);

sagaMiddleware.run(rootSaga);

export default store;

複製程式碼

Redux-saga 使用 Generator 函式實現

  • 監聽 action

建立 src/redux/sagas/sagas.js

/**
 * Created by Yuicon on 2017/6/28.
 */
import { takeLatest } from 'redux-saga/effects';
import {registerUserAsync, loginUserAsync} from './users';
import {REGISTER_USER, LOGIN_USER} from '../action/users';

export default function* rootSaga() {
  yield [
    takeLatest(REGISTER_USER, registerUserAsync),
    takeLatest(LOGIN_USER, loginUserAsync)
  ];
}
複製程式碼

我們可以看到在 rootSaga 中監聽了兩個 action 登陸和註冊 。

在上面的例子中,takeLatest 只允許執行一個 loginUserAsync 任務。並且這個任務是最後被啟動的那個。 如果之前已經有一個任務在執行,那之前的這個任務會自動被取消。

如果我們允許多個 loginUserAsync 例項同時啟動。在某個特定時刻,我們可以啟動一個新 loginUserAsync 任務, 儘管之前還有一個或多個 loginUserAsync 尚未結束。我們可以使用 takeEvery 輔助函式。

  • 發起一個 Ajax 請求
  • 獲取 Store state 上的資料

selectors.js

/**
 * Created by Yuicon on 2017/6/28.
 */
export const getAuth = state => state.auth;
複製程式碼
  • api

api.js

/**
 * Created by Yuicon on 2017/7/4.
 * https://github.com/Yuicon
 */

/**
 * 這是我自己的後臺伺服器,用 Java 實現
 * 專案地址:https://github.com/DigAg/digag-server
 * 文件:http://139.224.135.86:8080/swagger-ui.html#/
 */
const getURL = (url) => `http://139.224.135.86:8080/${url}`;

export const login = (user) => {
  return fetch(getURL("auth/login"), {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(user)
  }).then(response => response.json())
    .then(json => {
      return json;
    })
    .catch(ex => console.log('parsing failed', ex));
};

複製程式碼
  • 建立 src/redux/sagas/users.js
/**
 * Created by Yuicon on 2017/6/30.
 */
import {select, put, call} from 'redux-saga/effects';
import {getAuth, getUsers} from './selectors';
import {loginSuccessAction, loginFailureAction, registerSuccessAction, registerFailureAction} from '../action/users';
import {login, register} from './api';
import 'whatwg-fetch';

export function* loginUserAsync() {
  // 獲取Store state 上的資料
  const auth = yield select(getAuth);
  const user = auth.get('user');
  // 發起 ajax 請求
  const json = yield call(login.bind(this, user), 'login');
  if (json.success) {
    localStorage.setItem('token', json.data);
    // 發起 loginSuccessAction
    yield put(loginSuccessAction(json.data));
  } else {
    // 發起 loginFailureAction
    yield put(loginFailureAction(json.error));
  }
}
複製程式碼

select(selector, ...args) 用於獲取Store state 上的資料
put(action) 發起一個 action 到 Store
call(fn, ...args) 呼叫 fn 函式並以 args 為引數,如果結果是一個 Promise,middleware 會暫停直到這個 Promise 被 resolve,resolve 後 Generator 會繼續執行。 或者直到 Promise 被 reject 了,如果是這種情況,將在 Generator 中丟擲一個錯誤。

Redux-saga 詳細api文件

  • 結語

我在工作時用的是 Redux-Thunk, Redux-Thunk 相對來說更容易實現和維護。但是對於複雜的操作,尤其是面對複雜非同步操作時,Redux-saga 更有優勢。到此我們完成了一個 Redux-saga 的入門教程,Redux-saga 還有很多奇妙的地方,大家可以自行探索。


相關文章