爲何在Redux中咱們須要中間件來實現異步流?

根據文檔, 「沒有中間件,Redux存儲僅支持同步數據流」 。 我不明白爲何會這樣。 爲何容器組件不能調用異步API,而後dispatch操做? html

例如,想象一個簡單的UI:一個字段和一個按鈕。 當用戶按下按鈕時,該字段將填充來自遠程服務器的數據。 前端

字段和按鈕

import * as React from 'react';
import * as Redux from 'redux';
import { Provider, connect } from 'react-redux';

const ActionTypes = {
    STARTED_UPDATING: 'STARTED_UPDATING',
    UPDATED: 'UPDATED'
};

class AsyncApi {
    static getFieldValue() {
        const promise = new Promise((resolve) => {
            setTimeout(() => {
                resolve(Math.floor(Math.random() * 100));
            }, 1000);
        });
        return promise;
    }
}

class App extends React.Component {
    render() {
        return (
            <div>
                <input value={this.props.field}/>
                <button disabled={this.props.isWaiting} onClick={this.props.update}>Fetch</button>
                {this.props.isWaiting && <div>Waiting...</div>}
            </div>
        );
    }
}
App.propTypes = {
    dispatch: React.PropTypes.func,
    field: React.PropTypes.any,
    isWaiting: React.PropTypes.bool
};

const reducer = (state = { field: 'No data', isWaiting: false }, action) => {
    switch (action.type) {
        case ActionTypes.STARTED_UPDATING:
            return { ...state, isWaiting: true };
        case ActionTypes.UPDATED:
            return { ...state, isWaiting: false, field: action.payload };
        default:
            return state;
    }
};
const store = Redux.createStore(reducer);
const ConnectedApp = connect(
    (state) => {
        return { ...state };
    },
    (dispatch) => {
        return {
            update: () => {
                dispatch({
                    type: ActionTypes.STARTED_UPDATING
                });
                AsyncApi.getFieldValue()
                    .then(result => dispatch({
                        type: ActionTypes.UPDATED,
                        payload: result
                    }));
            }
        };
    })(App);
export default class extends React.Component {
    render() {
        return <Provider store={store}><ConnectedApp/></Provider>;
    }
}

渲染導出的組件後,我能夠單擊按鈕,而且輸入已正確更新。 react

注意connect調用中的update功能。 它調度一個動做,告訴應用程序正在更新,而後執行異步調用。 調用完成後,將提供的值做爲另外一個操做的有效負載進行分派。 git

這種方法有什麼問題? 如文檔所示,我爲何要使用Redux Thunk或Redux Promise? 程序員

編輯:我在Redux倉庫中搜索了線索,發現過去須要Action Creators是純函數。 例如, 如下用戶試圖爲異步數據流提供更好的解釋: es6

動做建立者自己仍然是一個純函數,可是它返回的thunk函數並不須要,它能夠執行異步調用 github

動做建立者再也不須要是純粹的。 所以,過去確定須要使用thunk / promise中間件,但彷佛再也不是這種狀況了? ajax


#1樓

簡短的回答 :對我來講,這彷佛是一種徹底合理的解決異步問題的方法。 有幾個警告。 npm

在剛開始工做的新項目中,我有很是類似的思路。 我是Vanilla Redux優雅的系統的忠實粉絲,該系統用於更新商店和從新渲染組件,而這種方式不影響React組件樹。 我彷佛迷上了那種優雅的dispatch機制來處理異步問題。 編程

最後,我採用了一種很是相似的方法,該方法與我從項目中剔除出來的庫中的庫相似,咱們稱之爲react-redux-controller

因爲如下幾個緣由,我最終沒有采用您所擁有的確切方法:

  1. 按照您編寫的方式,這些分派功能沒法訪問商店。 您能夠經過讓UI組件傳遞分派功能所需的全部信息來解決該問題。 可是我認爲這沒必要要地將這些UI組件耦合到調度邏輯。 更成問題的是,沒有明顯的方法可使分派函數以異步連續的方式訪問更新後的狀態。
  2. 調度功能能夠經過詞法範圍進行dispatch 。 一旦connect語句失控,這將限制重構的選項-僅使用一種update方法就顯得很是笨拙。 所以,若是須要將這些調度程序功能分解爲單獨的模塊,則須要一些系統來組成這些調度程序功能。

總而言之,您必須裝配一些系統以容許dispatch以及將商店以及事件的參數注入到您的調度功能中。 我知道這種依賴注入的三種合理方法:

  • redux-thunk經過將它們傳遞到您的thunk中(經過圓頂定義使其徹底不徹底是thunk)以一種功能性的方式實現了此目的。 我沒有使用其餘dispatch中間件方法,可是我認爲它們基本相同。
  • react-redux-controller用協程執行此操做。 另外,它還使您能夠訪問「選擇器」,這些選擇器是您做爲connect的第一個參數傳遞的功能,而沒必要直接與原始的規範化商店一塊兒使用。
  • 您還能夠經過多種可能的機制將它們注入this上下文中,從而以面向對象的方式進行操做。

更新資料

在我看來,這個難題的一部分是react-redux的侷限性。 connect的第一個參數獲取狀態快照,但不發送。 第二個參數獲取調度,但不獲取狀態。 因爲可以在繼續/回調時看到更新的狀態,所以兩個參數都不會關閉當前狀態。


#2樓

要回答開始時提出的問題:

爲何容器組件不能調用異步API,而後分派操做?

請記住,這些文檔適用於Redux,而不適用於Redux plus React。 鏈接到React組件的 Redux存儲能夠徹底按照您所說的進行操做,可是沒有中間件的Plain Jane Redux存儲不接受除普通ol'對象以外的用於dispatch參數。

沒有中間件,您固然仍然能夠作

const store = createStore(reducer);
MyAPI.doThing().then(resp => store.dispatch(...));

但這是相似的狀況,其中異步被圍繞在 Redux上,而不是 Redux處理。 所以,中間件經過修改能夠直接傳遞給dispatch內容來實現異步。


也就是說,我認爲您的建議的精神是正確的。 固然,您還可使用其餘方法在Redux + React應用程序中處理異步。

使用中間件的好處之一是,您能夠繼續正常使用動做建立者,而沒必要擔憂它們是如何鏈接的。 例如,使用redux-thunk ,您編寫的代碼看起來很像

function updateThing() {
  return dispatch => {
    dispatch({
      type: ActionTypes.STARTED_UPDATING
    });
    AsyncApi.getFieldValue()
      .then(result => dispatch({
        type: ActionTypes.UPDATED,
        payload: result
      }));
  }
}

const ConnectedApp = connect(
  (state) => { ...state },
  { update: updateThing }
)(App);

看起來與原始版本沒有什麼不一樣-只是改了一點-而且connect不知道updateThing是(或須要是)異步的。

若是你也想支持的承諾觀測傳奇 ,或瘋狂的定製高度聲明動做的創造者,那麼終極版能夠只經過改變你傳遞什麼作dispatch (又名,你的行動創造者返回的內容)。 無需對React組件(或connect調用)進行處理。


#3樓

這種方法有什麼問題? 如文檔所示,我爲何要使用Redux Thunk或Redux Promise?

這種方法沒有錯。 這在大型應用程序中很不方便,由於您將有不一樣的組件執行相同的操做,您可能但願對某些操做進行反跳操做,或者將某些本地狀態(例如,自動遞增ID)保持在操做建立者附近,等等。從維護角度將動做建立者提取到單獨的功能中。

您能夠閱讀我對「如何在超時時分派Redux操做」的回答,以獲取更詳細的演練。

中間件像終極版咚或終極版無極只是給你「語法糖」派遣的thunk或承諾,但你沒必要使用它。

所以,沒有任何中間件,您的動做建立者可能看起來像

// action creator
function loadData(dispatch, userId) { // needs to dispatch, so it is first argument
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
    );
}

// component
componentWillMount() {
  loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch
}

可是,使用Thunk中間件,您能夠這樣編寫:

// action creator
function loadData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`) // Redux Thunk handles these
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
    );
}

// component
componentWillMount() {
  this.props.dispatch(loadData(this.props.userId)); // dispatch like you usually do
}

因此沒有太大的區別。 我喜歡後一種方法的一件事是,該組件不在意動做建立者是異步的。 它只是正常地調用dispatch ,也可使用mapDispatchToPropsmapDispatchToProps法綁定此類操做建立者,等等。這些組件不知道操做建立者的實現方式,所以您能夠在不一樣的異步方法之間進行切換(Redux Thunk,Redux Promise, Redux Saga)而無需更改組件。 另外一方面,使用前一種顯式方法,您的組件能夠確切地知道特定的調用是異步的,而且須要經過某種約定來傳遞dispatch (例如,做爲sync參數)。

還考慮一下此代碼將如何更改。 假設咱們要具備第二個數據加載功能,並將它們組合在一個動做建立器中。

使用第一種方法時,咱們須要注意咱們正在調用哪一種動做建立者:

// action creators
function loadSomeData(dispatch, userId) {
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
    );
}
function loadOtherData(dispatch, userId) {
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
    );
}
function loadAllData(dispatch, userId) {
  return Promise.all(
    loadSomeData(dispatch, userId), // pass dispatch first: it's async
    loadOtherData(dispatch, userId) // pass dispatch first: it's async
  );
}


// component
componentWillMount() {
  loadAllData(this.props.dispatch, this.props.userId); // pass dispatch first
}

使用Redux Thunk動做建立者能夠dispatch其餘動做建立者的結果,甚至不考慮它們是同步的仍是異步的:

// action creators
function loadSomeData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
    );
}
function loadOtherData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
    );
}
function loadAllData(userId) {
  return dispatch => Promise.all(
    dispatch(loadSomeData(userId)), // just dispatch normally!
    dispatch(loadOtherData(userId)) // just dispatch normally!
  );
}


// component
componentWillMount() {
  this.props.dispatch(loadAllData(this.props.userId)); // just dispatch normally!
}

使用這種方法,若是之後您但願動做建立者查看當前的Redux狀態,則可使用傳遞給thunk的第二個getState參數,而無需徹底修改調用代碼:

function loadSomeData(userId) {
  // Thanks to Redux Thunk I can use getState() here without changing callers
  return (dispatch, getState) => {
    if (getState().data[userId].isLoaded) {
      return Promise.resolve();
    }

    fetch(`http://data.com/${userId}`)
      .then(res => res.json())
      .then(
        data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
        err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
      );
  }
}

若是須要將其更改成同步,則也能夠在不更改任何調用代碼的狀況下執行此操做:

// I can change it to be a regular action creator without touching callers
function loadSomeData(userId) {
  return {
    type: 'LOAD_SOME_DATA_SUCCESS',
    data: localStorage.getItem('my-data')
  }
}

所以,使用像Redux Thunk或Redux Promise這樣的中間件的好處是組件不知道動做建立者的實現方式,它們是否關心Redux狀態,它們是同步仍是異步以及是否調用其餘動做建立者。 缺點是間接的,但咱們認爲在實際應用中值得這麼作。

最後,Redux Thunk和朋友只是Redux應用程序中異步請求的一種可能方法。 另外一個有趣的方法是Redux Saga ,它容許您定義長時間運行的守護程序(「 sagas」),這些守護程序會在操做到來時執行操做,並在輸出操做以前轉換或執行請求。 這將邏輯從動做建立者轉移到了傳奇。 您可能想要檢查一下,而後選擇最適合您的東西。

我在Redux存儲庫中搜索了線索,發現過去過去要求Action Creators是純函數。

這是不正確的。 文檔說了這一點,可是文檔是錯誤的。
從未要求動做建立者是純粹的功能。
咱們修復了文檔以反映這一點。


#4樓

你不知道

可是...您應該使用redux-saga :)

丹·阿布拉莫夫(Dan Abramov)的答案是關於redux-thunk可是我將進一步討論redux-saga ,它很是類似,但功能更強大。

命令式VS聲明式

  • DOM :jQuery是命令性的/ React是聲明性的
  • Monads :IO勢在必行/ Free是聲明式
  • Redux效果redux-thunk是命令性的/ redux-saga是聲明性的

當您手頭有重擊時,例如IO monad或諾言,執行後就不會輕易知道它會作什麼。 測試一個thunk的惟一方法是執行它,並模擬調度程序(若是它與更多的東西交互,則模擬整個外部世界……)。

若是您正在使用模擬,那麼您就不在進行函數式編程。

從反作用的角度來看,模擬標誌着您的代碼是不純的,而且在功能程序員的眼中,它證實了某些錯誤。 與其下載圖書館來幫助咱們檢查冰山是否無缺,不如咱們繞着它航行。 一個TDD / Java頑固的傢伙曾經問我如何在Clojure中進行模擬。 答案是,咱們一般不這樣作。 咱們一般將其視爲咱們須要重構代碼的標誌。

資源

sagas(由於它們在redux-saga )是聲明性的,而且像Free monad或React組件同樣,它們無需任何模擬就易於測試。

另請參閱本文

在現代FP中,咱們不該該編寫程序,而應該編寫程序的描述,而後能夠對其進行自省,轉換和解釋。

(實際上,Redux-saga就像是一個混合體:流程是必須的,但效果是聲明性的)

混亂:動做/事件/命令...

前端世界對如何將某些後端概念(如CQRS / EventSourcing和Flux / Redux)相關聯感到困惑,主要是由於在Flux中,咱們使用術語「操做」來表示命令性代碼( LOAD_USER )和事件( USER_LOADED )。 我相信,像事件來源同樣,您應該只調度事件。

在實踐中使用sagas

想象一個帶有用戶配置文件連接的應用程序。 使用兩種中間件來處理此問題的慣用方式是:

redux-thunk

<div onClick={e => dispatch(actions.loadUserProfile(123)}>Robert</div>

function loadUserProfile(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'USER_PROFILE_LOADED', data }),
      err => dispatch({ type: 'USER_PROFILE_LOAD_FAILED', err })
    );
}

redux-saga

<div onClick={e => dispatch({ type: 'USER_NAME_CLICKED', payload: 123 })}>Robert</div>


function* loadUserProfileOnNameClick() {
  yield* takeLatest("USER_NAME_CLICKED", fetchUser);
}

function* fetchUser(action) {
  try {
    const userProfile = yield fetch(`http://data.com/${action.payload.userId }`)
    yield put({ type: 'USER_PROFILE_LOADED', userProfile })
  } 
  catch(err) {
    yield put({ type: 'USER_PROFILE_LOAD_FAILED', err })
  }
}

這個傳奇轉化爲:

每次單擊用戶名時,獲取用戶配置文件,而後使用加載的配置文件調度事件。

如您所見, redux-saga有一些優勢。

使用takeLatest能夠表示您只對獲取最後一次單擊的用戶名的數據感興趣(若是用戶在許多用戶名上快速單擊,則能夠處理併發問題)。 這樣的東西很難與暴徒。 若是您不但願這種行爲,可使用takeEvery

您可使動做創做者保持純正。 請注意,保留actionCreators(在sagas put和component dispatch )仍然頗有用,由於它可能會幫助您未來添加動做驗證(斷言/流程/打字稿)。

因爲效果是聲明性的,所以您的代碼變得更具可測試性

您再也不須要觸發相似rpc的調用,例如actions.loadUser() 。 您的UI只需調度已發生的事件。 咱們僅觸發事件 (始終以過去時!),再也不執行任何操做。 這意味着您能夠建立解耦的「鴨子」有界上下文 ,而且傳奇能夠充當這些模塊化組件之間的耦合點。

這意味着您的視圖更易於管理,由於它們再也不須要在已發生的事情和應發生的事情之間包含轉換層

例如,想象一個無限滾動視圖。 CONTAINER_SCROLLED能夠致使NEXT_PAGE_LOADED ,可是可滾動容器是否真的有責任決定咱們是否應該加載另外一頁? 而後,他必須知道更復雜的內容,例如是否成功加載了最後一頁,或者是否已有試圖加載的頁面,或者是否還有其餘要加載的項目? 我不這麼認爲:爲了得到最大的可重用性,可滾動容器應該只描述它已經被滾動。 頁面的加載是該滾動的「業務影響」

有人可能會爭辯說,生成器可使用本地變量將狀態固有地隱藏在redux存儲以外,可是若是您經過啓動計時器等開始在thunk中編排複雜的東西,則不管如何都會遇到一樣的問題。 如今有一個select效果,能夠從Redux存儲中獲取一些狀態。

Sagas能夠進行時間旅行,還能夠啓用當前正在使用的複雜流記錄和開發工具。 這是一些已經實現的簡單異步流日誌記錄:

傳奇流記錄

去耦

Sagas不只在替換redux thunk。 它們來自後端/分佈式系統/事件源。

一個廣泛的誤解是,sagas即將到來,以更好的可測試性取代您的redux thunk。 實際上,這只是redux-saga的實現細節。 使用聲明性效果比使用thunk更好,可是可在命令性或聲明性代碼之上實現saga模式。

首先,傳奇是一款軟件,能夠協調長期運行的事務(最終的一致性)以及跨不一樣有界上下文的事務(域驅動的設計術語)。

爲了簡化前端世界的操做,假設有widget1和widget2。 單擊widget1上的某個按鈕時,它將對widget2產生影響。 與其將2個小部件耦合在一塊兒(即,小部件1調度以小部件2爲目標的動做),小部件1僅調度其按鈕被單擊。 而後,傳奇偵聽此按鈕的單擊,而後經過調度widget2知道的新事件來更新widget2。

這增長了簡單應用程序沒必要要的間接級別,但使擴展複雜應用程序更加容易。 如今,您能夠將widget1和widget2發佈到不一樣的npm存儲庫,這樣它們就沒必要彼此瞭解,而沒必要共享全局的動做註冊表。 如今這2個小部件是能夠單獨使用的有界上下文。 他們不須要彼此保持一致,也能夠在其餘應用程序中重複使用。 傳奇是兩個小部件之間的耦合點,以對您的業務有意義的方式協調它們。

關於如何構建Redux應用的一些不錯的文章,出於解耦的緣由,您能夠在其中使用Redux-saga:

一個具體的用例:通知系統

我但願個人組件可以觸發應用內通知的顯示。 可是我不但願個人組件與具備本身的業務規則的通知系統高度耦合(最多同時顯示3條通知,通知隊列,4秒顯示時間等)。

我不但願個人JSX組件決定什麼時候顯示/隱藏通知。 我只是給它請求通知的功能,而將複雜的規則留在了傳奇中。 這種東西很難經過大塊頭或諾言實現。

通知

我在這裏描述了佐賀如何作到這一點

爲何叫佐賀?

saga一詞來自後端世界。 在最初的長時間討論中 ,我最初將Yassine(Redux-saga的做者)介紹給該術語。

最初,該術語是在論文中引入的,該傳奇模式原本應該用於處理分佈式事務中的最終一致性,可是後端開發人員已將其用法擴展到更普遍的定義,所以如今它也涵蓋了「流程管理器」模式(某種程度上,原始的傳奇模式是流程管理器的一種特殊形式)。

今天,「傳奇」一詞使人困惑,由於它能夠描述兩種不一樣的事物。 因爲它在redux-saga中使用,所以它並不描述處理分佈式事務的方法,而是協調應用程序中的操做的方法。 redux-saga也能夠稱爲redux-process-manager

也能夠看看:

備擇方案

若是您不喜歡使用生成器的想法,可是對saga模式及其解耦屬性感興趣,則還可使用redux-observable實現相同的功能,它使用名稱epic來描述徹底相同的模式,但使用RxJS。 若是您已經熟悉Rx,就會有賓至如歸的感受。

const loadUserProfileOnNameClickEpic = action$ =>
  action$.ofType('USER_NAME_CLICKED')
    .switchMap(action =>
      Observable.ajax(`http://data.com/${action.payload.userId}`)
        .map(userProfile => ({
          type: 'USER_PROFILE_LOADED',
          userProfile
        }))
        .catch(err => Observable.of({
          type: 'USER_PROFILE_LOAD_FAILED',
          err
        }))
    );

一些redux-saga有用的資源

2017年建議

  • 不要僅僅爲了使用它而過分使用Redux-saga。 僅可測試的API調用不值得。
  • 在大多數狀況下,請勿從您的項目中刪除垃圾。
  • 若是有意義的話,請不要猶豫,在yield put(someActionThunk)調度yield put(someActionThunk)

若是您對使用Redux-saga(或Redux-observable)感到恐懼,但只須要使用去耦模式,請檢查redux-dispatch-subscribe :它容許偵聽調度並在偵聽器中觸發新的調度。

const unsubscribe = store.addDispatchListener(action => {
  if (action.type === 'ping') {
    store.dispatch({ type: 'pong' });
  }
});

#5樓

Abramov的目標-也是每一個人的理想-只是在最合適的地方封裝複雜性(和異步調用)

在標準Redux數據流中最好的位置是哪裏? 怎麼樣:

  • 減速器 ? 沒門。 它們應該是純函數,沒有反作用。 更新商店是嚴肅的,複雜的業務。 不要污染它。
  • 啞吧視圖組件? 絕對不能。它們有一個問題:表示和用戶交互,而且應儘量簡單。
  • 容器組件? 可能,但次優。 有意義的是,容器是一個咱們封裝一些與視圖相關的複雜性並與商店交互的地方,可是:
    • 容器確實須要比啞組件更爲複雜,可是這仍然是一個單一的責任:在視圖與狀態/存儲之間提供綁定。 您的異步邏輯與此徹底無關。
    • 經過將其放在容器中,您能夠將異步邏輯鎖定在單個上下文中,用於單個視圖/路由。 餿主意。 理想狀況下,它們都是可重用的,而且徹底是分離的。
  • 還有其餘服務模塊嗎? 壞主意:您須要注入對商店的訪問權限,這是可維護性/可測試性的噩夢。 最好使用Redux,僅使用提供的API /模型訪問商店。
  • 動做和解釋它們的中間件? 爲何不?! 對於初學者來講,這是咱們剩下的惟一主要選擇。 :-)從邏輯上講,動做系統是分離的執行邏輯,能夠在任何地方使用。 它能夠訪問商店,而且能夠調度更多操做。 它的職責是組織應用程序周圍的控制流和數據流,而且大多數異步處理都適合於此。
    • 動做創做者呢? 爲何不在那裏異步,而不是在動做自己和中間件中同步呢?
      • 首先也是最重要的一點是,建立者沒有中間商能夠訪問的商店。 這意味着您不能調度新的或有操做,不能從存儲中讀取信息以組成異步,等等。
      • 所以,請將複雜性放在必不可少的地方,並將其餘全部內容保持簡單。 而後,建立者能夠是易於測試的簡單,相對純淨的功能。
相關文章
相關標籤/搜索