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 妹子一枚。

    希望大家一起进步。

    我的微博:小鱼周凌宇

    iOS 灵活的 模块化/组件化 工具与规范 Lotusoot 解说