iOS 灵活的 模块化/组件化 工具与规范 Lotusoot 解说
开篇
上一篇《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. 关系图
所有模块只需要依赖 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 文件。
同样 Lotusoot 通过一个 python 脚本,在『Compile Source』之前扫描工程目录下的文件,找出 Lotusoot 和 Lotus 对应关系,并生成一个 Lotusoot.plist 文件:
如何识别 Lotus?
目前,Lotusoot 使用了很 Low 的方式,在注解中表明了模块的 命名空间-@NameSpace、Lotusoot-@Lotusoot、Lotus-@Lotus,脚本就可以识别。
所以,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 妹子一枚。
希望大家一起进步。
我的微博:小鱼周凌宇
- 苹果自曝全新Mac Pro:模块化
- 佛系90后活的比谁都精彩
- 近 8 万首手写诗词,都藏在这个古风 App 里 - 郢雪 #iOS
- iPhone X/8去升级:iOS 11新更新修复摄像头大Bug
- 这艘矿砂船是个“灵活的胖子”
- 苹果发布iOS 11.2.1更新:修复安全漏洞,要求所有人必须升级!
- 厉害了美少年!黄浦区今年的“幸福小使者”出炉!
- 漏洞太要命!iOS 11.2.1更新:必升
- 房子是不是已经限制你对生活的想象力?
- 模块化飞机来了,这才是避免空难的最黑科技!