作者簡介 導演 螞蟻金服·資料體驗技術團隊
螞蟻金服資料平臺前端團隊主要負責多個資料相關的PC Web單頁面應用程式,業務複雜度類比Excel等桌面應用,業務前端程式碼量在幾萬行~幾十萬行,隨著產品不斷完善,破百萬指日可待。管理好10萬行級甚至百萬行級程式碼的前端應用,是我們團隊的核心挑戰之一。
接下來的系列文章,我會嘗試從以下幾個角度介紹我們團隊應對挑戰的方法:
- 前端架構
- 質量保障
- 效能優化
- 團隊前端開發流程
- 人員素養
前端架構
團隊的架構方案是多個產品經歷一年的持續迭代,不斷摸索出來的一套適合本團隊資料產品業務場景的架構方案,架構方案中還存在尚未解決的痛點和有爭議的部分需要持續優化,不保證這套架構適合您的產品。
產品特點
先介紹下我們團隊的產品特點:
- ToB產品,業務複雜度高、業務理解門檻高;
- 前端程式碼量巨大(資料分析產品從零開始經歷8個月迭代業務程式碼8萬行,僅實現了產品長期規劃需求的20%)
架構方案
架構的目的是管理複雜度,將複雜問題分而治之、有效管理,我們的具體方法如下:
1. 首先通過路由切割“頁面級”粒度的功能模組
這裡的“頁面級”粒度指一個路由對映的元件
2. 同一“頁面”內的模組再劃分
劃分原則:
- 縱向:通過業務功能(可根據檢視模組判斷)劃分
- 橫向:通過Model-View-Controller三種不同職能劃分
3. 合併同類項
繼續細分粒度,然後將可複用模組或元件抽離到公共區域
3.1 資料模型
資料模型根據職責分成兩類:
- Domain Model 領域模型
- App State Modal 應用狀態模型
3.1.1 領域模型
領域模型是業務資料,往往要持久化到資料庫或localStorage中,屬於可跨模組複用的公共資料,如:
- Users 使用者資訊
- Datasets 資料集資訊
- Reports 報表資訊
領域模型作為公共資料,建議統一存放在一個叫做Domain Model Layer的架構獨立分層中(前端業界一般對這層的命名為ORM層)。
下沉到Domain Model Layer(領域模型層)有諸多利處:
- 跨模組資料同步問題不復存在,例如:之前Users物件在A和B兩個業務模組中單獨儲存,A模組變更Users物件後,需將Users變更同步到B模組中,如不同步,A、B模組在介面上呈現的User資訊不一致,下沉到領域模型層統一管理後,問題不復存在;
- 除領域模型複用外,還可複用領域模型相關的CRUD Reducer,例如:之前Users物件對應的Create Read Update Delete方法可能在A和B兩個業務模組各維護一套,下沉到領域模型層統一管理後,減少了程式碼重複問題;
- 自然承擔了部分跨模組通訊職責,之前資料同步相關的跨模組通訊程式碼沒有了存在的必要性;
3.1.2 應用狀態模型
應用狀態模型是與檢視相關的狀態資料,如:
- 當前頁面選中了列表的第n行 currentSelectedRow: someId
- 視窗是否處於開啟狀態 isModalShow: false
- 某種檢視元素是否在拖拽中 isDragging: true
這些資料與具體的檢視模組或業務功能強相關,建議存放在業務模組的Model中。
3.2 檢視層元件
元件根據職責劃分為兩類:
- Container Component 容器型元件
- Presentational Component 展示型元件
3.2.1 容器型元件
容器型元件是與store直連的元件,為展示型元件或其它容器元件提供資料和行為,儘量避免在其中做一些介面渲染相關的事情。
3.2.2 展示型元件
展示型元件獨立於應用的其它部分內容,不關心資料的載入和變更,保持職責單一,僅做檢視呈現和最基本互動行為,通過props
接收資料和回撥函式輸出結果,保證接收的資料為元件資料依賴的最小集。
一個有成百上千展示型元件的複雜系統,如果展示型元件粒度切分能很好的遵循高內聚低耦合和職責單一原則的話,可以沉澱出很多可複用的通用業務元件。
3.3 公共服務
- 所有的HTTP請求放在一起統一管理;
- 日誌服務、本地儲存服務、錯誤監控、Mock服務等統一存放在公共服務層;
按照上面三點合併同類項後,業務架構圖變更為
4. 跨模組通訊
模組粒度逐漸細化,會帶來更多的跨模組通訊訴求,為避免模組間相互耦合、確保架構長期乾淨可維護,我們規定:
- 不允許在一個模組內部直接呼叫其他模組的Dispatch方法(寫操作、變更其他模組的state)
- 不允許在一個模組內部直接讀取其他模組的state方法(讀操作)
我們建議將跨模組通訊的邏輯程式碼放在父模組中,或者在一個叫做Mediator層中單獨維護。
最終得到我們團隊完整的業務邏輯架構圖:
資料流管理
剛剛從空間維度講了架構管理的方案,現在從時間維度說說應用的資料流轉 --- Redux單向資料流。
Redux架構的設計核心是單向資料流,應用中所有的資料都應該遵循相同的生命週期,確保應用狀態的可預測性。
1. Action
- 使用者操作行為:click drag input ...
- 服務端返回資料後續的行為
2. Reducer
每個Action都會對應一個資料處理函式,即Reducer。特別強調,Reducer必須是純函式(pure function),這個規定帶來一個非常大的好處,資料處理層程式碼變的非常容易寫單元測試。
純函式的特徵是入參相同的情況下,返回值恆等,舉個例子?:
純函式:
function add(a, b) {
return a + b;
}
複製程式碼
非純函式:
function now() {
let now = new Date();
return now;
}
複製程式碼
函式中如果包含 Math.random
,new Date()
, 非同步請求等內容,且影響到最終結果的返回,即為非純函式。
3. Store
Store 資料存放的地方,store儲存從進入頁面開始所有Action操作生成的資料狀態(state),每次Action引發的資料變更都必須生成一個新的state物件,且確保舊的state物件不被修改。這樣做可以保證 應用的狀態的可預測、可追溯,也方便設計Redo/Undo功能。
我們團隊使用輕量級的immutable方案immutability-helper
,相比完全拷貝一份(deep clone)效能更優、儲存空間利用率更高。
immutability-helper
的API不夠友好,我們寫了一個庫immutability-helper-x增強它的易用性。
immutability-helper API風格:
import update from 'immutability-helper';
const newData = update(myData, {
x: {
y: {
z: { $set: 7 }
}
},
});
複製程式碼
immutability-helper-x API風格:
import update from 'immutability-helper-x';
const newData = update.$set(myData, 'x.y.z', 7);
複製程式碼
4. 統一渲染檢視
React/Redux是一種典型的資料驅動的開發框架(Data-Driven-Development),在開發中,我們可以將更多的精力集中在資料(領域模型+狀態模型)的操作和流轉上,再也不用被各種繁瑣的DOM操作程式碼困擾,當Store變更時,React/Redux框架會幫助我們自動的統一渲染檢視。
監聽Store變更重新整理檢視的功能是由react-redux完成的:
- <Provider> 元件通過
context
屬性向後代<connect>元件提供(provide)store物件; - <connect> 是一個高階元件,作用是將store與view層元件連線起來(這裡重複提一句,redux官方將<connect>直接連線的元件定義為container component),<connect>向開發者開放了幾個回撥函式鉤子(mapStateToProps, mapDispatchToProps...)用於自定義注入container component的props的姿勢;
- react-redux監聽redux store的變更,store改變後通知每一個connect元件重新整理自己和後代元件,為了減少不必要的重新整理提升效能,connect實現了shouldComponentUpdate方法,如果props不變的話,不重新整理connect包裹的container component;
總結
嚴格遵循架構規範和單向資料流規範,可以保證我們的前端應用在比較粗的粒度上的可維護性和擴充套件性,對於更細的粒度的程式碼,我們組織童鞋學習和分享《設計模式》 和 《重構 - 改善既有程式碼的設計》,持續打磨和優化自己的程式碼,未來團隊會持續輸出這方面的系列文章。
本篇先聊前端通用架構,具體模組的業務架構、架構遵循的原則、團隊架構組的架構評審流程等內容會在接下來的系列文章中闡述。感興趣的同學關注專欄或者傳送簡歷至 'tao.qit####alibaba-inc.com'.replace('####', '@'),歡迎有志之士加入~