redux-saga 實踐總結
有關 redux-saga 的文章,網路上早已是汗牛充棟。因此,本篇主要談一談自己的理解,以及實踐中的經驗總結。
眾所周知,redux 大部分的想法,都來自於 elm。在 elm 和 redux 中,整個應用就是一個純函式。elm 通過在 reducer 中返回一些宣告副作用的 task 來處理非同步問題,而 redux 借鑑 koa 的外掛機制,用中介軟體改造 dispatch ,從而誕生了一批通過構造滿足特殊 pattern 條件的 action 來解決副作用的問題。
而 redux-saga 獨闢蹊徑,監聽 action 來執行有副作用的 task,以保持 action 的簡潔性。並且引入了
有意思的是,redux 借鑑了 elm,但在處理非同步問題(副作用問題在前端一般為非同步問題)上,借鑑了 koa 中介軟體的形式,而 redux-saga 卻又去從 elm 取經,借鑑了獨立 task 的形式。但是說到底,redux-saga 是一個 redux 的中介軟體。這個故事告訴我們,有好的設計不如有強大的擴充套件性。
redux-saga 本身也有良好的擴充套件性。比如,易證得,但凡 redux 中介軟體,都可以用 redux-saga 來重寫。當然了,不是說用了 redux-saga,其它非同步中介軟體就不能用了,只是說不能保證 redux-saga 能恰好和你之前使用的中介軟體配合良好。
redux-saga
簡介
redux-saga 是一個 redux 中介軟體,它具有如下特性:
-
集中處理 redux 副作用問題。
-
被實現為 generator 。
-
類 redux-thunk 中介軟體。
-
watch/worker(監聽->執行) 的工作形式。
讀者也可以從這裡檢視官方定義。
對於剛接觸 redux-saga 的同學,可以先來一段簡單的程式碼快速瞭解 redux-saga 諸多特性。
// 類 thunk 的 worker “程序”
function* load() {
yield put({ type: BEGIN_LOAD_DATA });
try {
const result = yield call(fetch, UrlMap.loadData);
yield put({
type: LOAD_DATA_SUCCESS,
payload: result,
});
} catch (e) {
yield put({
type: LOAD_DATA_ERROR,
payload: e,
error: true,
});
}
}
function* saga() {
// 建立一個監聽“程序”
yield fork(watch(CLICK_LOAD_BUTTON, load))
}
Effects
Effect 是一個 javascript 物件,裡面包含描述副作用的資訊,可以通過 yield 傳達給 sagaMiddleware 執行
在 redux-saga 世界裡,所有的 Effect 都必須被 yield 才會執行,所以有人寫了 eslint-plugin-redux-saga 來檢查是否每個 Effect 都被 yield。並且原則上來說,所有的 yield 後面也只能跟Effect,以保證程式碼的易測性。
例如:
yield fetch(UrlMap.fetchData);
應該用 call Effect :
yield call(fetch, UrlMap.fetchData)
從而可以使程式碼可測:
assert.deepEqual(iterator.next().value, call(fetch, UrlMap.fetchData))
關於各個 Effect 的具體介紹,文件已經寫得很詳細了,這裡只做簡要介紹。
1、put
作用和 redux 中的 dispatch 相同。
yield put({ type: 'CLICK_BTN' });
2、select
作用和 redux thunk 中的 getState 相同。
const id = yield select(state => state.id);
3、take
等待 redux dispatch 匹配某個 pattern 的 action 。
在這個例子中,先等待一個按鈕點選的 action ,然後執行按鈕點選的 saga:
while (true) {
yield take('CLICK_BUTTON');
yield fork(clickButtonSaga);
}
再舉一個利用 take 實現 logMiddleware 的例子:
while (true) {
const action = yield take('*');
const newState = yield select();
console.log('received action:', action);
console.log('state become:', newState);
}
這種監聽一個 action ,然後執行相應任務的方式,在 redux-saga 中非常常用,因此 redux-saga 提供了一個輔助 Effect —— takeEvery ,讓 watch/worker 的程式碼更加清晰。
yield takeEvery('*', function* logger(action) {
const newState = yield select();
console.log('received action:', action);
console.log('state become:', newState);
});
4、阻塞呼叫和無阻塞呼叫
redux-saga 可以用 fork 和 call 來呼叫子 saga ,其中 fork 是無阻塞型呼叫,call 是阻塞型呼叫。
如果看過 saga 的論文,就知道 saga 是由許多子 saga (或者 subtransaction)組合起來的。fork Effect 和它的字面意思一樣,即建立一個子 saga 。
4.1、fork
下面寫一個倒數的例子,當接收到 BEGIN_COUNT 的 action,則開始倒數,而接收到 STOP_COUNT 的 action, 則停止倒數。
function* count(number) {
let currNum = number;
while (currNum >= 0) {
console.log(currNum--);
yield delay(1000);
}
}
function countSaga* () {
while (true) {
const { payload: number } = yield take(BEGIN_COUNT);
const countTaskId = yield fork(count, number);
yield take(STOP_TASK);
yield cancel(countTaskId);
}
}
4.2、call
有阻塞地呼叫 saga 或者返回 promise 的函式。
同樣寫一個例子:
const project = yield call(fetch, { url: UrlMap.fetchProject });
const members = yield call(fetchMembers, project.id);
傳統非同步中介軟體簡介
在介紹 redux-saga 優缺點之前,這裡先簡要介紹傳統的 redux 非同步中介軟體,以便和 redux-saga 做比較。對傳統非同步中介軟體已經充分了解的讀者,可以直接跳到 “redux-saga 優缺點分析” 進行閱讀。
1. fetch-middleware
使用redux的前端技術團隊或個人,大多數都有一套自己 fetch-middleware,一來可以封裝非同步請求的業務邏輯,避免重複程式碼,二來可以寫一些公共的非同步請求邏輯,比如異常介面資料採集、介面快取、介面處理等等。例如 redux-composable-fetch,redux-api-middleware。
在當前 redux 社群中,fetch-middleware 封裝結果一般如下:
function loadData(id) {
return {
url: '/api.json',
types: [LOADING_ACTION_TYPE, SUCCESS_ACTION_TYPE, SUCCESS_ACTION_TYPE],
params: {
id,
},
};
}
值得一提的是,大多數 fetch-middleware 都會用到一個小技巧 —— 把最終處理好的 promise 返回出來,以便在 thunk-middleware 中複用,並組織不同非同步過程的先後邏輯。
function loadDetailThunk(id) {
return (dispatch) => {
// 先請求到 loadData 的結果,再請求 loadDetail
dispatch(loadData(id)).then(result => {
const { id: detailId } = result;
dispatch(loadDetail(detailId));
});
};
}
這個技巧在 redux-saga
中也同樣有效。
function* loadDetailSaga(id) {
const result = yield put.sync(loadData(id));
const { id: detailId } = result;
yield put.sync(loadDetail(detailId));
}
2. redux-thunk-middleware
redux 中大量應用了 thunk 的概念,例如 getState 以延遲執行的方式可以始終獲得最新值,redux-thunk 以延遲執行的方式把副作用的責任推卸到使用者身上。
任何非同步問題都能在 thunk 中解決。
sequence-middleware 用於保證 action 依次執行,無論是非同步 action 還是普通 aciton ,和 fetch-middleware 配合使用非常方便。
這裡可以把每個 action 可以寫成 thunk action,在 thunk 函式內從 store 拿到引數,避免 action 之間的依賴。這樣不管業務邏輯有多複雜,都可以通過用 sequence action 輕易組織。
function loadDetailThunk() {
return function(dispatch, getState) {
const detailId = _.get(getState(), `${currPath}.detailId`);
dispatch({
url: UrlMap.getDetail,
params: { detailId },
});
};
}
function loadDetail() {
return [loadData(), loadDetailThunk()];
}
redux-saga
優缺點分析
缺點
-
redux-saga 不強迫我們捕獲異常,這往往會造成異常發生時難以發現原因。因此,一個良好的習慣是,相信任何一個過程都有可能發生異常。如果出現異常但沒有被捕獲,redux-saga 的錯誤棧會給你一種一臉懵逼的感覺。
-
generator 的除錯環境比較糟糕,babel 的 source-map 經常錯位,經常要手動加 debugger 來除錯。
-
你團隊中使用的其它非同步中介軟體,或許難以和 redux-saga 搭配良好。或許需要花費一些代價,用 redux-saga 來重構一部分中介軟體。
優點
-
保持 action 的簡單純粹,aciton 不再像原來那樣五花八門,讓人眼花繚亂。task 的模式使程式碼更加清晰。
-
redux-saga 提供了豐富的 Effects,以及 sagas 的機制(所有的 saga 都可以被中斷),在處理複雜的非同步問題上十分趁手。如果你的應用屬於寫操作密集型或者業務邏輯複雜,快讓 redux-saga 來拯救你。
-
擴充套件性強。
-
宣告式的 Effects,使程式碼更易測試,檢視詳情。
利用 redux-saga 寫 redux 中介軟體
用 redux-saga 來寫中介軟體,可謂事半功倍。這裡舉一個輪詢中介軟體的例子。
function* pollingSaga(fetchAction) {
const { defaultInterval, mockInterval } = fetchAction;
while (true) {
try {
const result = yield put.sync(fetchAction);
const interval = mockInterval || result.interval;
yield delay(interval * 1000);
} catch (e) {
yield delay(defaultInterval * 1000);
}
}
}
function* beginPolling(pollingAction) {
const { pollingUrl, defaultInterval = 300, mockInterval, types,
params = {} } = pollingAction;
if (!types[1]) {
console.error('pollingAction pattern error', pollingAction);
throw Error('pollingAction types[1] is null');
}
const fetchAction = {
url: pollingUrl,
types,
params,
mockInterval,
defaultInterval,
};
const pollingTaskId = yield fork(pollingSaga, fetchAction);
const pattern = action => action.type === types[1] && action.stopPolling;
yield take(pattern);
yield cancel(pollingTaskId);
}
function* pollingSagaMiddleware() {
yield takeEvery(action => {
const { pollingUrl, types } = action;
return pollingUrl && types && types.length;
}, beginPolling);
};
最後,redux-saga
在實踐的沉澱,我已經總結到 redux-saga-sugar,歡迎點贊 ~