30 分钟完成 iOS monorepo 化改造 | iOS 组件化复盘

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

背景

代码解耦和复用是一个非常重要的问题。早期我们会以合理划分目录,提取公共组件的方式解决问题。

随着项目增大,项目编译速度变慢(调试成本变大),同时有了代码共享的需求,组件化开发是一个不错的选择,既能单个模块独立开发调试,又能多项目共享代码,看上去十分不错,研发效率提升也很明显(特别是调 UI 样式的时候)。

模块越来越多,涉及多模块同时改动的可能性急剧增加,终于在一次大规模重构中你吃到了组件化开发的第一个苦。怎么在壳工程同时修改多个模块并提交到对应仓库?怎么发 MR 让队友 Review 你的代码?各模块版本怎么管理?等一系列问题让人头大。本文带你深刻复盘 iOS 组件化之路。

阅读本文你将了解:

  • 什么是 monorepo
  • 为什么要 monorepo
  • iOS 怎么实现 monorepo
  • monorepo 最佳实践

单体应用阶段

项目起步阶段,团队规模非常小,此时适合「单体应用」。不过切记,一定要合理划分目录,提取公共组件,尽可能做到不同功能/业务间解耦。为将来低成本迁移到组件化方案留足空间。

此时你的目录结构大概是这样的:

└── monorepo-demo
  ├── monorepo-demo
  │   ├── AppDelegate.swift
  │   ├── Assets.xcassets
  │   ...
  │   ├── SceneDelegate.swift
  │   └── ViewController.swift
  └── monorepo-demo.xcodeproj
      ├── project.pbxproj
      ...

优点

  • 代码管理成本低
  • 不需要额外的学习成本
  • Merge Request 更友好

缺点

  • 代码质量容易失控
  • 项目规模太大时,开发调试效率开始显著降低(构建时间比较长,调一个 UI 问题要等全量构建完才能看到效果)
  • 跨项目复用代码受到局限(不是不能跨项目复用代码)
  • git 无法根据目录划分访问权限,仓库内全部代码开放给每个团队成员

组件化 + multirepo(多仓多模块)阶段

团队规模变大,人员分工开始明确,此时单体应用的缺点愈发突出,此时 multirepo 更适合我们。multirepo 可以等效理解为把部分组件或模块当做三方库开发和使用,这样可以使人员分工更明确,队员只需关心自己模块所在的仓库,并在自己的模块内进行开发和调试,甚至可以独立提测单个模块。

此时你的目录结构大概是这样的:

├── Podfile // 新增,用以配置依赖
├── Podfile.lock // 新增,用来锁定依赖版本,使团队内依赖的三方库版本一致,强烈建议纳入 git 版本管理
├── Pods // 新增
│   ├── ModuleA // 我们依赖的二方库
│   │   ├── LICENSE
│   │   ├── README.md
│   │   └── Source
│   │       ├── ModulA.swift
│   │   ...
│   ├── Headers
│   ├── Local Podspecs
│   ├── Manifest.lock
│   ├── Pods.xcodeproj
│   │   ├── project.pbxproj
│   │   ...
// 原有部分
├── monorepo-demo
│   ├── AppDelegate.swift
│   ...
│   ├── Info.plist
│   ├── SceneDelegate.swift
│   └── ViewController.swift
├── monorepo-demo.xcodeproj
│   ...
└── monorepo-demo.xcworkspace // 新增,后面我们要在 Xcode 中打开该文件进行开发而不再是 monorepo-demo.xcodeproj
  └── ...

组件化开发的意义

  • 便于代码复用
  • 模块独立开发、调试
  • 人员分工更明确
  • 源代码访问权限设置更灵活
  • 强迫程序员降低代码耦合

需要注意的点

  • 模块力度划分不太容易把握
    • 划分模块原则:基础组件、功能组件和业务模块
    • 不要划得太细,组件数量激增会增大维护成本
  • 各模块分支管理(比较影响开发体验)
    • 问题:业务迭代往往需要同时修改多个模块,此时被修改的模块需要各自准备一个分支,并且需要一一对应
    • 方案:被修改的模块版本与壳工程版本保持一致,拉出同名分支
    • 仍然存在的问题:1. 为了模块版本统一,未改动的模块也要拉出同名分支,是多余的操作
    • 分支管理需要借助脚本完成
  • 同时涉及多个模块的重构时需要在壳工程同时修改多个模块,并提交到各自仓库。
    • 方案:使用 git submodule 实现本地依赖,以 git flow 方式管理分支,提供配套脚本完成分支管理任务。
    • 仍然存在的问题:1. 脚本执行效率比较低,且会偶现失败
  • 同时涉及多个模块修改的 MR(Merge Request)需要在各自模块仓库发出,显得比较分散。

组件化 + monorepo(单仓多模块)阶段

monorepo(单仓多模块/项目),即把多个模块/项目源码放在同一个代码仓库管理,乍一听这并不是一个好的想法,甚至有点扯,但这确实能解决不少问题,提升了开发体验,真的让效率提升了不少。

随着组件/模块越来越多, multirepo 维护成本越来越大,意外情况也变得更频繁了(打包机打包失败更频繁了),于是我们意识到我们的方案是时候改进了。

此时你的目录结构大概是这样的:

.
├── modules // 新增目录
│   ├── ModuleA // A 模块源码,该模块具备独立开发调试能力
│   │   ├── Example
│   │   │   ├── ModuleA
│   │   │   │   ├── AppDelegate.swift
│   │   │   │   ...
│   │   │   │   └── ViewController.swift
│...
│   ├── ModuleB // B 模块源码,该模块具备独立开发调试能力
│   │   ├── Example
│   │   │   ├── ModuleB
│   │   │   │   ├── AppDelegate.swift
│   │   │   │   ...
│   │   │   │   └── ViewController.swift
│...
│...
└── monorepo-demo
  ├── Podfile
  ├── Podfile.lock
  ├── Pods
  │   ├── Alamofire
│...
  │   └── Target Support Files
  │       ├── Alamofire
  │       │   ├── ...
  │       ├── ModuleA
  │       │   ├── ...
  │       ├── ModuleB
  │       │   ├── ...
│...
  ├── monorepo-demo
  │   ├── AppDelegate.swift
  │   ├── ...
  │   ├── SceneDelegate.swift
  │   └── ViewController.swift
  ├── monorepo-demo.xcodeproj
  │   ...
  └── monorepo-demo.xcworkspace
      └── ...

实操步骤

  1. 主仓库新建 modules 目录,用以存放组件/模块源码,目录 monorepo-demo 存放壳工程源码
  2. 壳工程 Podfile 文件以本地依赖方式依赖各个模块
  3. 齐活了,就是那么简单

Podfile 示例

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
​
target 'monorepo-demo' do
 # Comment the next line if you don't want to use dynamic frameworks
 use_frameworks!
 pod 'Alamofire'
​
 # 以本地依赖方式依赖各二方库
 pod 'ModuleA', :path => '../modules/moduleA'
 pod 'ModuleB', :path => '../modules/moduleB'
 pod 'ModuleC', :path => '../modules/moduleC'
 # Pods for monorepo-demo
​
end
​

monorepo 的优势

  • 所有源码在同一个仓库,分支管理与单体应用一样简单
  • MR 在同一个仓库发出,队友阅读起来很顺畅
  • 保留 multirepo 的主要优势
    • 便于代码复用(单个组件/模块依然可以单独发布版本,跨应用共享代码与 mutirepo 无异)
    • 模块独立开发、调试
    • 人员分工更明确
    • 强迫程序员降低代码耦合

monorepo 的不足

  • git 无法根据目录划分访问权限,仓库内全部代码开放给每个团队成员
  • 代码规模大到一定程度时 git 操作速度到达瓶颈,影响 git 操作体验(这个中小规模团队不用考虑)。

提示:如果你要从 multirepo 方式迁移到 monorepo ,则只需把各模块仓库 clone 到 modules 目录下,并切换到最新代码所在分支,然后删掉各模块目录下的 .git 文件,并按照「实操步骤」完成迁移即可。

组件化 + monorepo(单仓多模块) + multirepo(多仓多模块)

我们各自的司情不同,再加上每种方案都有自己的局限和不足,为了能够满足我们现实的需求,我们继续探索。既然我们清楚的预见了 monorepo 的缺陷,那么解决它其实也并不难。

首先我们要知道 monorepo 与 multirepo 不是二选一的关系,而是主次的关系,既然是主次的关系,那么就是可以共存的。相信嘴角上扬的你已经悟了。

monorepo 优化方案

  • 稳定且独立的模块使用 multirepo 方式管理,像三方库一样
  • 非常驻业务(临时性的需求)可以使用 multirepo 方式管理,接下来某个版本就可以干掉,也不会污染 monorepo 仓库(比如一个拉新的活动,只用几天,那么过了这几天,代码自然就没有存在的意义了)
  • 需要较高权限的模块可以使用 multirepo 方式管理,提供二进制库依赖即可

至此,我们似乎已经解决了所有已知痛点,也许接下来还会冒出新的痛点,不过请相信「办法总比困难多」。

小结

一点感悟分享给大家

  1. 没有最好的方案,只有当下最适合自己的方案
  2. 选方案不为追逐「潮流」,而是追逐「收益」
  3. 各方案的思想并非互相对立,相互结合往往有奇效
  4. 方案选型要为后期调整留足空间
  5. 要坚信「办法总比困难多」

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYFHvBkh' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片