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