原發於知乎專欄,歡迎關注 zhuanlan.zhihu.com/p/55357377
2019 農曆新年即將到來,是時候總結一下團隊過去一年的技術沉澱。過去一年我們支撐的資料相關業務突飛猛進,其中兩個核心平臺級產品程式碼量分別達到30+萬行和80+萬行,TS 模組數均超過1000個,協同開發人員增加到20+人。由於歷史原因,開發框架同時基於 React 和 Angular,考慮到產品的複雜性、人員的短缺和技術背景各異,我們嘗試了各種方法打磨工具體系來提升開發效率,以下是節選的5項主要方法。
一、基於 Redux 的狀態管理
從2013年React釋出至今已近6個年頭,前端框架逐漸形成 React/Vue/Angular 三足鼎立之勢。幾年前還在爭論單向繫結和雙向繫結孰優孰劣,現在三大框架已經不約而同選擇單向繫結,雙向繫結淪為單純的語法糖。框架間的差異越來越小,加上 Ant-Design/Fusion-Design/NG-ZORRO/ElementUI 元件庫的成熟,選擇任一你熟悉的框架都能高效完成業務。
那接下來核心問題是什麼?我們認為是 狀態管理。簡單應用使用元件內 State 方便快捷,但隨著應用複雜度上升,會發現資料散落在不同的元件,元件通訊會變得異常複雜。我們先後嘗試過原生 Redux、分形 Fractal 的思路、自研類 Mobx 框架、Angular Service,最終認為 Redux 依舊是複雜應用資料流處理最佳選項之一。
慶幸的是除了 React 社群,Vue 社群有類似的 Vuex,Angular 社群有 NgRx 也提供了幾乎同樣的能力,甚至 NgRx 還可以無縫使用 redux-devtools 來除錯狀態變化。
無論如何優化,始終要遵循 Redux 三原則:
原則 | 方法 | 引發的問題 |
---|---|---|
Single source of truth | 元件 Stateless,資料來源於 Store | 如何組織 Store? |
State is read-only | 只能通過觸發 action 來改變 State | action 數量膨脹,大量樣板程式碼 |
Changes are made with pure functions | Reducer 是純函式 | 副作用如何處理,大量樣板程式碼 |
這三個問題我們是通過自研 iron-redux 庫來解決,以下是背後的思考:
如何組織 Action?
- action type 需要全域性惟一,因此我們給 action type 新增了 prefix,其實就是 namespace 的概念
- 為了追求體驗,請求(Fetch)場景需要處理 3 種狀態,對應 LOADING/SUCCESS/ERROR 這 3 個action,我們通過
FetchTypes
型別來自動生成對應到 3 個 action
如何組織 Store/Reducer?
- reducer 和 view 不必一一對應,應用中同時存在元件樹和狀態樹,按照各自需要去組織,通過 connect 來繫結狀態樹的一個或多個分支到元件樹
- 通過構造一些預設資料型別來減少樣板程式碼。對於 Fetch 返回的資料我們定義了 AsyncTuple 這種型別,減少了樣板程式碼
- 明確的組織結構,第1層是 ROOT,第2層是各個頁面,第3層是頁面內的卡片,第4層是卡片的資料,這樣劃分最深處基本不會超過5層
最終我們得到如下扁平的狀態樹。雖龐大但有序,你可以快速而明確的訪問任何資料。
[Redux 狀態樹]如何減少樣板程式碼? 使用原生 Redux,一個常見的請求處理如下。非常冗餘,這是 Redux 被很多人詬病的原因
const initialState = {
loading = true,
error = false,
data = []
};
function todoApp(state = initialState, action) {
switch (action.type) {
case DATA_LOADING:
return {
...state,
loading: true,
error: false
}
case DATA_SUCCESS:
return {
...state,
loading: false,
data: action.payload
}
case DATA_ERROR:
return {
...state,
loading: false,
error: true
}
default:
return state
}
}
複製程式碼
使用 iron-redux 後:
class InitialState {
data = new AsyncTuple(true);
}
function reducer(state = new InitialState(), action) {
switch (action.type) {
/** 省略其它 action 處理 */
default:
return AsyncTuple.handleAll(prefix, state, action);
}
}
複製程式碼
程式碼量減少三分之二!!
主要做了這2點:
- 引入了預設的
AsyncTuple
型別,就是{data: [], loading: boolean, error: boolean}
這樣的資料結構; - 使用
AsyncTuple.handleAll
處理 LOADING/SUCCESS/ERROR 這 3 種 action,handleAll 的程式碼很簡單,使用 if 判斷 action.type 的字尾即可,原始碼在這裡。
曾經 React 和 Angular 是兩個很難調和的框架,開發中浪費了我們大量的人力。通過使用輕量級的 iron-redux,完全遵循 Redux 核心原則下,我們內部 實現了除元件層以外幾乎所有程式碼的複用。開發規範、工具庫達成一致,開發人員能夠無縫切換,框架差異帶來的額外成本降到很低。
二、全面擁抱 TypeScript
TypeScript 目前可謂大紅大紫,根據 2018 stateofjs,超過 50% 的使用率以及 90% 的滿意度,甚至連 Jest 也正在從 Flow 切換到 TS。如果你還沒有使用,可以考慮切換,絕對能給專案帶來很大提升。過去一年,我們從部分使用 TS 變為全面切換到 TS,包括我們自己開發的工具庫等。
TS 最大的優勢是它提供了強大的靜態分析能力,結合 TSLint 能對程式碼做到更加嚴格的檢查約束。傳統的 EcmaScript 由於沒有靜態型別,即使有了 ESLint 也只能做到很基本的檢查,一些 typo 問題可能線上出了 Bug 後才被發現。
下圖是一個前端應用常見的4層架構。 程式碼和工具全面擁抱 TS 後,實現了從後端 API 介面到 View 元件的全鏈路靜態分析,具有了完善的程式碼提示和校驗能力。
[前後端協作簡圖]除了上面講的 iron-redux,我們還引入 Pont 實現前端取數,它可以自動把後端 API 對映到前端可呼叫的請求方法。
Pont 實現原理:(法語:橋) 是我們研發的前端取數層框架。對接的後端 API 使用 Java Swagger,Swagger 能提供所有 API 的元資訊,包括請求和響應的型別格式。Pont 解析 API 元資訊生成 TS 的取數函式,這些取數函式型別完美,並掛載到 API 模組下。最終程式碼中取數效果是這樣的:
Pont 實現的效果有:
- 根據方法名自動匹配 url、method,並且對應到 prams、response 型別完美,並能自動提示
- 後端 API 介面變更後,前端相關聯的請求會自動報錯,再也不擔心後端悄悄改介面前端不知曉
- 再也不需要前後端介面約定文件,使用程式碼保證前端取數和後端介面定義完全一致
另外 iron-redux 能接收到 Pont 介面響應資料格式,並推匯出整個 Redux 狀態樹的靜態型別定義,Store 中的資料完美的型別提示。效果如下:
最終 TS 讓程式碼更加健壯,尤其是對於大型專案,編譯通過幾乎就代表執行正常,也給重構增加了很多信心。
三、迴歸 Sass/Less
2015 年我們就開始實踐 CSS Modules,包括後來的 styled-components 等,到 2019 年 css-in-js 方案依舊爭論不休,雖然它確實解決了一些 CSS 語言天生的問題,但同時增加了不少成本,新手不夠友好、全域性樣式覆蓋成本高漲、偽類處理複雜、與AntD等元件庫結合有坑。與此同時 Sass/Less 社群也在飛速發展,尤其是 Stylelint 的成熟,可以通過技術約束的手段來避免 CSS 的 Bad Parts。
- 全域性汙染:約定每個樣式檔案只能有一個頂級類,如
.home-page{ .top-nav {/**/}, .main-content{ /**/ } }
。如果有多個頂級類,可以使用 Stylelint rule 檢測並給出警告。 - 依賴管理不徹底。藉助 webpack 的 css-loader,已夠用。
- JS 和 CSS 變數共享。關於 JS 和 Sass/Less 變數共享,我們摸索出了自己的解法:
// src/styles/variables.js
module.exports = {
// 主顏色
'primary-color': '#0C4CFF',
// 出錯顏色
'error-color': '#F15533',
// 成功顏色
'success-color': '#35B34A',
};
複製程式碼
// webpack.config.js
const styleVariables = require('src/styles/variables');
// ...
{
test: /\.scss$/,
use: [
'style-loader',
'css-loader?sourceMap&minimize',
{
loader: 'sass-loader',
options: {
data: Object.keys(styleVariables)
.map(key => `\$${key}: ${styleVariables[key]};`)
.join('\n'),
sourceMap: true,
sourceMapContents: true
}
}
]
}
//...
複製程式碼
在 scss 檔案中,可以直接引用變數
// page.scss
.button {
background: $primary-color;
}
複製程式碼
四、開發工具覆蓋全鏈路
2019 年,你幾乎不可能再開發出 React/Angular/Vue 級別的框架,也沒必要再造 Ant-Design/Fusion-Design/Ng-Zorro 這樣的輪子。難道就沒有機會了嗎?
當然有,結合你自身的產品開發流程,依舊有很多機會。下面是常規專案的開發流程圖,任何一個環節只要深挖,都有提升空間。如果你能通過工具減少一個或多個環節,帶來的價值更大。
單拿其中的【開發】環節展開,就有很多可擴充套件的場景:
一個有代表性的例子是,我們開發了國際化工具 kiwi。它同樣具有 TS 的型別完美,非常強大的文案提示,另外還有:
- VS Code 外掛 kiwi linter,自動對中文文案標紅,如果已有翻譯文案能自動完成替換
- Shell 命令全量檢查出沒有翻譯的文案,批量提交給翻譯人員
- Codemod 指令碼自動實現舊的國際化方案向 Kiwi 遷移,成本極低
除了以上三點,未來還計劃開發瀏覽器外掛來檢查漏翻文案,利用 Husky 在 git 提交前對漏翻文案自動做機器翻譯等等。
未來如果你只提供一個程式碼庫,那它的價值會非常侷限。你可以參照上面的圖表,開發相應的擴充套件來豐富生態。如果你是新手,推薦學習下編譯原理和對應的擴充套件開發規範。
五、嚴格徹底的 Code Review
過去的一年,我們一共進行了 1200+ 多次 Code Review(CR),很多同事從剛開始不好意思提 MR 到後來追著別人 Review,CR 成為每個人的習慣。通過 CR 讓專案中任何一行程式碼都至少被兩人觸達過,減少了絕大多數的低階錯誤,提升了程式碼質量,這也是幫助新人成長最快的方式之一。
【其中一個專案MR截圖】
Code Review 的幾個技巧:
- No magic
- Explicit not implicit
- 覆蓋度比深度重要,覆蓋度追求100%
- 頻率比儀式感重要,坐公交蹲廁所開啟手機都可以 Review 別人程式碼,不需要專門組織會議
- 粒度要儘可能小,一個元件一個方法均可,可以結合 Git Flow
- 24h 小時內處理,無問題直接 merge,有問題一定要留 comment,並且提供 action
- 對於亟待上線來不及 Review 的程式碼,可以先合併上線,上線後再補充 Review
- 需要自上而下的推動,具有完善的規範,同時定期總結 Review 經驗來豐富開發規範
- CR 並不只是為了找錯,看到好的程式碼,不要吝嗇你的讚美
- 本質是鼓勵開發者間更多的溝通,互相學習,營造技術文化氛圍
總結
以上5點當然不是我們技術的全部。除此之外我們還實踐了移動端開發、視覺化圖表/WebGL、Web Worker、GraphQL、效能優化等等,但這些還停留在術的層面,未來到一定程度會拿出來分享。
如果你也準備或正在開發複雜的前端應用,同時團隊人員多樣技術背景各異,可以參考以上5點,使用 Redux 實現規範清晰可預測的狀態管理,深耕 TypeScript 來提升程式碼健壯性和可維護性,藉助各種 Lint 工具迴歸簡單方便的 CSS,不斷打磨自己的開發工具來保證開發規範高效,並嚴格徹底實行 Code Review 促進人的交流和提升。
Links
- Pont:github.com/nefe/pont
- Kiwi:github.com/nefe/kiwi
- iron-redux: github.com/nefe/iron-r…
- The State of JavaScript 2018
我們還在招聘,歡迎郵件簡歷,來信必回。shaoyin.ssy@alibaba-inc.com