最近用 redux-observable
搭建了一個樣板專案,起先我就被人安利過這個庫,由於自己工作的關係,一直沒能用上,恰巧最近專案不緊,遂搭一個簡單專案來瞅瞅,下面就請跟著我的步伐一步一步的探索這個庫的奧祕。
redux-observable
背景
這個庫是基於 rxjs
基礎上,為 redux
提供的非同步解決方案。
redux
的非同步流
原本 redux
的 action creator
只提供一個同步的 action
但隨著業務的擴充套件,在某個場景下需要非同步的 action
來延時呼叫 dispatch
。
經典的庫有 redux-thunk
,redux-saga
以及redux-observable
。
redux-thunk
redux-thunk
的程式碼很短,很巧妙的使用了 redux
的 applyMiddleware
中介軟體模式,它讓 action creator
不僅可以輸出 plain object
,也可以輸出一個 function
來處理 action
,而這個 function
傳遞的引數就是 上下文的 dispatch
,當這個 function
在某個時段執行時,就可以實現延時觸發 dispatch
了。
**這個就是一個典型的函數語言程式設計的案例,巧用了閉包,讓 dispatch
方法在函子內沒有被銷燬。 **
redux-thunk
及其 applyMiddleware
原始碼解讀
但是這也是有一定的缺點的,就拿常用的 ajax
請求來說,每個 action creator
輸出的 function
不盡相同,非同步操作分散,而且邏輯也千變萬化,action
多了,就不易維護了。
redux-saga
redux-saga
是另一種非同步流,不過它的 action
是統一形式的,並且會集中處理非同步操作。
可以理解 redux-saga
做了一個監聽器,專門監聽 action
,此處的 action
就是 plain object
,當接收到 UI 觸發了某個 action
時, redux-saga
就會觸發相應的 effects
來處理對應的副作用函式,這個函式返回的也是一個 plain object
的 action
給 reducer
。
這樣做的好處是,redux-saga
接收了非同步函式的管理,將複雜的業務邏輯部分與 redux
解耦,這也是 redux
設計的初衷,action
始終是 plain object
,而且 redux-saga
提供了不少工具函式來處理非同步流,極大的方便了開發者處理非同步。
網上有諸多教程,這裡就不一一贅述了。
不過有點鬱悶的就是 redux-saga
使用的是 generator
,寫起來還要在 function
那裡加個 *
,在我個人看來非常的不習慣,就是特別的彆扭。
redux-observable
redux-observable
和 redux-saga
有些類似,可以理解為它是將 action
當作即是 observable
也是 observer
(釋出者與訂閱者),就是 rxjs
的 Subject
物件,他是資料流的中轉站,能夠訂閱上游資料流,也能被一個或者多個下游訂閱。
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 簡易流程:
- 編碼
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
方法就是獲得 action
的 type
是 操作符 oftype
返回的一致,則繼續管道後面的操作,switchMap
是一個高階的操作符,它一般用在 ajax
網路服務請求上,主要處理多個內部 Observable
物件產生併發的情況下,只訂閱最後一個資料來源,其他的都退訂,這樣的操作符,非常適合網路請求。
這個網路請求就是獲取 github 的 api,當獲取資料後,呼叫 action creator
方法傳遞獲取的資料,這個時候並沒有返回一個真正的 plain object
,而是一個最終的 action$
資料流,觸發 subscribe
的 store.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 高階方法,它類似與 reducers
的 combineReducers
。
那麼,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 中介軟體中,這樣,就能接收到上下文的 action
和 dispatch
,不過要注意的是,epicMiddleware
要在store
設定之後,執行 run 方法,這和 redux-saga
一致。
這樣,基本上 redux
和 redux-observable
組合的基本操作已經差不多了,reducer
的操作基本不變
yarn && yarn start
# localhost:8000
複製程式碼
喜歡的話給個 star 啊!
推薦學習路徑
最後說下學習路徑:
函數語言程式設計
從頭開始學程式設計吧,用函式式,純函式的那種。
redux
原始碼閱讀
大牛的作品,閉包用的爐火純青,各種高階函式,精妙絕倫的操作大大降低了程式碼量,更能看到函數語言程式設計的妙處。
rxjs
及其操作符
響應式程式設計的系統學習,但不必要所有操作符都過一遍,這裡推薦一本書 《深入淺出 rxjs》,不過書裡的版本是 v5 的,官網是 v6 的,除了一些改變外,原理都是相同的。