redux-observable 使用小記

愛甩尾巴的貓發表於2019-03-28

最近用 redux-observable 搭建了一個樣板專案,起先我就被人安利過這個庫,由於自己工作的關係,一直沒能用上,恰巧最近專案不緊,遂搭一個簡單專案來瞅瞅,下面就請跟著我的步伐一步一步的探索這個庫的奧祕。

redux-observable 背景

這個庫是基於 rxjs 基礎上,為 redux 提供的非同步解決方案。

redux 的非同步流

原本 reduxaction creator 只提供一個同步的 action 但隨著業務的擴充套件,在某個場景下需要非同步的 action 來延時呼叫 dispatch。 經典的庫有 redux-thunkredux-saga以及redux-observable

  • redux-thunk

redux-thunk的程式碼很短,很巧妙的使用了 reduxapplyMiddleware 中介軟體模式,它讓 action creator 不僅可以輸出 plain object,也可以輸出一個 function 來處理 action,而這個 function 傳遞的引數就是 上下文的 dispatch,當這個 function 在某個時段執行時,就可以實現延時觸發 dispatch 了。

**這個就是一個典型的函數語言程式設計的案例,巧用了閉包,讓 dispatch 方法在函子內沒有被銷燬。 **

redux-thunk 及其 applyMiddleware 原始碼解讀

redux-thunk 流程

但是這也是有一定的缺點的,就拿常用的 ajax 請求來說,每個 action creator 輸出的 function 不盡相同,非同步操作分散,而且邏輯也千變萬化,action 多了,就不易維護了。

  • redux-saga

redux-saga 是另一種非同步流,不過它的 action 是統一形式的,並且會集中處理非同步操作。

可以理解 redux-saga 做了一個監聽器,專門監聽 action ,此處的 action 就是 plain object ,當接收到 UI 觸發了某個 action 時, redux-saga 就會觸發相應的 effects 來處理對應的副作用函式,這個函式返回的也是一個 plain objectactionreducer

這樣做的好處是,redux-saga 接收了非同步函式的管理,將複雜的業務邏輯部分與 redux 解耦,這也是 redux 設計的初衷,action 始終是 plain object,而且 redux-saga 提供了不少工具函式來處理非同步流,極大的方便了開發者處理非同步。

redux-saga 簡易流程

網上有諸多教程,這裡就不一一贅述了。

不過有點鬱悶的就是 redux-saga 使用的是 generator,寫起來還要在 function 那裡加個 *,在我個人看來非常的不習慣,就是特別的彆扭。

  • redux-observable

redux-observableredux-saga 有些類似,可以理解為它是將 action 當作即是 observable 也是 observer(釋出者與訂閱者),就是 rxjsSubject物件,他是資料流的中轉站,能夠訂閱上游資料流,也能被一個或者多個下游訂閱。

redux-observable 將從 ui 觸發的 action 轉化為一個資料流,並且訂閱它。當資料流有資料發出時,這個流的資料管道中設定了對此資料流做的一系列的操作符,或者是高階 observable,資料流通過管道後,將最終的流轉成 action

上述所說的管道就是由 rxjs 提供的操作符組合

redux 中使用 redux-observable

現在開始做一個簡易的專案(呼叫 github api 獲取使用者的頭像和名稱):

  • 安裝 redux 全家桶

常規的 redux 專案所需的庫,基本上都會用到

yarn add react react-dom redux react-redux react-router redux-logger ...
yarn add -D webpack webpack-cli ...
複製程式碼
  • 安裝 redux-observable
yarn add rxjs redux-observable

# ts宣告庫
yarn add -D @types/rx
複製程式碼
  • 目錄結構
.
├── actions
├── components
├── constants
├── epics
├── reducers
├── types
└── utils
├── index.html
├── index.tsx
├── routes.tsx
├── store.ts
複製程式碼
  • 解讀 Epics

從上面的目錄結構就能看出,比常規的 redux 專案多了一個 epics 目錄,這個目錄是存放什麼檔案呢。

redux-observable 的核心就是 Epics ,它是一個函式,接收一個 action (plain object) ,返回一個 action 流,它是一個 Observable 物件。

函式簽名:

function (action$: Observable<Action>, store: Store): Observable<Action>;
複製程式碼

Epics 函式出來的 action 已經是一個 Observable 物件了,是一個上游資料流了,可以被各種 rxjs 操作符操作了。

資料流的終端就是一個訂閱者,這個訂閱者只做一件事兒,就是被 store.dispatch 分發至 reducer

epic(action$, store).subscribe(store.dispatch);
複製程式碼

redux-observable 簡易流程:

redux-observable 簡易流程

  • 編碼
import { USER } from "@constants";
import { createAction } from "typesafe-actions";

export namespace userActions {
  export const getGitHubUser = createAction(USER.GITHUB_USER_API);

  export const setUserInfo = createAction(USER.SET_USER_INFO, resolve => user =>
    resolve(user)
  );
}
複製程式碼

建立一個 user 的操作 action,定義兩個 action

epic 檔案:

import { ofType, ActionsObservable } from "redux-observable";
import { throwError } from "rxjs";
import { switchMap, map, catchError } from "rxjs/operators";
import { ajax } from "rxjs/ajax";
import { getType } from "typesafe-actions";
import { userActions } from "@actions";

const url = "https://api.github.com/users/soraping";

export const userEpic = (action$: ActionsObservable<any>) =>
  action$.pipe(
    ofType(getType(userActions.getGitHubUser)),
    switchMap(() => {
      return ajax.getJSON(url).pipe(
        map(res => userActions.setUserInfo(res)),
        catchError(err => throwError(err))
      );
    })
  );
複製程式碼

建立一個 userEpic ,它是一個高階函式,這個高階函式攜帶的引數就是 action$ ,它就是一個上游資料流,這個函式的基礎邏輯就是一個 rxjs 的一般操作了。

上游 action$ 的資料管道中,監聽 action 的變化,當 getType 方法就是獲得 actiontype 是 操作符 oftype 返回的一致,則繼續管道後面的操作,switchMap 是一個高階的操作符,它一般用在 ajax 網路服務請求上,主要處理多個內部 Observable 物件產生併發的情況下,只訂閱最後一個資料來源,其他的都退訂,這樣的操作符,非常適合網路請求。

這個網路請求就是獲取 github 的 api,當獲取資料後,呼叫 action creator 方法傳遞獲取的資料,這個時候並沒有返回一個真正的 plain object ,而是一個最終的 action$ 資料流,觸發 subscribestore.dispatch(action) 方法,將 plain action 送至 reducer

typesafe-actions 庫是一個 action 封裝庫,簡化了 action 的操作,它和 redux-actions 很像,但是typesafe-actions這個庫對 epic 支援得很好。

整合多個epic

import { combineEpics } from "redux-observable";
import { userEpic } from "./user";
export const rootEpic = combineEpics(userEpic);
複製程式碼

combineEpics 方法用來整合多個 epic 高階方法,它類似與 reducerscombineReducers

那麼,epic 方法已經有了,redux-observable 畢竟是一箇中介軟體,它在 store 中的操作:

import { createStore, applyMiddleware } from "redux";
import { createEpicMiddleware } from "redux-observable";
import { composeWithDevTools } from "redux-devtools-extension";
import { routerMiddleware } from "connected-react-router";
import { createLogger } from "redux-logger";
import { createBrowserHistory } from "history";
import { rootReducer } from "./reducers";
import { rootEpic } from "./epics";

export const history = createBrowserHistory();

const epicMiddleware = createEpicMiddleware();

const middlewares = [
  createLogger({ collapsed: true }),
  epicMiddleware,
  routerMiddleware(history)
];

export default createStore(
  rootReducer(history),
  composeWithDevTools(applyMiddleware(...middlewares))
);

// run 方法一定要在 createStore 方法之後
epicMiddleware.run(rootEpic);
複製程式碼

epicMiddleware註冊到 redux 中介軟體中,這樣,就能接收到上下文的 actiondispatch,不過要注意的是,epicMiddleware要在store設定之後,執行 run 方法,這和 redux-saga一致。

這樣,基本上 reduxredux-observable 組合的基本操作已經差不多了,reducer 的操作基本不變

上述例子的 github 原始碼

yarn && yarn start

# localhost:8000
複製程式碼

喜歡的話給個 star 啊!

推薦學習路徑

最後說下學習路徑:

函數語言程式設計

從頭開始學程式設計吧,用函式式,純函式的那種。

redux 原始碼閱讀

大牛的作品,閉包用的爐火純青,各種高階函式,精妙絕倫的操作大大降低了程式碼量,更能看到函數語言程式設計的妙處。

redux原始碼閱讀參考

rxjs 及其操作符

響應式程式設計的系統學習,但不必要所有操作符都過一遍,這裡推薦一本書 《深入淺出 rxjs》,不過書裡的版本是 v5 的,官網是 v6 的,除了一些改變外,原理都是相同的。

相關文章