使用 Swift 的 Monorepo:实践中的代码组织
在现代 iOS 和 macOS 开发中,选择代码管理策略是影响可扩展性、交付速度和产品质量的关键因素之一。传统上,开发者使用多仓库(polyrepo),每个模块或服务都存在于独立的 Git 仓库中。然而,随着项目和团队的增长,越来越多的问题浮现:如何组织代码,使其保持完整性、可复用且易于维护?答案是——使用 Swift 的 Monorepo。在本文中,我们将探讨什么是 monorepo,为什么它适合 Swift 项目,以及如何使用 Swift Package Manager (SPM) 在实践中进行组织。
什么是 Monorepo,为什么在 Swift 中需要它?
Monorepo 是一种将所有项目代码(或多个相关项目)存储在一个仓库中的方法。与每个组件都隔离的 polyrepo 不同,monorepo 将库、应用程序、测试、配置和文档统一在单一版本控制下。
对于 Swift 开发者来说,monorepo 尤其重要,原因如下:
- 统一的依赖版本: 避免了不同应用程序之间库版本不同步的问题。
- 简化重构: 对公共模块的更改会立即对仓库内的所有使用者可见。
- 快速集成: 新功能可以在单个 pull request 中进行测试和发布。
- 与 SPM 兼容: Swift Package Manager 非常适合 monorepo,因为它支持本地包和相对路径。
Swift 项目的 Monorepo 结构
让我们看一个实际例子。假设我们有一个 iOS 应用程序(使用 SwiftUI)和一个 macOS 应用程序(使用 AppKit),它们共享公共模块:网络层、认证逻辑和 UI 组件。我们按如下方式组织 monorepo:
MyMonorepo/├── Package.swift├── Sources/│ ├── AppiOS/│ │ └── main.swift│ ├── AppMacOS/│ │ └── main.swift│ ├── CoreNetworking/│ │ ├── Sources/│ │ │ └── HTTPClient.swift│ │ └── Tests/│ ├── CoreAuth/│ │ ├── Sources/│ │ │ └── AuthService.swift│ │ └── Tests/│ └── UIComponents/│ ├── Sources/│ │ └── CustomButton.swift│ └── Tests/├── Tests/│ ├── AppiOSTests/│ └── AppMacOSTests/├── .gitignore└── README.md关键文件是 Package.swift。它描述了所有模块及其依赖关系。该文件的示例如下:
// swift-tools-version:5.9import PackageDescription
let package = Package( name: "MyMonorepo", platforms: [ .iOS(.v17), .macOS(.v14) ], products: [ .executable(name: "AppiOS", targets: ["AppiOS"]), .executable(name: "AppMacOS", targets: ["AppMacOS"]), .library(name: "CoreNetworking", targets: ["CoreNetworking"]), .library(name: "CoreAuth", targets: ["CoreAuth"]), .library(name: "UIComponents", targets: ["UIComponents"]) ], dependencies: [ // 外部依赖(如果需要) // .package(url: "https://github.com/...", from: "1.0.0") ], targets: [ .executableTarget( name: "AppiOS", dependencies: ["CoreNetworking", "CoreAuth", "UIComponents"], path: "Sources/AppiOS" ), .executableTarget( name: "AppMacOS", dependencies: ["CoreNetworking", "CoreAuth", "UIComponents"], path: "Sources/AppMacOS" ), .target( name: "CoreNetworking", dependencies: [], path: "Sources/CoreNetworking/Sources" ), .target( name: "CoreAuth",