大家好,我是若川。持續組織了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、拉你進源碼共讀群
今日話題
略。分享、收藏、點贊、在看我的文章就是對我最大的支持~