[周小鱼]& iOS 混编 模块化/组件化 经验指北
NSString *className = [NSString stringWithFormat:@"%@.`AuthLoginManager", [NSString targetName]];
|
id delegate = [[UIApplication sharedApplication] delegate];
|
4.2 处理模块耦合代码-协议调用
保持第一遍中充满 NSClassFromString 是不可取的,因为这类代码往往属于硬编码,不能在类名出现改动、或者方法名出现改动的时候及时在编译阶段抛出 error。
在这里引出一段讨论。
之前跟大神们讨论组件化(模块化)的具体实践时候,说到了主流的组件化可能都借用了 + (void)load 方法和 rumtime 操作来注册路由和服务。这时候 casa 大神提出了一种说法『组件化的根本目的是隔离、隔离问题影响域、隔离业务、隔离开发时的依赖。所以让两个本来有关系的人变得没有关系,就需要一个中间人,如果不用 runtime 能省掉不少事,但是用 URL 是一件相对来说比较多余的事,一个包含了 target-action 的字符串就足够了,URL 是字符串的更复杂表征,target-action 的意义体现的更明显。同时 URL 应该仅限于 H5 调度和跨 App 的 URL Scheme 调度』。
这里要向 casa 大神非常非常郑重的道歉,上面一段,原来在第一版的时候是预留修改的片段,本想再读一遍大神 《 [iOS应用架构谈 组件化方案]》 仔细理解以后再次修改这块,本来是悄咪咪的发了文章,没想到被推送出去了,有引导大家曲解大神的愿意。非常非常抱歉!现在已经修改。
下面在贴上大佬自己对 URL 的见解:
那个时候听了 casa 大神的说法觉得『哎?有道理』,但是在后期的实践中,我觉得就我个人的代码习惯,是希望尽可能的将问题暴露在编译阶段,能让它抛出 error 就抛出 error,纵使使用字符串可以定义常量,但由于大家不是独立负责项目,在其他人看到你的方法参数时,比如:+ (void)callService:(NSString *)sUrl 或者 + (void)openURL:(NSString *)url ,对方发现你的参数是 NSStrring,很有可能直接出现硬编码字符串而不去查阅常量列表,这是习惯性编码很容易出现的问题。但我对 casa 『URL 没有 target-action 表征明显』是非常仍可的,所以 Lotusoot 的重点只在于解耦的服务调用,URL 只是为了更好的为 H5 页面提供外部调用服务,在工程内部大可使用更加简洁的方式。
最后一点原因是,反射或者通过类/方法字符串字典的方式实在太 OC 了,不管怎么样我们是一个尽量 Swift 化的项目,应该尽量吸取其优点,虽然抽出的 OC 库可以使用反射,那 Swift 库咋办?目前 Swift3 与 4 都没有很好的支持反射。
所以,第二遍处理使用协议替换反射是很有必要的。但实质上,处理的并不是很好。大致如下(我们以 LPDBLoginModule 为例):
4.2.1 在 LPDBLoginModule 整理用到的服务,归类整理
如我们的 LPDBLoginModule 用到了 AppDelegate 中的一些方法,同事用到了 AuthLogin 相关类中的一些方法
4.2.2 在 LPDBLoginModule 中建立相应的协议
即建立 AuthLoginDelegate.h 和 AppDelegateProtocol
大致的代码如下:
@protocol AppDelegateProtocol <NSObject>
|
@protocol AuthLoginDelegate <NSObject>[Pods](media/Pods.)
|
4.2.3 在主工程中去实现协议
AppDelegateProtocol 由 AppDelegate 扩展实现:
@import LPDBLoginModule;
|
AuthLoginDelegate 由 AuthLoginManager(这个 Manager 在主工程中是 swift 编写的) 实现:
extension AuthLoginManager: AuthLoginDelegate {
|
4.2.4 在 LPDBLoginModule 调用服务
id delegate = [[UIApplication sharedApplication] delegate];
|
NSString *className = [NSString stringWithFormat:@"%@.AuthLoginManager", [NSString targetName]];
|
经过这些改造之后,模块间的状态如图所示:
但是,可以很明显感受到,这次的改变并不彻底:
- 还是存在大量的
![delegate conformsToProtocol:@protocol(AppDelegateProtocol)]这样的判断,仅仅是起到了容错,保证不会 crash,但是却不能将问题暴露在编译阶段。 AppDelegateProtocol明明是一个公共的,多个模块使用的协议,却被定义到了LPDBLoginModule- 概念颠倒,理想状态下,应该是各个子模块提供协议和实现,告知其他模块可以调用该模块哪些功能。而目前是子模块告知其他模块需要调用哪些方法,由其他模块实现。
那么为了彻底解决问题,我们引入了 Lotusoot —— 组件通信和工具。
4.3 处理模块耦合代码-Lotusoot
Lotusoot 的最初目的就是为了解决模块间的耦合,并且同时支持 OC 和 Swift 使用,也是这几个月中去做的一个比较重要的东西,库本身小巧灵活,包含的东西也很少,但是起到的规范作用却是我非常满意的一点。
Lotusoot 规范的核心思想主要是以下几步,我们同样使用上面的 LPDBLoginModule 为例:
4.3.1 建立共用模块——LPDBPublicModule
LPDBPublicModule中定义了各个模块可以提供的服务,做成协议,称为 Lotus,一个 Lotus 协议包含了一个模块的所有的能调用的方法的列表。举例如下:
|
|
4.3.2 各个模块中,实现 LPDBPublicModule 中对应的 Lotus 协议
实现协议的 Class 称为 Lotusoot。举例如下:
class AppDelegateLotusoot: NSObject, AppDelegateLotus {
|
class MainLotusoot: NSObject, MainLotus {
|
4.3.3 注册服务
需要着重说明的是,这一步是可以省略的,通过 Lotusoot 提供的脚本和注解,可以自动为所有的路由进行注册。请移步 Lotusoot参考『3. 注解与规范』部分。
didFinishLaunchingWithOptions 中注册服务:
[LotusootCoordinator registerWithLotusoot:[AppDelegateLotusoot new] lotusName:@"AppDelegateLotus"];
|
4.3.3 在其他模块中调用服务
现在只需要 import Lotusoot、import ModulePublic
id<MainLotus> mainModule = [LotusootCoordinator lotusootWithLotus:@"MainLotus"];
|
// 如果使用字符串 @"AppDelegateLotus" 注册,建议定义在 LPDBPublicModule
|
无论 OC 还是 Swift,都可以顺畅调用
// 或者使用类似字符串 "AccountLotus",但需要你管理好 kAccountLotus,尽量不要硬编码
|
let mainLotus = s(MainLotus.self)
|
到此为止,就比较完整的解决了模块间耦合。清爽的风格用一张图表示就是这样(这是我在做 Lotusoot 解说时候用的一张配图):
LPDBPublicModule 中的 Lotus 协议,像一张清单列出了所有模块提供的服务声明,而在各个模块中,直接通过这些公共协议就可以调用想要的服务。很多问题都可以在编译前和编译阶段显示出来(如果模块不提供服务,是不能通过编译的;如果没有一项服务没有声明,是不能通过编译的)。
4.4 语言耦合
我们抽模块中一个重要的目的就是『分割两种语言』,但是实践过程中,会发现,分割语言比分割业务还要难。
一个 Pod 库中只能包含一种语言,但往往,在抽离代码的最后,会发现有无数的基础 Model 耦合,如:
@interface ShopInfo : LPDBModel
|
class DeliveryService: BaseModel {
|
如果需要将 ShopInfo 和 DeliveryService 抽出到一个模块时,必须要『有舍有得』,在涉及到基础 Model 语言不同时,可以适当的重写,因为 Model 的代码量是极小的,Model 通常也只包含属性声明,作为数据传输的中介,即使更改,产生的不可预支错误的可能性也较低。
如果要抽出的模块主体使用 OC,那么可以将 DeliveryService 重新用 OC 编写。
但要注意,要先尽量通过拆分更基础的服务模块,在考虑重新编写文件,保证项目的稳定性。
4.5 模块的积木化
模块化的最终目的,不仅仅是去耦,还应当让每个模块像积木一样,随意拼接,最后达到主工程完全没有代码,通过 Pod 集成各个模块,组成完整的功能。而每个模块也应当可以独立测试,独立开发。
还是以 LPDBLoginModule 和 LPDBNetWort 为例。
登录模块是一个非常特殊的模块,所有的子模块如果想独立测试和开发,一般都需要通过登录验证,比如订单模块,必须要登录后,该业务模块内能才能正确的拉取订单信息。
由于 LPDBLoginModule 依赖基础库 LPDBNetWort,LPDBNetWort 需要做的有:
- 包含 cer 文件,可以正确的提供给其他模块正常的 https 接口访问
- 便利的网络服务调用
而 LPDBLoginModule 至少要做的事有:
- 可以正确的保存登录信息,完成登录操作
- 提供登录的 UI 界面,可以直接调用 LoginVC
在具备以上功能后,LPDBLoginModule 就可以快速的集成进其他模块,为其他模块提供独立开发、独立测试的功能。
4.6 资源打包
上一小结提到『 LPDBLoginModule 要提供登录的 UI 界面』。对于 UI 界面,需要做的是资源打包,在模块拆分中,要非常注意资源分割。
因为业务模块的划分,不仅仅是是代码抽出,也有资源抽出。
资源库包括但不仅限于:
.xib文件- 声音资源
- 图片资源
- 纯文本文件
- 视频资源
所以,所有的资源文件,应当单独创立 Res 文件夹,放入其中,并在 .podspec 中表明资源文件路径
s.resources = ["Source/**/*.xib", "Source/Res/*.xcassets"]
|
注意图片资源,如果想保留 @2x、@3x,是可以按照 xcassets 的格式直接 copy 过来的。
5 结尾
以上是我在混编项目中进行 模块化/ 组件化的经验总结,写成了指导的模式,希望这篇文章能对走同样路的人有所帮助,希望你们会有所收获,么么哒。
有什么问题都可以在博文后面留言,或者微博上私信我,或者邮件我 coderfish@163.com。
博主是 iOS 妹子一枚。
希望大家一起进步。
我的微博:小鱼周凌宇
T










浙公网安备 33010602011771号