開篇
上一篇《iOS 混編 模組化/元件化 經驗指北》中介紹到的 Lotusoot ,將在本篇中做一個更為詳細的介紹。
最初 Lotusoot 簡稱為『混編路由』,但是隨後反而曲解了它的功能,其真正的定位是『模組化工具和規範』。
Lotusoot 可以做到:
- 模組間、模組內服務呼叫
- Swift、OC、或者兩者混編專案均可使用
- 短鏈註冊、路由呼叫
- 指令碼自動註冊服務/路由表
注:這裡的模組化,也就是大家說的『元件化』,不是在主工程用資料夾分模組,而是指將獨立模組抽調成 CocoaPods 庫、或者其他形式的庫檔案,成為一個獨立工程。 下文中的模組就代表一個 CocoaPods 庫
模組化解耦——路由 or 服務呼叫?
關於模組化,大多數人的第一反應是製作路由、註冊短鏈、呼叫短鏈,通過這樣的方式來去耦,來實現模組間的頁面跳轉、服務呼叫。類似 MGJRouter 一類的庫就是基於這樣的思想。
但我也非常認可 casa 在反駁使用 URL 作為模組化核心的理由。即:『短鏈的實質還是通過 URL 來呼叫服務或者開啟頁面,反而不如字串直接,反而增加了 URL 本身的維護成本』。
所以,我們應當迴歸模組化的本質。我們模組化的最初的目的往往是為了:
- 程式碼拆分,將關聯性強的基礎服務程式碼或者業務程式碼抽調在一起,單獨封版,獨立開發
- 防止主工程越來越大,變得臃腫
相對應的,模組化需要的功能是:
- 提供多個庫之間的服務呼叫
- 保持庫與庫之間的獨立、非強依賴
所以,總的來說,模組化的重點還是如何去除多個模組之間的耦合,讓每個模組在不強依賴的情況下可以呼叫其他模組的服務。
**URL 短鏈、甚至是路由、都不是模組化重點之處。**路由只要你想,都可以通過服務註冊實現。
注:『不強依賴』指的是,模組A 呼叫 模組B 不需要在 Pod 依賴中寫出對 B 的依賴,或者簡單的認為,模組A 的程式碼中不出現
import B
。
公共模組和依賴關係
Lotusoot 是這樣解耦的:
1. Lotus
建立一個 PublicModule,其中存放各個模組的 Lotus,Lotus 其實就是協議,定義了每個模組可以提供的服務(即可以呼叫的方法),舉例如下:
public protocol AccountLotus {
func login(username: String, password: String, complete: (Error?) -> Void)
func register(username: String, password: String, complete: (Error?) -> Void)
func email(username: String) -> String
func showLoginVC(username: String, password: String)
}
複製程式碼
2. Lotusoot
在各個模組中,實現 PublicModule 中對應的 Lotus,即具體的服務類,稱為 Lotusoot。
Lotusoot 中具體實現了服務的邏輯,並在 註解 中表明瞭模組的 名稱空間-@NameSpace
、Lotusoot-@Lotusoot
、Lotus-@Lotus
。舉例如下:
// @NameSpace(TestAccountModule)
// @Lotusoot(AccountLotusoot)
// @Lotus(AccountLotus)
class AccountLotusoot: NSObject, AccountLotus {
func email(username: String) -> String {
return OtherService.email(username: "zhoulingyu")
}
func login(username: String, password: String, complete: (Error?) -> Void) {
LoginService.login(username: username, password: password, complete: complete)
}
func register(username: String, password: String, complete: (Error?) -> Void) {
RegisterService.register(username: username, password: password, complete: complete)
}
func showLoginVC(username: String, password: String) {
// 可以用你們喜歡的非耦合方式處理跳轉
// 或者傳入 rootvc
// 更好的方式是自己的非耦合 UI 跳轉處理模組
print("show login view controller")
}
}
複製程式碼
註解是非必須的,註解是為了 Lotusoot.py
可以掃描 Lotusoot 自動註冊,後面一節將會說到。如果你不想使用自動註冊,也可以選擇手動註冊。
注:這裡做一點解釋,『協議-服務類』即『Lotus-Lotusoot』的命名由來純屬賣個萌,因為,協議是暴露外部的,所以叫蓮花,而具體實現的服務類自然就是蓮藕(Loutsoot)了。
3. 自動註冊 or 手動註冊
如果使用了註解,可以自動註冊所有服務,只需要:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
LotusootCoordinator.registerAll()
}
複製程式碼
如果先手動註冊,如下所示:
[LotusootCoordinator registerWithLotusoot:[AppDelegateLotusoot new] lotusName:@"AppDelegateLotus"];
[LotusootCoordinator registerWithLotusoot:[MainLotusoot new] lotusName:@"MainLotus"];
複製程式碼
不過手動註冊就是去了使用 Lotusoot 的意義了,所以在無法滿足條件時再使用手動註冊(比如目前 0.0.2 版本的 Lotusoot,主工程如果有多 Target,是無法動態獲取 Target 名,導致無法正確獲取名稱空間,反射到類,當然除工程,各個模組由於使用 CocoaPos 不存在這個問題。下一個版本 Lotusoot 將會重點解決這個問題)
4. 關係圖
通過 Lotusoot 搭建的工程如下圖所示:
[圖片上傳失敗...(image-442260-1513091120014)]
所有模組只需要依賴 PublicModule,通過 PublicMoudle 下的 Lotusoot 即可呼叫其他模組的服務。例項程式碼如下:
let loginLotus = s(LoginLotus.self)
let loginModule: LoginLotus = LotusootCoordinator.lotusoot(lotus: loginLotus) as! LoginLotus
loginModule.login(username: "test", password: "test", complete:nil)
複製程式碼
OC 中使用:
id<LoginLotus> loginModule = [LotusootCoordinator lotusootWithLotus:@"LoginLotus"];
[loginModule login:@"test" password:@"test" complete:nil];
複製程式碼
Lotusoot 是如何實現自動註冊服務的?在什麼時機?
這個問題也是在編寫 Lotusoot 最初重點考慮的問題。原因是 Swift 沒有 +(void)load
。
大概是每個用 Swift 開發模組路由和解耦工具的人都糾結過的問題。
1. OC 中常見的解決方案
先說說通常 OC 的路由或者解耦,都是在 +(void)load
註冊類服務的,大致的做法類似於:
+ (void)load {
@autoreleasepool {
[[Router shared] map:@"LoginViewController" toController:[self class]];
}
}
複製程式碼
或者
+ (void)load {
@autoreleasepool {
[[ServiceManager shared] register:@"LoginService" toService:[self class]];
}
}
複製程式碼
這樣,即使是在每個模組內,也可以正常註冊自己的服務。而主工程和其他模組呼叫只需要通過字串呼叫即可。
2. Swift 中的痛點
由於 Swift 是沒有 +(void)load
的,也沒有其他可靠的方法可以替代,那麼勢必需要在主工程中加入註冊路由這一步驟,通常可以放在 didFinishLaunchingWithOptions
,因為主模組可以調到所有模組的類。那麼隨之而來的問題就是,你可能會出現這樣常常的程式碼:
ServiceManager().register("LoginService", toService:LoginService.self)
ServiceManager().register("UserCenterService", toService:UserCenterService.self)
ServiceManager().register("HistoryService", toService:HistoryService.self)
...
複製程式碼
可能會有一張長長的列表,而且由於服務都分散在各個模組,但是卻集中在主模組,往往難以看到路由表和服務類的關聯,表徵不夠明顯、關係不夠強烈。
3. Lotusoot 的解決方案
有什麼更好的辦法註冊?mmoaay 給了我一個灰常棒的建議,參考 R.swift 的做法,通過指令碼,來完成自動註冊。
R.swift 的提供的功能是,可以讓使用的 iOSer 可以像開發 Android 的一樣呼叫圖片、字串、音訊等等資原始檔。在 Project 中插入 Run Script,這個指令碼可以在編譯階段掃描整個工程,列算所有的資原始檔,最後生成一個
R.generated.swift
檔案,就像這個樣子:[圖片上傳失敗...(image-4c4f17-1513091120014)]
使用的時候就可以:
[圖片上傳失敗...(image-75f168-1513091120014)]
同樣 Lotusoot 通過一個 python 指令碼,在『Compile Source』之前掃描工程目錄下的檔案,找出 Lotusoot 和 Lotus 對應關係,並生成一個 Lotusoot.plist
檔案:
[圖片上傳失敗...(image-91bed0-1513091120014)]
如何識別 Lotus?
目前,Lotusoot 使用了很 Low 的方式,在註解中表明瞭模組的 名稱空間-@NameSpace
、Lotusoot-@Lotusoot
、Lotus-@Lotus
,指令碼就可以識別。舉例如下:
[圖片上傳失敗...(image-e94313-1513091120014)]
所以,didFinishLaunchingWithOptions
只需要一句,即可自動註冊路由。
LotusootCoordinator.registerAll()
複製程式碼
為什麼使用指令碼解決?是因為到目前為止,所有的解耦工具或者路由都是通過程式設計師手動去寫程式碼新增路由,不管是在 + (void)load
中註冊也好,在程式啟動後註冊也好,都是有程式設計師手動管理的。使用指令碼是希望在編譯階段前,就準備好所有的『協議-服務類』對應關係表,在程式啟動後通過這張表自動註冊,實現程式設計師不手動註冊、完全無感。我覺得這才是真正的解耦工具應當具備的功能。
Lotusoot 的重大缺點和下一版本目標
Lotusoot 的缺點是顯而易見的,雖然通過指令碼可以在編譯階段建立好『協議-服務類』關係表。但 Lotusoot.py
識別『協議-服務類』是通過註解來的,而這裡的註解其實就是註釋,不能編譯檢測錯誤,及時誤寫錯也無法及時檢查出問題。如果解決了這一痛點,就可以相對完美的解決了 Swift 的模組化方案。
目前的思路如下:
嘗試通過全域性方法或是其他語法方式實現真正的註解,可以像 Java 中的註解一樣,不僅可以作為一種標識,也可以進行編譯檢查。
其實 OC 中是可以直接用巨集定義做的:
#define Service(_name_) \
+ (void)load { \
[self registerService:_name_]; \
}
// 使用
@Service(@"LoginService")
@implementation LoginService
...
@end
複製程式碼
但 Lotusoot 是提供給 Swift 和 OC 以及混編專案都可以使用的,所以實現方案還需要我繼續探索。
另一種方式可以使用 LLVM 是提供了 @annotation 操作的,如果通過這種方式生成 .plist
的註冊列表檔案應該會放到編譯結束時。
以上,是以後的一些構思,希望可以完美的解決 Swift 模組化方案。如果你有什麼好的建議,都可以來找我討論哦~~~
Demo 和 Github
如果想更清晰的感受 Lotusoot 帶來的模組化改造,請必須下載 Demo 來看喲。
總專案的地址在這裡。
非常歡迎一起討論(賣萌~~)
有什麼問題都可以在博文後面留言,或者微博上私信我,或者郵件我 coderfish@163.com。
博主是 iOS 妹子一枚。
希望大家一起進步。
我的微博:小魚周凌宇