深刻理解React 高階組件

React 中的五種組件形式

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

無狀態組件

無狀態組件(Stateless Component)是最基礎的組件形式,因爲沒有狀態的影響因此就是純靜態展現的做用。通常來講,各類UI庫裏也是最開始會開發的組件類別。如按鈕、標籤、輸入框等。它的基本組成結構就是屬性(props)加上一個渲染函數(render)。因爲不涉及到狀態的更新,因此這種組件的複用性也最強。html

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

無狀態組件的寫法十分簡單,比起使用傳統的組件定義方式,我一般就直接使用ES6語法中提供的箭頭函數來聲明這種組件形式。固然,若是碰到稍微複雜點的,可能還會帶有生命週期的hook函數。這時候就須要用到Class Component的寫法了。前端

有狀態組件

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

class StatefulComponent extends Component {

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

    componentWillMount() {
        //do something
    }

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

    render() {
        return (
            //render
        );
    }
}複製代碼

容器組件

在具體的項目實踐中,咱們一般的前端數據都是經過Ajax請求獲取的,並且獲取的後端數據也須要進一步的作處理。爲了使組件的職責更加單一,引入了容器組件(Container Component)的概念。咱們將數據獲取以及處理的邏輯放在容器組件中,使得組件的耦合性進一步地下降。react

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()等。ios

高階組件

其實對於通常的中小項目來講,你只須要用到以上的這三種組件方式就能夠很好地構造出所需的應用了。可是當面對複雜的需求的時候,咱們每每能夠利用高階組件(Higher-Order Component)編寫出可重用性更強的組件。那麼什麼是高階組件呢?其實它和高階函數的概念相似,就是一個會返回組件的組件。或者更確切地說,它實際上是一個會返回組件的函數。就像這樣:git

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

作爲一個高階組件,能夠在原有組件的基礎上,對其增長新的功能和行爲。咱們通常但願編寫的組件儘可能純淨或者說其中的業務邏輯儘可能單一。可是若是各類組件間又須要增長新功能,如打印日誌,獲取數據和校驗數據等和展現無關的邏輯的時候,這些公共的代碼就會被重複寫不少遍。所以,咱們能夠抽象出一個高階組件,用以給基礎的組件增長這些功能,相似於插件的效果。es6

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

//檢驗規則,表格組件
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中的邏輯。而高階組件的處理方式則更加容易維護。ajax

另外一方面,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進行狀態管理

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 還有不少奇妙的地方,你們能夠自行探索。

相關文章
相關標籤/搜索