我报名参加金石计划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
└── ...
实操步骤:
- 主仓库新建 modules 目录,用以存放组件/模块源码,目录 monorepo-demo 存放壳工程源码
- 壳工程 Podfile 文件以本地依赖方式依赖各个模块
- 齐活了,就是那么简单
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 方式管理,提供二进制库依赖即可
至此,我们似乎已经解决了所有已知痛点,也许接下来还会冒出新的痛点,不过请相信「办法总比困难多」。
小结
一点感悟分享给大家:
- 没有最好的方案,只有当下最适合自己的方案
- 选方案不为追逐「潮流」,而是追逐「收益」
- 各方案的思想并非互相对立,相互结合往往有奇效
- 方案选型要为后期调整留足空间
- 要坚信「办法总比困难多」