一份 2.5k star 的《React 开发思想纲领》
大家好,我是若川。持續組織了6個月源碼共讀活動,感興趣的可以點此加我微信 ruochuan12?參與,每周大家一起學習200行左右的源碼,共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》?包含20余篇源碼文章。歷史面試系列
翻譯自:https://github.com/mithi/react-philosophies[1]?2.5k star
原文作者:mithi[2]
已獲作者授權
概要
介紹
最低要求
面向幸福設計
性能優化技巧
測試原則
🧘 0. 介紹
《React 開發思想綱領》是:
我開發 React 時的一些思考
每當我 review 他人或自己的代碼時自然而然會思考的東西
僅僅作為參考和建議,并非嚴格的要求
會隨著我的經驗不斷更新
大多數技術點是基礎的重構方法論,SOLID 原則以及極限編程等思想的變體,僅僅是在 React 中的實踐而已 🙂
你可能會覺得我寫的這些非常基礎。但以下示例都來自一些復雜大型項目的線上代碼。
《React 開發思想綱領》的靈感來源于我實際開發中遇到的各種場景。
🧘 1. 最低要求
1.1 計算機比你更「智能」
使用 ESLint 來靜態分析你的代碼,開啟 rule-of-hooks 和 exhaustive-deps 這兩個規則來捕獲 React 錯誤。
開啟 JS 嚴格模式吧,都 2202 年了。
直面依賴,解決在useMemo,useCallback 和 useEffect 上 exhaustive-deps 規則提示的 warning 或 error 問題。可以將最新的值掛在 ref 上來保證這些 hook 在回調中拿到的都是最新的值,同時避免不必要的重新渲染。
使用 map 批量渲染組件時,都加上 key。
只在最頂層使用 hook,不要在循環、條件或嵌套語句中使用 hook。
理解不能對已經卸載的組件執行狀態更新的控制臺警告。
給不同層級的組件都添加錯誤邊界(Error Boundary)來防止白屏,還可以用它來向錯誤監控平臺(比如 Sentry)上報錯誤,并設置報警。
不要忽略了控制臺中打印的錯誤和警告。
記得要 tree-shaking!
使用 Prettier 來保證代碼的格式化一致性!
使用 Typescript 和 NextJS這樣的框架來提升開發體驗。
強烈推薦 Code Climate(或其他類似的)開源庫。這類工具會自動檢測代碼異味(Code Smell,代碼中的任何可能導致深層次問題的癥狀),它可以促使我去處理項目里留下的技術債。
1.2 Code is just a necessary evil
譯者注:程序員的目標是解決客戶的問題,代碼只是副產品
1.2.1 先思考,再加依賴
依賴加的越多,提供給瀏覽器的代碼就越多。捫心問問自己,你是否真的使用了某個庫的 feature?
🙈 ?你真的需要它嗎? 看看這些你可能不需要的依賴
你是否真的需要 Redux?有可能需要,但其實 React 本身也是一個狀態管理庫。
你是否真的需要 Apollo client?Apollo client 有許多很強大的功能,比如數據規范化。但使用的同時也會顯著提高包體積。如果你的項目使用的并非是 Apollo client 特有的 feature,可以考慮使用一些輕量的庫來替代,比如 react-query 或 SWR(或者根本不用)。
Axios 呢?Axios 是一個很棒的庫,它的一些特性不容易通過原生的 fetch API 來復刻。但是如果使用 Axios 只是因為它有更好的 API,完全可以考慮在 fetch 上做一層封裝(比如 redaxios 或自己實現)。取決于你的 App 是否真正地使用了 Axios 的核心 feature。
Decimal.js 呢?或許 Big.js 或者其他輕量的庫就足夠了。
Lodash/underscoreJS呢?推薦你看看【你不需要系列之“你不需要 Lodash/Underscore”】[3]。
MomentJS呢?【你不需要系列之“你不需要 Momentjs”】[4]。
你不需要為了主題(淺色/深色模式)而使用 Context,考慮下用 css 變量 代替。
你甚至不需要 Javascript,CSS 也足夠強大。【你不需要系列之“你不需要 JavaScript”】[5]
1.2.2 不要自作聰明,提前設計
"我們的軟件在未來會如何迭代?可能會這樣或者那樣,如果在當下就開始往這些方向進行代碼設計,這就叫 future-proof(防過時,面向未來編程)。"
不要這樣搞! 應該在面臨需求的時候再去實現相應功能,而不是在你預見到可能需要的時候。代碼應該越少越好!
1.3 發現了就優化它
1.3.1 檢測代碼異味(Code Smell),并在必要時對其進行處理。
當你意識到某個地方出現了問題,那就馬上處理掉。但如果當前不容易修復,或者沒有時間,那請至少添加一條注釋(FIXME 或者 TODO),附上對該問題的簡要描述。來讓項目里的每個人都知道這里有問題,讓他們意識到當他們遇到這樣的情況時也該這樣做。
🙈 來看看這些容易發現的代碼異味
? 定義了很多參數的函數或方法
? 難以理解的,返回 Boolean 值的邏輯
? 單個文件中代碼行數太多
? 在語法上可能相同(但格式化可能不同)的重復代碼
? 可能難以理解的函數或方法
? 定義了大量函數或方法的類/組件
? 單個函數或方法中的代碼行數太多
? 具有大量返回語句的函數或方法
? 不完全相同但代碼結構類似的重復代碼(比如變量名可能不同)
切記,代碼異味并不一定意味著代碼需要修改,它只是告訴你,你應該可以想出更好的方式來實現相同的功能。
1.3.2 無情的重構。簡單比復雜好。
💁?♀? 小技巧: 簡化復雜的條件語句,最好能提前 return。
🙈 提前 return 的示例
#???不太好if?(loading)?{return?<LoadingScreen?/> }?else?if?(error)?{return?<ErrorScreen?/> }?else?if?(data)?{return?<DataScreen?/> }?else?{throw?new?Error('This?should?be?impossible') }#???推薦if?(loading)?{return?<LoadingScreen?/> }if?(error)?{return?<ErrorScreen?/> }if?(data)?{return?<DataScreen?/> }throw?new?Error('This?should?be?impossible')💁?♀? 小技巧: 比起傳統的循環語句,鏈式的高階函數更優雅
如果沒有明顯的性能差異,盡量使用鏈式的高階函數(map, filter, find, findIndex, some等) 來代替傳統的循環語句。
1.4 你可以做的更好
💁?♀? 小技巧: 可以在 setState 時傳入回調函數,所以沒必要把 state 作為一個依賴項
你不用把 setState 和 dispatch 放在 useEffect 和 useCallback 這些 hook 的依賴數組中。ESLint 也不會給你提示,因為 React 已經確保了它們不會出錯。
#???不太好 const?decrement?=?useCallback(()?=>?setCount(count?-?1),?[setCount,?count]) const?decrement?=?useCallback(()?=>?setCount(count?-?1),?[count])#???推薦 const?decrement?=?useCallback(()?=>?setCount(count?=>?(count?-?1)),?[])💁?♀? 小技巧: 如果你的 useMemo 或 useCallback 沒有任何依賴,那你可能用錯了
#???不太好 const?MyComponent?=?()?=>?{const?functionToCall?=?useCallback(x:?string?=>?`Hello?${x}!`,[])const?iAmAConstant?=?useMemo(()?=>?{?return?{x:?5,?y:?2}?},?[])/*?接下來可能會用到?functionToCall?和?iAmAConstant?*/ }#???推薦 const?I_AM_A_CONSTANT?=??{?x:?5,?y:?2?} const?functionToCall?=?(x:?string)?=>?`Hello?${x}!` const?MyComponent?=?()?=>?{/*?接下來可能會用到?functionToCall?和?I_AM_A_CONSTANT?*/ }💁?♀? 小技巧: 巧用 hook 封裝自定義的 context,會提升 API 可讀性
它不僅看起來更清晰,而且你只需要 import 一次,而不是兩次。
? 不太好
//?你每次需要?import?兩個變量 import?{?useContext?}?from?'react'; import?{?SomethingContext?}?from?'some-context-package';function?App()?{const?something?=?useContext(SomethingContext);?//?看起來?ok,但可以更好//?... }? 推薦
//?在另一個文件中,定義這個?hook function?useSomething()?{const?context?=?useContext(SomethingContext);if?(context?===?undefined)?{throw?new?Error('useSomething?must?be?used?within?a?SomethingProvider');}return?context; }//?你只需要?import?一次 import?{?useSomething?}?from?'some-context-package';function?App()?{const?something?=?useSomething();?//?看起來會更清晰//?... }💁?♀? 小技巧: 在寫組件之前,先思考該怎么用它
設計 API 很難,README 驅動開發(RDD)是個很有用的辦法,可以幫助你設計出更好的 API。并不是說應該無腦使用 RDD,但它背后的思想是很值得學習的。我自己發現,在設計實現組件 API 之前,使用 RDD 通常比不用時設計地更好。
🧘 2. 面向幸福設計
太長不看版
💖 通過刪除冗余的狀態來減少狀態管理的復雜性。
💖 “傳遞香蕉,而不是拿著香蕉的大猩猩和整個叢林“(意思是組件要什么傳什么,不要傳大對象)。
💖 讓你的組件小而簡單 —— 單一職責原則。
💖 復制比錯誤的抽象要“便宜”的多(避免提早/不恰當的設計)。
避免 prop 層層傳遞(又叫 prop 鉆取,prop drilling)。Context 不是解決狀態共享問題的銀彈。
將巨大的 useEffect 拆分成獨立的小 useEffect。
將邏輯提取出來都放到 hook 和工具函數中。
useCallback, useMemo 和 useEffect 依賴數組中的依賴項最好都是基本類型。
不要在 useCallback, useMemo 和 useEffect 中放入太多的依賴項。
為了簡單起見,如果你的狀態依賴其他狀態和上次的值,考慮使用 useReducer,而不是使用很多個 useState。
Context 不一定要放在整個 app 的全局。把 Context 放在組件樹中盡可能低的位置。同樣的道理,你的變量,注釋和狀態(和普通代碼)也應該放在靠近他們被使用的地方。
💖 2.1 刪除冗余的狀態來減少狀態管理的復雜性
冗余的狀態指可以通過其他狀態經過推導得到的狀態,不需要單獨維護(類似 Vue computed),當你有冗余的狀態時,一些狀態可能會丟失同步性,在面對復雜交互的場景時,你可能會忘記更新它們。
刪除這些冗余的狀態,除了避免同步錯誤外,這樣的代碼也更容易維護和推理,而且代碼更少。
💖 2.2 “傳遞香蕉,而不是拿著香蕉的大猩猩和整個叢林“
為了避免掉入這種坑,最好將基本類型(boolean, string, number 等)作為 props 傳遞。(傳遞基本類型也能更好的讓你使用 React.memo 進行優化)
組件應該僅僅只了解和它運作相關的內容就足夠了。應該盡可能地與其他組件產生協作,而不需要知道它們是什么或做什么。
這樣做的好處是,組件間的耦合會更松散,依賴程度會更低。低耦合更利于組件修改,替換和移除,而不會影響其他組件。
💖 2.3 讓你的組件小而簡單
什么是「單一職責原則」?
一個組件應該有且只有一個職責。應該盡可能的簡單且實用,只有完成其職責的責任。
具有各種職責的組件很難被復用。幾乎不可能只復用它的部分能力,很容易與其他代碼耦合在一起。那些抽離了邏輯的組件,改起來負擔不大而且復用性更強。
如何判斷一個組件是否符合單一職責?
可以試著用一句話來描述這個組件。如果它只負責一個職責,描述起來會很簡單。如果描述中出現了“和“或“或”,那么這個組件很大概率不是單一職責的。
檢查組件的 state,props 和 hooks,以及組件內部聲明的變量和方法(不應該太多)。問問自己:是否這些內容必須組合到一起這個歌組件才能工作?如果有些不需要,可以考慮把它們抽離到其他地方,或者把這個大組件拆解成小組件。
🧘 3. 性能優化技巧
如果你覺得應用速度慢,就應該做一次基準測試(benchmark)來證明。 "面對模凌兩可的情況,拒絕猜測。" 多使用 Chrome 插件 - React 開發者工具的 profiler!
useMemo 主要用在大開銷的計算上。
如果你打算使用 React.memo, useMemo, 和 useCallback 來減少重新渲染,它們不該有過多的依賴項,且這些依賴項最好都是基本類型。
確保你清楚代碼里 React.memo, useCallback 或 useMemo 它們都是為了什么而使用的(是否真的能防止重新渲染?是否能證明在這些場景中真的可以顯著提高性能? Memoization 有時會起到反作用,所以需要關注!)
優先修復慢渲染,再修復重新渲染。
把狀態盡可能地放在它被使用的地方,一方面讓代碼讀起來更順,另一方面,能讓你的 app 更快(state colocation(狀態托管))
Context 應該按邏輯分開,不要在一個 provider 中管理多個 value。如果其中某個值變化了,所有使用該 context 的組件(即便沒有用到這個值),都會重新渲染。
可以通過拆分 state 和 dispatch 來優化 context。
了解下 lazy loading(懶加載)和 bundle/code splitting(代碼分割)。
長列表請使用 tannerlinsley/react-virtual 或其它類似的庫。
包體積越小,app 越快。你可以使用 source-map-explorer 或者 @next/bundle-analyzer(用于 NextJS) 來進行包體積分析。
關于表單的庫,推薦使用 react-hook-forms,它在性能和開發體驗各方面都做的比較好。
🧘 4. 測試原則
測試應該始終與軟件的使用方式相似。
確保不是在測試一些邊界細節(用戶不會使用,看不到甚至感知不到的內容)。
如果你的測試不能讓你對自己的代碼產生信任,那測試就是無意義的。
如果你正在重構某個代碼,且最后實現的功能都是完全一致的,其實幾乎不需要修改測試,而且可以通過測試結果來判定你正確的重構了。
對于前端來說,不需要 100% 的測試覆蓋率,70% 就足夠了。測試應該提升你的開發效率,雖然維護測試會暫時地阻塞你目前的開發,但當你不斷地增加測試,會在不同階段得到不同的回報。
我個人喜歡使用 Jest,React testing library,Cypress,和 Mock service worker。
End
翻譯的不好,請大家見諒。如有任何想法,歡迎評論交流
參考資料
[1]
https://github.com/mithi/react-philosophies: https://github.com/mithi/react-philosophies
[2]mithi: https://github.com/mithi
[3]【你不需要系列之“你不需要 Lodash/Underscore”】: https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore
[4]【你不需要系列之“你不需要 Momentjs”】: https://github.com/you-dont-need/You-Dont-Need-Momentjs
[5]【你不需要系列之“你不需要 JavaScript”】: https://github.com/you-dont-need/You-Dont-Need-JavaScript
·················?若川簡介?·················
你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》20余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經堅持寫了8年,點擊查看年度總結。
同時,最近組織了源碼共讀活動,幫助3000+前端人學會看源碼。公眾號愿景:幫助5年內前端人走向前列。
掃碼加我微信 ruochuan02、拉你進源碼共讀群
今日話題
略。分享、收藏、點贊、在看我的文章就是對我最大的支持~
總結
以上是生活随笔為你收集整理的一份 2.5k star 的《React 开发思想纲领》的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 前端学习(2963):跨域问题
- 下一篇: 只需 1 分钟,这个网站用 AI 分离歌