iOS 模块化

iOS 模块化方案记录

背景

前东家是一家很老的(始于1997年)服务提供商,同时和大多数软件公司一样,很多服务需要额外购买,同时升级软件版本也要掏一大笔钱。这么长时间导致后台服务有多个版本,且每个客户能访问的功能五花八门,数据也是无比的杂乱。这就给客户端带来了很大的挑战,之前的解决方案是写一个包含最大公共功能的 App,再以此为基础为每一个客户定制单独的 App。但这样的 App 通常是一次性的,除非有重大的 bug,否则不会更新。终于公司下定决心重构,重构有几个目标:

  1. 要将所有 App 整合为一个 App,便于维护和升级
  2. 因为是重构,要原有功能的完整和准确性,因此单元测试覆盖率要达到 70% 以上
  3. 因为客户遍及全球,因为 RTL 、弱视、盲人模式都要有
  4. 因为业务需求,需要支持功能复杂的深度链接
  5. 由于后端服务和数据复杂,为了保证客户端(Android、iOS、WindowsPhone)数据一致性,所以用 C++ 做了统一的网络请求中间层

基于以上要求,能想到的最好解决方案自然就是模块化:

  1. 基于路由 URL 对模块 UI 页面跳转管理
  2. 基于协议对数据的请求、缓存和分享管理
  3. 维护通用 UI 库保证满足 RTL、弱视、盲人模式的支持

从结构上,整个 App 被划分为 5 层

app structure

跨平台公共层

这一层使用 C++ 包装了多种版本和多种服务端 URL 的 Api,保证了各个客户端 Api 请求的一致性

公共组件库

  • 公共数据结构 framework:C++ 层返回的请求结果都可以转换为公共的数据结构,同时也是以公共数据结构定义的结构体进行缓存;各个业务模块的数据结构都是用公共数据结构组合起来的
  • 数据管理 framework:管理与 C++ 层的请求,将请求数据转换为客户端定义的公共结构、数据缓存等
  • 通用 UI framework:通用 UI 库维护了 App 的基础库,同时保证基础组件满足 RTL、弱视下的大字体布局、盲人模式下的阅读、各尺寸屏幕适配等

app common

模块管理层

  • 模块接口协议 ComponentProtocol:定义了模块的启动规则、生命周期、参数、数据源等,用于模块的启动
  • 模块数据协议 DataProviderProtocol: 是一个空协议,各个子模块可以扩展协议方法。然后通过实现了该协议的类获取数据
  • 路由管理 Router:管理模块之间的 UI 页面跳转、模块之间的参数传递、转场动画等

app component

模块接口协议 ComponentProtocol

模块接口协议大致定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public protocol ComponentProtocol: class {
/// Component's unqiue identifier
var uid: String! { get set }

/// Component's uri
var urlComponent: RouterURLComponent? { get set }

/// Router object
var router: Router? { get set }

/// Component instance's navigate mode
var navigateMode: RouterNavigateMode! { get set }

/// Component's main entry UIViewController
var viewController: UIViewController! { get set }

/// Component's data provider
var dataProvider: DataProvider! { get set }


/// componentWIllMount() is invoked immediately before a component is mounted into component hierarchy tree
/// At that moment, perform any necessarily initialize things, setup component's state. Just remember the componentWillMount is only called once during the component lifecycle.
/// Avoid introducing any UI rendering related things in this methods.
func componentWillMount()

/// componetWillUmount() is invoked before a component is umounted from the component hierarchy tree.
/// At that moment, perform any necessary cleanup for the component at this method, such as invalidating timers, cancelling network requests, cleanup used resources.
/// This method is only called once during the component's while lifecycle.
func componentWillUnmount()

/// componentWillActive() is invoked before a component is presented at top of the component hierarchy tree.
/// Use this as an opportunity to operate any things when the component is active for user
func componentWillActive()

/// componentWillInactive is invoked before a component is no more at the top of the component hierarchy tree.
/// There're two cases
/// 1) The component is removed from the hierarchy tree.
/// 2) Another component is pushed into the tree, the component is at the second top of the tree.
func componentWillInactive()
}

extension ComponentProtocol {
public func componentWillMount() {}
public func componentWillUnmount() {}
public func componentWillActive() {}
public func componentWillInactive() {}
}

每个模块 framework 暴露一个公共类实现模块接口,填入必要的参数例如 转场动画的模式、模块入口的 ViewController 等信息,Router 解析后会补齐进入模块的 URI、数据源 DataProvider 等信息,同时调用该类实现的生命周期方法

模块数据协议 DataProviderProtocol

模块数据协议 DataProviderProtocol 定义了当前模块获取数据的方法接口,在模块向外暴露的 Component 入口类中,规定 dataProvider 必须实现当前模块的数据协议方法,这样就能保证模块可以通过外部数据源获取数据

路由管理 Router

路由管理模块负责维护 URL 与模块之间的对应关系,当访问到对应的模块时:

  1. 初始化该模块的公共 Component 类,填充 Component 中的属性
  2. 调用对应的生命周期函数
  3. 拿到入口 ViewController,打开页面
  4. 调用 Component 的生命周期函数,这样就完成了一次模块间的跳转

Router 与 NavigationController 一一对应,因此在模块内部的 VC 间跳转也要通过 Router,否则在模块之间的跳转会打乱 VC 的栈
Router 会优先选择 URL 参数中的转场动画模式,如果没有设置再使用 Componet 类中定义的转场模式,这是为了 Deeplink 打开 App 时可以在用户无感知的情况下一次跳转多个页面

App 与模块的数据

App 通过 Router 控制模块的生命周期、UI跳转,然后通过 DataProvider 的实例控制与模块之间的数据交互

app dataprovider

DataProvider 会在模块解析注册完成时注入到 Component 中,从而保证模块可以通过 Component 中的 dataProvider 拿到对应的数据

DataProvider 实例遵守 DataProvider 协议,并实现了各个模块扩展协议中的方法,它负责将公共的数据结构汇总为各个模块所需的数据结构,管理各个模块的数据请求回调等

例如,每次打开一个模块会立即返回之前请求缓存的数据,保证 UI 可以立即渲染出来,然后 DataProvider 会保存回调,当最新的数据请求完成时,再通过回调返回新的数据

模块间通信与数据传输

  • 参数:在 componentWillActive 生命周期函数调用之后,模块中可以拿到进入模块对应的 URL,从而可以从 URL 中解析出传入的参数,此时可以根据参数初始化入口 ViewController
  • 数据:在 componentWillMount 生命周期函数调用之后,模块中可以拿到 dataProvider 对象,开启请求数据
  • 复杂数据传输:通常模块化设计的一大问题就是复杂数据如何传输,我们使用的是缓存机制,需要传输的数据通过协议接口传递给 dateProvider 对象临时持有,下一个模块再通过指定的 key 去取出

模块的测试

一个模块的完整测试包括功能数据的测试与 UI 测试:

  • 由于模块采用 MVVM 的设计模式,通过 Mock 数据很容易对 VM 进行数据和功能的单元测试
  • 而 UI 的测试也很简单,只要对当前 framework 创建一个单模块的 App,然后创建一个数据类实现模块的数据请求协议MockDataProvider<ComponentDataProviderProtocol>,在该类中 Mock 各种 UI 测试的边界数据。再在 App 中通过模块的公共 Component 类进入模块即可完整的模拟出打开模块的流程

app text

最终每个模块的项目结构如下:

app module

  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2017-2021 HonQi

请我喝杯咖啡吧~