14-2.【模块化与包管理】SPM 中的 target、product、dependencies 有什么区别?如何合理规划多模块依赖?

1 阅读3分钟

在 Swift Package Manager (SPM) 中,理解 targetproductdependencies 的区别是构建模块化项目的基石。简单来说,它们分别代表了代码的构建单元输出的交付物以及外部的支撑力


1. 核心概念辨析

Target:构建的“原子”单位

Target 是 SPM 的最小构建单元。它定义了一组源代码文件以及它们如何被编译。

  • 代码隔离:每个 Target 都有自己的命名空间。
  • 类型:可以是 target(应用代码)、executableTarget(命令行工具)或 testTarget(单元测试)。
  • 内部依赖:Target 之间可以相互依赖。

Product:对外暴露的“门面”

Product 是 Package 对外提供的输出产物。它是 Target 的逻辑组合,决定了其他 Package 能看到什么。

  • library:静态或动态库,供其他项目链接。
  • executable:可执行二进制文件。
  • 可见性控制:一个 Package 可能有 10 个 Target,但如果 Product 只包含其中 1 个,那么外部调用者只能看到这 1 个。

Dependencies:外部的“源泉”

Dependencies 定义了当前 Package 运行所需的外部资源。

  • Package 级别依赖:指向 Git 仓库地址或本地路径。
  • Target 级别依赖:指定某个具体的 Target 需要用到哪个 Package 里的哪个 Product。

2. 三者的关系模型

你可以把 SPM 想象成一个工厂:

  1. Target 是工厂里的生产线,负责加工零件。
  2. Product 是工厂展厅里的最终商品,由一条或多条生产线组装而成。
  3. Dependencies 是工厂的原材料供应商

3. 多模块依赖的规划策略

当项目规模扩大时,合理的规划能避免编译时间过长和依赖地狱。

A. 遵循“功能分层”原则

将项目划分为四个核心层级,避免循环依赖:

层级职责依赖限制
App/Feature 层业务逻辑、UI 界面依赖 Domain 和 Utility
Domain 层业务模型、协议(Interface)仅依赖 Utility
Utility 层网络库、扩展工具、存储不依赖业务逻辑
Core/Third-party 层外部库、二进制 target处于依赖链最底层

B. 接口与实现分离 (Interface Segregation)

为了提高编译速度,可以采用“接口包”模式:

  • 创建一个 FeatureInterface Target,仅包含协议和数据模型。
  • 创建一个 FeatureImplementation Target,包含具体的逻辑。
  • 其他模块只依赖 Interface,这样当 Implementation 的内部代码修改时,不会触发其他模块的重新编译。

C. 慎用动态库 (Dynamic vs Static)

在定义 product 时:

  • .library(name: "MyLib", targets: ["MyLib"]) 默认通常是静态链接
  • 如果你的多模块项目中,多个动态库同时依赖同一个静态库,会导致代码冗余和符号冲突
  • 建议:除非有特殊需求(如插件化或减小包体积),否则让 SPM 自动决定链接方式。

4. 实战:Package.swift 配置示例

Swift

let package = Package(
    name: "MyModularApp",
    products: [
        // 外部可见的模块
        .library(name: "CoreUI", targets: ["CoreUI"]),
        .library(name: "NetworkLayer", targets: ["NetworkLayer"])
    ],
    dependencies: [
        // 外部依赖
        .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.0.0")
    ],
    targets: [
        // 基础工具层
        .target(name: "NetworkLayer", dependencies: ["Alamofire"]),
        // UI 组件层
        .target(name: "CoreUI", dependencies: []),
        // 业务层:依赖内部的两个模块
        .target(name: "HomeFeature", dependencies: ["CoreUI", "NetworkLayer"]),
        // 测试层
        .testTarget(name: "HomeFeatureTests", dependencies: ["HomeFeature"])
    ]
)

5. 规划避坑指南

  1. 避免循环依赖:如果 A 依赖 B,B 依赖 C,C 又依赖 A,SPM 会直接报错。此时应提取公共部分到 Core 模块。
  2. 隐藏内部 Target:不要把所有的 Target 都包装进 Product。只暴露必要的接口,保持 Package 的整洁。
  3. 合理使用 Binary Target:对于闭源库或极其庞大的第三方库,使用 binaryTarget 可以极大地提升编译速度。