App 模組化實戰經驗總結
隨著業務的不斷髮展壯大,App 端所承擔的功能也越來越重,特別是程式碼幾易其主之後開始變得雜亂無章,牽一髮而動全域性的事情時常發生。為了應對團隊壯大之後的開發模式,我們必須要對業務進行隔離,同時沉澱出通用元件,完善移動開發的基礎設施。
1. 痛點
模組化之前,我們主要面臨以下痛點:
業務邊界不清晰
通用程式碼與業務程式碼耦合
程式碼、資原始檔大量重複
常量滿天飛
其中業務邊界不清晰是最大的痛點,最直接的表現就是處處有雷,經常會引入新的 Bug,而且很多 Bug 往往不能從根本上解決,程式碼維護成本居高不下。
2. 重構原則
模組化並不能一蹴而就,我們在重構的同時也在做新需求,每次看到那一坨舊程式碼心中就會有無數只”草泥馬”奔騰而過,乾脆重寫的無奈之情難以抑制,結果在紅牛的日夜陪伴下寫出來的新程式碼雖然看上去“漂亮”,但是實際上問題更多,得不償失。吃過幾次苦頭之後,我們總結出了重構的三項基本原則:
2.1 漸進式重構
如果一段程式碼已經比較穩定,可以從中抽取一部分功能重寫,不要一上來就全部推翻重寫,可以慢慢淘汰掉老程式碼。
2.2 iOS / Android 互相參考
業務程式碼總是驚人的相似,兩端互相參考的過程中,不但可以 Review 程式碼,還能加深對業務的理解,可謂一舉兩得。
實踐證明,如果人手緊張,專案早期可以只讓一端的開發人員跟需求,另一端直接“翻譯程式碼”,甚至一個人寫兩端程式碼。
2.3 理清業務再動手
App 作為業務鏈的末端,由於角色所限,開發人員對業務的理解比後端要淺,所謂欲速則不達,重構不能急,理清楚業務邏輯之後再動手。(可以找熟悉業務的同學聊一下 — PD、後端、測試)
3 模組化過程
所謂模組化,是一個分而治之的過程,概念類似於 SOA,首先進行垂直拆分,過程中必然會催生出業務共享的 Common 模組,而 Common 又可以繼續水平拆分,逐漸變薄,直到 Common 消失。
剛開始不需要完美的目標,簡單粗暴一點,後續再逐漸改善。
3.1 抽取 Common
Common 層服務於所有的上層業務,是通用層,不允許引用業務層程式碼。
首先把 Common 層用到的 Business 層程式碼下放到各個業務
然後把多個 Business 之間共用的程式碼提取到 Common 層
資原始檔的處理方式與程式碼一致
Common 層作為權宜之計,它的命運是向死而生,最終會誕生出許多功能獨立的基礎模組。而這個過程是漫長的,我們只能在業務隔離的同時,不斷豐富 Common 模組,然後在某個節點將其再拆分成一個一個獨立模組。
程式碼也逃不出分久必合、合久必分的的宿命。
3.2 業務隔離
業務模組之間不能互相依賴,只能單向依賴 common。
業務之間存在兩種耦合關係:
頁面耦合
功能耦合
要做到徹底隔離就必須打破這兩種耦合關係:
頁面解耦 - 跳轉協議
功能解耦 - 模組間 RPC
3.2.1 統一跳轉協議
頁面解耦可以借鑑 Web 的設計原理,給業務模組中對外的頁面定義一個 URI,然後頁面之間通過 URI 跳轉。
舉個栗子,A、B 兩個頁面分屬於不同的業務模組,在頁面未解耦之前,A 如果要跳轉到 B,必須要依賴 B 的模組,那麼跳轉程式碼會寫成如下形式:
Android
1.
Intent intent =
new
Intent(getContext(), BbbActivity.
class
);
2.
intent.putParcelable(BbbActivity.EXTRA_MESSAGE, message);
3.
startActivity(intent);
iOS
1.
BbbViewController *bbbVC = [[BbbViewController alloc] init];
2.
bbbVC.messageModel = messageModel;
3.
[self.navigationController pushViewController:bbbVC animated:YES];
如果 A、B 之間還需要傳遞資料,就要共享常量、Model,耦合繼續加重。
如果我們為 B 頁面定義一個 URI - wsc://home/bbb
,然後把共享的 messageModel
拍平序列化成 Json 串,那麼 A 只需要拼裝一個符合 B 頁面 scheme 的跳轉協議就可以了。
1.
wsc:
//home/bbb?message={ "name":"John", "age":31, "city":"New York" }
URL Router
有很多種實現方式,網上資料也是多如牛毛,這裡只提供一種思路。
Android 實現方式
1. 在 AndroidManifest.xml 檔案中定義 URI
01.
<activity
02.
android:name=
".ui.BbbActivity"
03.
<intent-filter>
04.
<category android:name=
"android.intent.category.DEFAULT"
/>
05.
<action android:name=
"android.intent.action.VIEW"
/>
06.
<data
07.
android:host=
"bbb"
08.
android:path=
"/home"
09.
android:scheme=
"wsc"
/>
10.
</intent-filter>
11.
</activity>
2. 封裝跳轉 Intent
1.
final
Uri uri =
new
Uri.Builder().authority(
"wsc"
).path(
"home/bbb"
)
2.
.appendQueryParameter(
"message"
,
new
Gson().toJson(messageModel)).build();
3.
final
Intent intent =
new
Intent(Intent.ACTION_VIEW);
4.
intent.setData(uri);
5.
startActivity(intent);
3. 步驟 2 程式碼進一步封裝
1.
ZanURLRouter.from(getContext())
2.
.withAction(Intent.ACTION_VIEW)
3.
.withUri(
"wsc://home/bbb"
)
4.
.withParcelableExtra(
"message"
, messageModel)
5.
.navigate();
iOS實現方式
1. 通過 plist 檔案儲存 URI 到 Controller class 的對映
2. 封裝一個根據 URI 跳轉到 Controller 的 SDK
3. 頁面跳轉
1.
[ZanURLRouter routeURL:@
"wsc://home/bbb"
];
注意事項
兩端協議要保持一致
需要通過工程手段保證頁面 URI 唯一
3.2.2 模組間 RPC
「業務 A 」與「Remote: 服務端」之間通過 HTTP 或者其他協議進行遠端呼叫,「Remote: 服務端」是服務提供者,「業務 A 」是服務消費者。
對於「業務 A 」來說,「Local: 業務 B」也是服務提供者,但是兩者不存在依賴關係,所以只能通過協議來通訊。
iOS 通過
protocol
提供服務,利用 BeeHive 做“服務治理”。Android 通過
interface
提供服務,然後我們模仿 Retrofit 做了一個“服務治理”框架 - ServiceRouter,它的優勢在於可以只在業務提供方的 module 中定義interface
,解耦更徹底。
4 程式碼管理
如果被隔離的業務模組仍然在一個 Project 中,就無法從“物理”上徹底隔絕程式碼間的相互引用,我們需要從工程上保證業務之間互相獨立。
4.1 程式碼結構
Android (Module) | iOS (Project) |
---|---|
4.2 獨立發版
每一個 subproject 可以獨立發版,然後通過座標依賴組裝成 App,以 Android 為例:
4.3 獨立 Repo
現在還沒有找到一個很好的程式碼組織形式,所以我們的觀點是:
在團隊規模不大的時候,一個人要 Cover 多個子工程,所以沒有必要獨立 Repo,當一個 Repo 需要多個人 Cover 時可以考慮獨立 Repo。
規模 | 是否獨立 Repo |
---|---|
Developer 1 : N projects | 否 |
Project 1 : N developers | 是 |
當解耦方案確定之後,模組化其實就是一個體力活,返工重做便成了家常便飯,所以我們覺得比較好的方式應該是專人負責、一氣呵成。
5 詩和遠方
通過移動配置中心動態下發跳轉協議
抽取移動端業務通用 UI 元件庫
主工程可選擇性依賴業務模組