文章的名字我想了很久,備選項有“我再不推薦 Redux”,“Redux 為什麼令我頭疼”,“Redux 進化啟示錄”等等。透過這一系列名字我想你大概能猜到我接下來想聊的問題是什麼,但這個問題放眼望去不是 Redux 獨有,而是在做技術決策時經常會遇到的,即使對於非前端背景的開發者也同樣成立。最後決定用一個帶有開放式標題也許能夠引起更多的共鳴。
這篇文章三年前(2019年)就想動筆,幸運的是當三年後再次拾起這個話題時,發現當初的觀點依然成立。三年的時間 Redux 的社群和Flux 的生態都有了巨大變化,這些變化可以作為我們討論的補充
如何在 Redux 裡發起非同步請求
如果你對 Redux 還算熟悉的話,那麼不妨回答這個問題:如何在 Redux 裡發起非同步請求?我們暫且不管 Redux Toolkit(以下簡稱 RTK)的場景,把時鐘撥回到三年前 RTK 還未誕生(RTK 1.0 釋出於 2019 年10月23日 )前,用最核心的 Redux 技術思考這個問題
我這裡給出一個備選答案看行不行?為了後面引用我們把這段程式碼稱為方案1:
const mapDispatchToProps = (dispatch) => {
return {
fetchUser: () => {
dispatch({
type: 'FETCH_START'
});
fetch('https://randomuser.me/api/')
.then(res => res.json())
.then(({ results }) => {
dispatch({
type: 'FETCH_END',
results,
});
})
},
}
}
在 Redux 的官網教程中有專門一節來教授如何處理非同步邏輯和資料抓取,無論是當下還是三年前(感謝 web.archive)的教程,它推薦的都是藉助 redux-thunk 這個中介軟體來達到目的,這裡我們稱之為方案2:
function fetchUser() {
return dispatch => {
dispatch(requestUserStart())
return fetch(`https://randomuser.me/api/`)
.then(response => response.json())
.then(json => dispatch(requestUserEnd(json)))
}
}
然而在圍觀過 Dan Abramov(Redux 和 React 的核心成員) 在 StackOverflow 上解釋為什麼你應該使用 redux-thunk 的回答("How to dispatch a Redux action with a timeout?"、"Why do we need middleware for async flow?")之後,你會發現方案1並非有什麼大的過錯,它可行,不過當用例複雜之後程式碼可維護性會下降。
隨之而來的是,如果你使用了 redux-thunk,它提供的 getState 方法你是否應該使用?在這個 StackOverflow 上的回答(Accessing Redux state in an action creator?)裡我們看到的是兩位的官方維護者(Dan Abramov 和 Mark Erikson)的兩種想左意見,Mark 還專門有一篇長文解釋這個問題(Idiomatic Redux: Thoughts on Thunks, Sagas, Abstraction, and Reusability)
Redux 是模式,而非框架
我們從上面看到了 Redux 社群裡的有趣現象,即是“原則”與“實踐”的分離:redux-thunk 作為官方出品並且推薦使用(但非必須,因為 redux-saga 或者 redux-promise 也能起到同樣的效果)的中介軟體,並不在預設 Redux 安裝包中。同樣的,Redux 的官方文件中有大篇幅來談 immutable 的重要性,或者是如何對資料進行 normalizing, 但在工具上它卻推薦使用第三方類庫進行程式碼約束(主流的第三方外掛在官方文件中的 Ecosystem 一頁中都可以找到)
與 getState 的情況類似,在另一個“如何處理資料載入過程中報錯”的問題中(What is the best way to deal with a fetch error in react redux?),Dan 給出了他心目中的最佳實踐。Dan 的權威性讓人很難不把他的回答當作為來自官方的建議。但是讓人疑惑的地方在於,我們看到的有關 Redux 的最佳實踐來自於 StackOverflow 上,而非官方文件中。
為什麼出現這樣的結果?我的理解是 Redux 從始至終並未被當作一款常規框架來設計,它絕不會指出完成工作的唯一方式。引用核心維護者 Mark Erikson 的原話來解釋就是:
Redux is not intended to be the "most concise way of doing things", but rather to make data flow obvious and readable……Docs are written in a deliberately verbose style for clarity and learning, and not specifically intended as "the one true way to write Redux code",……Redux is a generic framework that provides a balance of just enough structure and just enough flexibility
話雖如此,但我很難不認為他們自己也在動搖。上面的引用摘自在 Github 上的一次關於 Redux 廣泛討論(這是一次很重要的討論,有興趣的同學建議讀完):Request for Discussion: Redux "boilerplate", learning curve, abstraction, and opinionatedness。正如討論的標題所示,以靈活性優先的設計理念,以及為學習而非實踐服務的文件內容,給其他開發者帶來了與我同身受的問題:高昂的學習成本、高度的抽象以及缺少對最佳實踐的指導。
這也是 Redux Toolkit 誕生的原因,從 Mark 對於 RTK 的願景(My Vision for Redux Starter Kit)中就不難看出它的目標在此:
- Make it easier to get started with Redux
- Simplify common tasks
- Opinionated defaults guiding towards "best practices"
- Provide solutions to make people stop using the word "boilerplate"
RTK 在當下已經作為 Redux 專案的標配而存在了,在 Redux 官網的第一章 Getting Started with Redux 我們便會看到:Redux Toolkit is our official recommended approach for writing Redux logic——“approach”是個有意思的詞,它讓我感覺原始的 Redux 框架更像是摸不著的理論而存在——而它的下半句 Redux Toolkit builds in our suggested best practices, simplifies most Redux tasks, prevents common mistakes, and makes it easier to write Redux applications. 將是我們聊技術選型的切入點。
從 Redux 到技術選型
"prevent common mistakes"
我反覆向人推薦過一篇 StackOverflow 聯合創始人Jeff Atwood 的文章 Falling Into The Pit of Success,簡而言之它的中心思想是,好的系統設計應該很容易的讓人們把事情做對,杜絕把事情做錯。比如 type checking
很顯然 RTK 之前的 Redux 並不符合這一條件,甚至與之相悖。因為“容易做對”實踐起來務必會削減框架提供的技術選項(甚至你最好只採用這一種方式去做),而 Redux 主要目的是服務於靈活而非最佳實踐。另一點是官方用大量段落的文字去講述 immutable、normalize 的重要性,但是在技術層面卻不提供任何約束。
而 RTK 徹底解決了這個問題嗎?RTK 解決了程式碼模板的問題,天然整合了最佳實踐,但它移除不掉高昂的學習成本,這是我最擔心的。
如果你有興趣去拿 Redux 文件與前流行的幾個 Flux 框架文件進行對比,比如 Mobx、Zustand 以及 Akita,你會發現 Redux 所需要掌握的概念和篇幅長度令人髮指(很有意思 Angular 是另一個極端,學習 Angular 同樣要掌握很多概念,但是對於你想做的每件事情它都幫你想到了,在文件中給出了最佳實踐)。我理解工程師熱愛“挑戰”,但是在現實中,團隊是由不同水平的個體組成,團隊所能接納的“難度”常常不盡如人意。如果他們不理解他們的工作,那麼他們就很難把工作做好。
"simplifies most Redux tasks, easier to write Redux applications"
Clojure 的作者 Rich Hickey 在 2011 年有一篇很有意思的演講,名為“Simple Made Easy”
他給 easy 和 simple 做出了精確的定義
- Simple:單個事物比如一件任務、一個角色、一個維度;簡單的事物不應該包含交織的概念,比如一個例項,一次操作。simple 是客觀的
- Easy:凡是我們熟悉的或者近在咫尺的事物都會讓我們感到簡單,比如說我們熟悉的語言,使用我們常用的 IDE,easy 是主觀的
這兩者看起來區別不大,但是他認為的這兩者對開發速率的影響是:
easy 可以最佳化你的啟動速度,但如果你只是一味的追求 easy 而忽略了複雜性的話長遠看還是事倍功半的。比如在 Flux 之前的 MVC 時代,在 model 中去更新 view 或者在 view 中直接呼叫 model 是多麼 easy 的事情,但這卻讓狀態管理的複雜性大增。
Redux 算 easy 嗎?不,從 Flux 到 Redux 是一整套全新的概念;它算 simple 嗎?未必,在你設計 reducer 的時候你很難不去思考 normalize
流行
“流行”或者“標配”不應該是技術選型的參考之一。
你的前端專案需要 Redux 嗎?你也許會用反問來回答我這個問題:Redux 不是 React 專案的標配嗎?
並非如此。無論是在 Hacker News 上網友對於 Redux 的抱怨 (God I hate redux)裡,還是在 reddit 上在對 Redux 的仇恨討論中(Why all the sudden hate for Redux?),Mark 都解釋到團隊從來都沒有以 Redux 作為 React 的主流狀態管理工具去營銷它,它之所以變得主流一方面是因為它贏得了 2015 年的 Flux Wars,另一方面它切實解決了開發者的問題。Dan 也寫過一篇文章來刻意強調你也許並不需要 Redux(You Might Not Need Redux)。
如果說我們把 2015 年的 Flux War 比喻成第一次 Flux 世界大戰的話,那麼當下這個時間點第二次世界大戰正進行的如火如荼,在市面上我們可以看到更多優秀的 Flux 框架,比如 Mobx,比如冉冉升起的 Zustand,比如小眾的我很喜歡的 Akita。Redux 很大可能不會是你下一個新專案的最佳選項
在 StafkOverflow 直白的發文求網友推薦技術框架一類的問題通常都會被關閉。因為在沒有任何上下文的前提大部分答案過於主觀了。如何思考我建議你至少參考這個回答(How can Stack Overflow help developers evaluate technologies?),先問問自己:
- 我需要什麼(Know what you need)
- 我不需要什麼(Know what you don't want)
- 我還想要些什麼(Know what you want)
但我可以理解“簡歷驅動開發”是選擇當下的流行技術的重要原因,我也深表同情。
你可能會喜歡
- CSS 裡的整潔架構
- 前端架構 101(一):在談論它們之前我們需要達成的共識
- 前端架構 101(二): MVC 初探
- 前端架構 101(三):MVC 啟示錄:模組的職責,作用域和通訊
- 前端架構 101(四):MVC的不足與Flux的崛起
- 前端架構 101(五):從 Flux 進化到 Model-View-Presenter
- 前端架構 101(六):整潔(Clean Architecture)架構是歸宿
- 【譯文】【前端架構鑑賞 01】:Angular 架構模式與最佳實踐
- 【譯文】【前端架構鑑賞 02】:可擴充 Angular 2 架構
- 【譯文】【前端架構鑑賞 03】:Angular 與 MVP 模式
- 微前端說明書
- 從美團這篇文章聊聊微前端的聚合問題
- 寫給前端看的架構文章(1):MVC VS Flux
- 從MVC模式在前端開發中的侷限性談起
- Flux與Redux背後的設計思想(二):CQRS, Event Sourcing, DDD
- Flux與Redux背後的設計思想(一):Command Bus, Event Bus, Service Bus