編寫可維護的前端程式碼
關注微信公眾號“依賴注入”獲得更佳閱讀體驗。
以下是本人一年前在團隊內部分享的整理和補充,水平有限,如有錯誤,請不吝賜教。
大家好,我叫王力國,目前是 RPA 前端團隊負責人,過去一年我們從零構建了 RPA 前端平臺,目前前端維護的程式碼行數在 13 萬行左右,其中超過 92% 以上是 TypeScript 程式碼,主要有以下三個活躍迭代的程式碼倉庫:
- 使用 TS3.5 + Angular8 + Rxjs 構建的管理後臺(目前已經升級到 Angular9)
- 使用 TS3.7 + Electron5 + React16.8 + Redux + Mobx + Nodejs 構建的低程式碼開發平臺
- 使用 TS3.7 + Electron8 + React16.8 + Mobx + Nodejs 構建的桌面應用
在這一段時間裡經過大家的努力,應該來說目前看來以上三個倉庫的程式碼是比較優雅,維護成本是比較低的(大家應該已經半年沒有加班了,哈哈)。我本人在這段時間裡做的主要工作是架構設計和程式碼評審,過程積累了一些經驗,非常開心今天能夠有機會同大家一起交流,今天的分享有 10 個部分,分別是:
在正式開始之前,我要跟大家先宣告一下,以上 10 個部分僅是我個人認為優先順序比較高的問題,單獨整理出來跟大家一起探討,大家如果想要深究到具體某一行程式碼如何編寫的更加優雅的話,可以閱讀市面上更多關於編碼方法論的書,另外,今天這些內容僅適用於上層業務開發的專案,也就是說不適用於開源專案/基礎庫專案。
另外因為我們同時使用了 Angular 和 React,所以今天舉例的時候可能會穿插兩個框架的程式碼,不過不會涉及太多框架相關概念,不會影響大家理解。
1/10. 基本約定:
1. 目錄結構
大部分情況下你的目錄結構組織得好,你的可維護性就已經及格了,而一個好的目錄結構應該是可以自解釋的。
這裡需要特別解釋一下為什麼在 Angular 專案裡你最好還可以有一個 modules
目錄,因為大家時間久了會發現,簡單一個 shared 目錄是很難滿足需求的,有時候你僅僅依賴 shared 裡的一個元件,卻需要匯入整個 shared,這是因為有時候一個路由模組對應的不僅僅是一個領域,在這種情況下,我非常建議你將 shared 目錄按照領域模型拆分得更細,甚至移除 shared 改為 modules 目錄,可以實現一個路由模組按需匯入幾個領域模組。
2. 命名風格
需要說明幾點:
- 型別定義最好加上字首,區別型別和值(這可以通過 TSlint 約束)
json
// tslint.json
{
"rules": {
"interface-name": [true, "always-prefix"],
}
}
- CSS Class 的命名也是可以使用 stylelint 約束的
json
// .stylelintrc.json
{
"rules": {
// example:aa-bb-cc,aa-bb-width120
"selector-class-pattern": "^[a-z][a-z0-9]*((-[a-z0-9]+)*|[a-z0-9]*)$"
}
}
2/10. 型別安全
在早期團隊還只有我一個人的時候為了快速開發選擇使用 es6,後來產品被提升為公司戰略級產品,團隊也在爆發式擴大,工程師水平產生了梯度,型別約束的需求越來越大,於是我們非常果斷決定把整個專案改造至 100% TS,當時為了降低遷移成本,程式碼仍然充斥著大量 'any',讓人感覺沮喪的是,如果不能用好 TS 的型別系統的話,使用 TS 反倒擡高了心智負擔降低了編碼效率。 後來我們發起了一個學習 TS 的熱潮,大家不斷閱讀文件,高階教程,學習市面上設計優雅的 TS 程式碼,嘗試去建設一個比較標準TS體系。
很快專案裡的 any 就在肉眼變少,我們目前已經在全鏈路 lint 檢查中開啟了兩個重要約束:
- 錯誤級別的 -> 不允許 any
- 錯誤級別的 -> 不允許隱式 any
另外這裡要提一點,人的自覺性總是無法被完全信任的,即使你使用了 pre-commit 鉤子來跑 lint,也可以輕易被人繞過,我強烈建議你在雲端 ci 流中加入 lint 檢查,且強制約束未通過 lint 檢查的分支無法被合併。
在早期開啟全面禁用 any 之後,我們編碼的效率非常低下,很多時候我們不得不編寫大量的型別定義。
不過好在大部分情況下,我們使用的工具或者庫已經匯出了一些工具型別,我們需要自行編寫的工具型別非常少,跟大家推薦幾個資源可以幫助你更快地編寫更安全的 TS 程式碼:
3/10. 註釋有罪
這種註釋看起來很搞笑吧?但是我相信你的專案裡一定有,而且還在源源不斷產生這種註釋。如何編寫註釋確實是一門藝術,寫太多寫太少都要被人罵。我的建議非常簡單:永遠不要註釋,除非你有充分的理由。
大家想想什麼情況下你會抱怨程式碼沒有註釋?
- 很難看懂或完全看不懂
- 以為看懂其實誤解了(刪除一段以為無用的程式碼導致了嚴重的問題)
那麼這就很好理解我們什麼時候需要註釋了:
- 複雜的程式碼: 複雜業務/使用了難以理解的技術,取巧的實現方法
- 妥協的程式碼: 設計不好,但是為了實現業務又暫時沒有其他更好選擇
- 相容性程式碼: 向下/平臺相容程式碼最好註釋,避免誤刪
4/10 配置分離
對於元件級別的差異,很多時候 Props
就夠了,如果是多個元件組成的一個模組需要差異化的話,你可能會使用多級引數透傳或者是靜態變數的方式,不過我想告訴你大多數時候可能 Provider
更合適,尤其是你希望同時在一個應用生命週期裡使用幾種配置方案。
其實大家對 Provider
並不陌生,不管是 Angular 或者是 React 實際上都可以輕鬆使用 Provider
,如下
這裡要特別說一下,大家在 Angular
專案中編寫複用模組時,最好養成暴露 InjectToken
的習慣,即使它看起來暫時還不需要配置。
5/10 狀態管理
市面上關於全域性狀態管理方案已經比較成熟了,比如 Rxjs,Mobx,Redux... 今天我們不會再過多探討全域性狀態管理,更多想要跟大家探討一下區域性狀態管理。
首先問大家一個問題:區域性狀態管理可以被複用麼?或者說應該複用區域性狀態管理程式碼麼?🤔
再問一個問題:如果可以被複用,那麼我們應該使用組合還是使用繼承呢?🤔
我的建議是:可以複用,但最好不要使用繼承,可以考慮組合。
實際上在 Angular 裡,你只需要簡單的從元件級別注入服務就可以複用區域性狀態管理程式碼了。是不是好簡單?實際上這就是 MVP
架構的實現,這裡的元件級別 Providers 充當的就是 Presenter 層。
而在 16.8 版本 React 開始,你可以使用 Hooks 來組合區域性狀態管理,相信大家應該都有聽說過,我們實際用下來的感受是:好用是真的好用,坑也是真的多。
我們在過去使用過程中也沉澱了自己的 Hooks 庫(@bixi/hooks),大家有興趣可以拿去玩玩。
6/10 效能優化
我對效能優化的一貫看法是:最好的時機就是專案立項的時候,其次是現在。
大家最好還是能夠養成編寫高效能程式碼的習慣,以下是我們在開發過程中經常會使用的一些效能優化手段,不細說,大家可以自己挨個去研究。
7/10 版本管理
這裡版本管理有兩個部分:
- 依賴庫的版本管理:
- 務必鎖定第三方依賴(yarn.lock)
- 業務程式碼的版本管理
- 至少向下相容一個主版本
- 相容程式碼打上標記,做好備註
- 定時清理相容程式碼
8/10 適度封裝
說到封裝,我們先看下面這個程式碼段變化過程
- 你需要寫一段程式碼,監聽四個元件的值變動,同步到本地 State 中,你寫了左邊這樣的程式碼,覺得程式碼很簡潔,提交程式碼下班
- 需求發生變動,a 和 b 元件值變動時候會帶來一些副作用,於是你將程式碼調整成右邊這樣,程式碼似乎在慢慢朝著不可控制的方向演進
- 你開始抱怨是因為需求很亂才導致程式碼很糟糕....
我在過去的程式碼評審過程中發現很多同學會很喜歡編寫上面這種程式碼,他們都能給我一個很好的理由:函式拆得細好複用啊。
可是,你的程式碼真的有被複用麼?
實際上,在業務程式碼開發過程中,很多時候過度封裝反倒會提高複雜度降低可維護性,因此我經常跟大家說的一句話是 “你把它搞複雜了”。
我們可以試試用最笨最簡單的方式改造一下上面這個程式碼如下
大家會發現,這段程式碼行數看起來變多了,但是維護起來變得特別簡單,我在處理 a 邏輯時候我才不關心會不會影響到 b/c/d...
9/10 元件設計
我們來看下面這個元件設計需求,這是我們在應用裡的一個真實元件,它有以下四個主要特性:
有些同學的元件拆解原則是:管它三七二十一,先拆到它不能再拆為止。
那麼在遇到上面這個元件設計需求的時候它會拆解成下面左邊這樣,甚至粒度更細,那麼拆成這樣有什麼問題呢?
-
1/2/3/4 的樣式是耦合的,拆解會導致樣式編寫困難
-
5/6/7 元件是沒有過多複用價值的,拆解只會提升複雜度
很明顯,不是元件拆得越細就越好,因此們更推薦你像右圖這樣拆解成兩個元件就夠了(當然如果你的程式碼複雜的話,可以再適當拆解)。
我們再看下面這個簡易的 Input
元件,看看它有幾宗罪:
- 疑似同其它元件的樣式有依賴關係
css
input{
border-right: none;
}
-
元件不具備複用性,產生了一個
searchTasks
事件 -
元件暴露了實現細節,需要依賴外部的
validate
方法 -
元件在初始化時拷貝了
value
副本,之後並沒有繼續監聽外部value
變化重新整理副本,導致不是資料驅動的,不是冪等的
10/10 防禦式程式設計
編寫更可靠的程式碼當然應該是我們的長期追求,但一些不好的程式設計習慣可能會讓你的程式碼問題變得難以追蹤,你的錯誤監控工具(我們使用 sentry 監控程式碼錯誤)可能會變得形同虛設,比如上圖中的程式碼:
```typescript // bad if (this.editor) { this.editor.destory(); }
// bad,等價於上面程式碼 this.editor?.destory(); ```
之所以說它不好,因為我們知道在這個元件掛載之後,this.editor
是應該必然存在的,那麼我們在解除安裝的時候如果不存在我們應該及時丟擲錯誤,而不是悄無聲息吞噬掉,更好的處理方式應該是下面這種方式:
typescript
// good,我們斷定它一定存在
(this.ediotor as Editor).destory();
總得來說,你最好還是謹慎使用 lodash
或者 optional chaining
,該拋錯的地方就讓它及時丟擲來。
當然為了不讓你的應用奔潰得太難看,你還是需要做好錯誤收集以及 UI 降級。
謝謝大家。