本文用以記錄從調研Redux Saga,到應用到項目中的一些收獲。
什么是Redux Saga
官網解釋
來自:https://github.com/redux-saga/redux-saga
redux-saga
is a library that aims to make side effects (i.e. asynchronous things like data fetching and impure things like accessing the browser cache) in React/Redux applications easier and better.
The mental model is that a saga is like a separate thread in your application that's solely responsible for side effects. redux-saga is a redux middleware, which means this thread can be started, paused and cancelled from the main application with normal redux actions, it has access to the full redux application state and it can dispatch redux actions as well.
剛開始了解Saga時,看官方解釋,并不是很清楚到底是什么?Saga的副作用(side effects)到底是什么?
通讀了官方文檔后,大概了解到,副作用就是在action觸發reduser之后執行的一些動作, 這些動作包括但不限于,連接網絡,io讀寫,觸發其他action。并且,因為Sage的副作用是通過redux的action觸發的,每一個action,sage都會像reduser一樣接收到。并且通過觸發不同的action, 我們可以控制這些副作用的狀態, 例如,啟動,停止,取消。
所以,我們可以理解為Sage是一個可以用來處理復雜的異步邏輯的模塊,并且由redux的action觸發。
使用Saga解決的問題
最初,在開始探究Saga之前,我們是希望尋求一種方式來隔離開應用前端的展現層
,業務層
和數據層
。 大概想法是使用react展現數據,redux管理數據,然后借助redux的middleware來實現業務層。這樣原有的react為核心的項目架構,變成了redux為核心的架構。
在最初的調研中redux-thunk
是首先考慮的,redux-thunk
是在action作用到reducer之前觸發一些業務操作。剛好起到控制層的作用。
但是,馬上了解到了redux-sage
,因為大家都在對比兩者。本文并不會做對比,在文章的最后會簡單介紹為什么選了Saga而不是thunk的原因,僅供參考。
在瀏覽了很多比較文章后,最終,我們選擇了redux-saga來處理應用的控制層
。
下面是一個簡單的例子:
在用戶提交表單的時候,我們想要做如下事情:
- 校驗一些輸入信息 (簡單, 寫在組件里)
- 彈起提示信息(聰明的我,一定要寫一個公用的提示信息模塊,這樣別的頁面引入就可以用了, 呵呵呵呵。。。)
- 提交后端服務 (直接組件里面fetch吧。。。)
- 拿到后端返回狀態 (promise so easy...)
- 隱藏提示信息 (這個有點難度,不過難不倒我,我給組建加一個控制屬性)
- 更新redux store (dispatch咯。。。)
好了,現在我們要把剛剛做的事情加到所有的表單上。。。 (WTF, 每個form組件都要做同樣的事情。。。頁面的代碼丑的不想再多看一眼。。。)
用了redux-saga之后:
- form組件觸發提交action (一行簡單的dispatch)
- reducer這個action不需要我處理 (打醬油了)
- saga提交表單的副作用走起~ (監聽到觸發副作用的action)
- 校驗一下
- 通知
顯示層
彈起信息框 (dispatch一下變更控制信息框彈起的store) - 提交表單 (yield一個promis,yield是javascript generator的語法,稍后有介紹)
- 拿到后端返回狀態
- 更新redux store (dispatch一下)
可以看到在使用了Saga后,react只負責數據如何展示,redux來負責數據的狀態和綁定數據到react,而Saga處理了大部分復雜的業務邏輯。
通過這個改變,前端應用的代碼結構更加清晰,業務層可復用的部分增加。當然,Saga對自動化測試也支持的很好,可以將邏輯單獨使用自動化腳本測試,提高項目質量。
開始前需要了解的幾個概念
redux中間件
redux中文文檔解釋如下:
如果你使用過 Express 或者 Koa 等服務端框架, 那么應該對 middleware 的概念不會陌生。 在這類框架中,middleware 是指可以被嵌入在框架接收請求到產生響應過程之中的代碼。例如,Express 或者 Koa 的 middleware 可以完成添加 CORS headers、記錄日志、內容壓縮等工作。middleware 最優秀的特性就是可以被鏈式組合。你可以在一個項目中使用多個獨立的第三方 middleware。
相對于 Express 或者 Koa 的 middleware,Redux middleware 被用于解決不同的問題,但其中的概念是類似的。它提供的是位于 action 被發起之后,到達 reducer 之前的擴展點。 你可以利用 Redux middleware 來進行日志記錄、創建崩潰報告、調用異步接口或者路由等等。
可以簡單理解為,中間件是可以在action到達reducer之前做一些事情的層。(有意思的是,saga應該是在reducer被觸發之后才觸發的。TODO, 需要進一步驗證)
Javascript Generator
在使用Saga之前,建議先了解Javascript生成器,因為Saga的副作用都是通過生成器來實現的。
可以在阮一峰的ECMAScript 6 入門: Generator 函數的語法和Generator 函數的異步應用章節中了解更多細節。
如何使用
redux-sage官方文檔有很詳細的使用說明,這里只做簡單的上手說明。
安裝redux-sage
npm install --save redux-saga
給redux添加中間件
在定義生成store的地方,引入并加入redux-sage中間件。
import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import reducer from './reducers'
import mySaga from './sagas'
// create the saga middleware
const sagaMiddleware = createSagaMiddleware()
// mount it on the Store
const store = createStore(
reducer,
applyMiddleware(sagaMiddleware)
)
// then run the saga
sagaMiddleware.run(mySaga)
副作用
副作用,顧名思義,在主要作用(action觸發reducer)之外,用來處理其他業務邏輯。redux-saga提供了幾種產生副作用的方式, 主要用到了有兩種takeEvery
和takeLates
。
takeEvery
會在接到相應的action之后不斷產生新的副作用。 比如,做一個計數器按鈕,用戶需要不斷的點擊按鈕,對后臺數據更新,這里可以使用takeEvery
來觸發。
takeLatest
在相同的action被觸發多次的時候,之前的副作用如果沒有執行完,會被取消掉,只有最后一次action觸發的副作用可以執行完。比如,我們需要一個刷新按鈕, 讓用戶可以手動的從后臺刷新數據, 當用戶不停單機刷新的時候, 應該最新一次的請求數據被刷新在頁面上,這里可以使用takeLatest
。
import { call, put } from 'redux-saga/effects'
import { takeEvery } from 'redux-saga'
export function* fetchData(action) {
try {
const data = yield call(Api.fetchUser, action.payload.url);
yield put({type: "FETCH_SUCCEEDED", data});
} catch (error) {
yield put({type: "FETCH_FAILED", error});
}
}
function* watchFetchData() {
yield* takeEvery('FETCH_REQUESTED', fetchData)
}
注意,takeEvery
第一個參數可以是數組或者方法。 也可以有第三個參數用來傳遞變量給方法。
call方法
call有些類似Javascript中的call函數, 不同的是它可以接受一個返回promise的函數,使用生成器的方式來把異步變同步。
put方法
put就是redux的dispatch,用來觸發reducer更新store
有什么弊端
目前在項目實踐中遇到的一些問題:
- redux-saga模型的理解和學習需要投入很多精力
- 因為需要用action觸發,所以會產生很多對于reducer無用的action, 但是reducer一樣會跑一輪,雖然目前沒有觀測到性能下降,但還是有計算開銷
- 在action的定義上要謹慎,避免action在saga和reducer之間重復觸發,造成死循環
后記
總體而言,對于redux-saga的第一次嘗試還是很滿意的。 在業務邏輯層,可以簡化代碼,使代碼更加容易閱讀。 在重用方面,解耦顯示層和業務層之后, 代碼的重用度也得到了提升。
選擇Saga的原因
開始的時候一直在猶豫是否需要使用Saga或thunk,因為并不能很好的把握這兩者到底解決了什么問題。之后,在瀏覽文章的時候看到了一遍對比兩者的長文,列出了不少開發者對兩者的擔憂和爭論,其中不乏閃光的觀點,長文的最后作者寫到:“不管是否用得上,你都應該嘗試一下”。 這句話使我決定了嘗試用saga或thunk來實踐把前端分層的設想。
之所以最后選擇了saga是因為這段 Cheng Lou 的視頻:
On the Spectrum of Abstraction (youtube)
視頻中講述了在一種抽象的概念下如何去選擇一種技術。 其中一個理論是:越是用來解決具體問題的技術,使用起來越容易,越高效,學習成本越低;越是用來解決寬泛問題的技術,使用起來越難,學習成本越高。 thunk解決的是很具體的一個問題,就是在action到達reducer之前做一些其他的業務,比如fetch后端, 它在做這件事的上很高效。而Saga解決的問題要更寬泛一些,因為saga只是攔截了action,至于做什么,開發者需要自己來考慮,可以是fetch后端,也可以是更新redux store, 甚至可以執行action帶進來的callback。 很顯然對于一個業務層
來說,saga會是一個更合適的選擇,但同時也帶來了學習成本的提高。