Dart 之包与 pubspec.yaml

前言
组装书架时,我们会直接使用螺丝刀和卷尺,很少为了拧一颗螺丝先做一把工具。写程序也是一样:处理文件路径、格式化日期、编写测试,这些常见功能已经有人做好,可以直接放进自己的项目使用。
在 Dart 中,这些可以重复使用的代码通常装在包里。path 包负责处理文件路径,intl 包可以格式化日期和数字,test 包提供测试功能。使用它们,既能少写重复代码,也能避免自己重新处理许多容易出错的细节。
那么,Dart 里的“包”到底是什么?它只是一个装着代码的文件夹吗?
不过,把工具带回家并不等于马上就能使用。项目需要知道工具叫什么、从哪里取得、哪些型号能够配合;真正动手时,还要从工具箱里拿出需要的那一件。Dart 用 pubspec.yaml 记录项目需要哪些包,用 pub 获取合适的版本,再用 import 选择包中的具体库。
因此,代码中已经写了 import,仍然可能提示找不到 package。只有弄清包、库和文件的关系,才能明白 pubspec.yaml、dart pub get 和 package: 导入路径各自完成了哪一步。
一、包是什么
包是一个带有 pubspec.yaml、能够由 pub 管理的项目目录。
pub 是 Dart 的包管理工具。它会读取 pubspec.yaml,知道这个包叫什么、支持哪个 Dart SDK、还需要哪些其他包。
一个包的核心不是“里面有几个 Dart 文件”,而是它有一份 pub 能读取的说明,知道自己怎样被使用、还需要什么。Dart 对包的说明见 How to use packages。
一个包不只放 Dart 代码,还可以包含可执行程序、测试、示例、图片和说明文档:
weather_app/
├── pubspec.yaml
├── lib/
├── bin/
├── test/
└── example/
可以把包想成一个工具箱。工具箱有自己的名称和清单,里面可以放多件工具,也可以放工具的测试和使用示例。
1.1 文件、库和包不是一回事
文件是保存代码的载体,库是组织 Dart 声明的单位,包是 pub 管理的项目目录。
例如:
weather_app/
├── pubspec.yaml
└── lib/
├── weather_label.dart
└── temperature.dart
weather_label.dart 和 temperature.dart 是两个文件。普通 Dart 文件通常也是一个库,所以这里可以有两个库。整个 weather_app 目录是一个包。
一个包可以包含多个库。import 使用的是某个库,pubspec.yaml 描述的是整个包。
二、pubspec.yaml 是什么
pubspec.yaml 是包的说明文件,位于包的顶层目录。
它像工具箱外面的清单:工具箱叫什么、适合什么环境、还需要配合哪些工具,都写在清单中。
name: weather_app
version: 1.0.0
description: A small weather application.
environment:
sdk: ^3.0.0
dependencies:
path: ^1.9.0
dev_dependencies:
test: ^1.25.0
这些字段分别表示:
| 字段 | 作用 |
|---|---|
| name | 包名,也是 package: 导入路径中的包名 |
| version | 当前包的版本 |
| description | 对这个包的简短说明 |
| environment | 这个包接受的 Dart SDK 范围 |
| dependencies | 程序运行时需要的依赖 |
| dev_dependencies | 开发、检查和测试时需要的依赖 |
所有包都需要 name。environment 用来防止项目在不支持的 SDK 上运行。发布到 pub.dev 的包还需要 version 和 description。
pubspec.yaml 使用 YAML 格式,缩进表示所属关系:
dependencies:
path: ^1.9.0
path 前面的两个空格表示它属于 dependencies。缩进错误时,即使 Dart 代码没有问题,pub 也无法正确读取配置。
homepage、repository、executables 和 publish_to 也是 pubspec.yaml 支持的字段,分别用于主页、代码仓库、命令行程序和发布位置。它们用于特定场景,不影响包依赖的基本过程。
三、项目怎样使用一个包
假设程序要拼接 data 和 today.json。直接写斜杠可能受到操作系统路径规则的影响,path 包已经处理了这些差异。
让项目使用 path 包,需要完成三件事。
3.1 声明依赖
在 pubspec.yaml 中声明 path:
name: weather_app
environment:
sdk: ^3.0.0
dependencies:
path: ^1.9.0
这一步告诉 pub:“weather_app 运行时需要 path,并且接受符合 ^1.9.0 的版本。”此时只是写好了需求,依赖还没有准备完成。
3.2 获取依赖
在 pubspec.yaml 所在目录运行:
dart pub get
pub 会读取清单,检查 SDK 和版本要求,选择一组能够共同工作的依赖,并生成项目需要的包配置。
这像按照购物清单采购。把商品写在清单上并不等于商品已经到家,dart pub get 才执行采购。
3.3 导入并使用库
依赖准备好以后,Dart 文件仍要说明自己使用 path 包中的哪个库:
import 'package:path/path.dart' as p;
void main() {
final filePath = p.join('data', 'today.json');
print(filePath);
}
在 Windows 上,这段代码会得到类似 data\today.json 的路径;在 Linux 或 macOS 上,会使用对应系统的路径分隔符。程序只调用 p.join,不需要自己判断系统差异。
pubspec.yaml 和 import 各有自己的职责:
- pubspec.yaml 说明整个项目依赖 path 包。
- import 说明当前 Dart 库使用 path 包中的 path.dart 库。
只写 pubspec.yaml,当前文件不会自动得到 path.dart 中的名称;只写 import,项目又没有声明并获取 path,Dart 也找不到它。
四、package: 路径表示什么
package: URI 是 Dart 定位包中某个库的地址。
它的结构是:
package:包名/lib 目录下的库路径
例如当前包的 pubspec.yaml 写着:
name: weather_app
包中有这个文件:
weather_app/
└── lib/
└── weather_label.dart
导入方式是:
import 'package:weather_app/weather_label.dart';
weather_app 来自 pubspec.yaml 中的 name,weather_label.dart 对应 lib/weather_label.dart。路径从 lib 内部开始写,所以不包含 lib/。
package: URI 像一个快递地址。包名先确定要去哪个仓库,后面的路径再确定从仓库里的哪个位置取库。它不是 C:\project\lib 这样的电脑磁盘路径。
导入第三方 path 包时:
import 'package:path/path.dart' as p;
第一个 path 是包名,path.dart 是这个包在 lib 目录中提供的库,p 是当前文件为这个库取的前缀。
五、dependencies 和 dev_dependencies
dependencies 保存程序运行时需要的依赖,dev_dependencies 保存只在开发过程中需要的依赖。
判断依据不是一个包重不重要,而是程序交付后是否仍然需要它。
餐厅营业时需要食材,也需要清洁工具。食材会直接参与顾客拿到的餐点,清洁工具帮助餐厅完成准备工作,但顾客吃饭时不需要它。dependencies 像运行时必须使用的食材,dev_dependencies 像开发时使用的辅助工具。
path 参与正式程序的路径处理,所以放在 dependencies:
dependencies:
path: ^1.9.0
test 用来执行测试,正式程序运行时不需要它,所以放在 dev_dependencies:
dev_dependencies:
test: ^1.25.0
如果正式程序中的代码 import 了某个包,这个包通常应该位于 dependencies。格式化、代码生成、静态检查和测试工具只参与开发过程时,才放在 dev_dependencies。
这个判断也适用于自己编写的包:包对外提供的功能依赖哪些包,就把它们放在 dependencies;只帮助作者检查和测试代码的工具,就放在 dev_dependencies。相关字段的定义见 pubspec 文件说明。
如果正式代码使用了只写在 dev_dependencies 中的包,依赖就放错了位置。dev_dependencies 中的工具不能代替运行依赖。
5.1 直接依赖和传递依赖
项目主动声明并使用的包叫直接依赖,依赖包继续需要的其他包叫传递依赖。
假设 weather_app 需要 package_a,而 package_a 又需要 package_b:
weather_app → package_a → package_b
package_a 是 weather_app 的直接依赖,package_b 是 weather_app 的传递依赖。weather_app 只声明自己直接使用的 package_a,pub 会根据 package_a 的配置找到 package_b。
项目只需要声明自己直接使用的包,其他包需要什么由 pub 根据依赖关系继续处理。
六、版本约束决定哪些版本可以使用
版本约束是 pubspec.yaml 中允许使用的版本范围。
dependencies:
path: ^1.9.0
^1.9.0 表示版本不能低于 1.9.0,也不能达到 2.0.0。1.9.1 和 1.10.0 在范围内,2.0.0 不在范围内。
它也可以写成:
dependencies:
path: '>=1.9.0 <2.0.0'
版本约束像采购时写下的规格范围。范围规定哪些版本可以接受,并不表示所有版本都会安装。pub 会在允许范围内选择一个能够和其他依赖共同工作的版本。
主版本发生变化时,包可能带来不兼容修改。^1.9.0 不自动接受 2.0.0,可以避免项目在没有准备的情况下进入新的主版本。
对于 1.0.0 以下的版本,兼容范围会更窄。例如 ^0.2.3 小于 0.3.0。不能把所有 ^ 约束都理解为“只要小于下一个整数版本”。
版本约束的具体规则由 pub 的版本解析机制决定,不能只把 ^ 当成普通数学符号。遇到特殊版本范围时,应以 Pub 依赖说明 中的规则为准。
七、pubspec.lock 为什么存在
pubspec.lock 是依赖锁定文件,记录 pub 实际选中的版本。
pubspec.yaml 中的 ^1.9.0 是允许范围,pubspec.lock 可能记录这次实际选中了 1.9.1。两者分别解决“可以选什么”和“这次选了什么”。
可以把 pubspec.yaml 看成采购要求,把 pubspec.lock 看成采购完成后的明细。要求允许多个型号,明细只记录真正买到的型号。
应用项目通常提交 pubspec.lock。团队成员和部署环境拿到同一份锁文件,再运行 dart pub get,就会尽量使用相同的依赖版本。
供其他项目使用的普通可复用包通常不提交 pubspec.lock。它需要在使用者项目中,根据双方的版本约束重新寻找能够共同工作的依赖。
不要手动修改 pubspec.lock 来更新依赖。依赖要求写在 pubspec.yaml 中,锁文件由 pub 命令生成。
如果应用项目没有提交 pubspec.lock,不同开发者可能在同一个版本范围内得到不同的具体版本。更新依赖时,应修改 pubspec.yaml,再运行 pub upgrade 或 pub get 生成新的锁文件。
八、.dart_tool 做什么
.dart_tool/ 是 Dart 工具为当前项目生成的配置和缓存目录。
运行 dart pub get 后,其中的 package_config.json 会记录包名和实际位置之间的对应关系。代码导入 package:path/path.dart 时,Dart 就通过这份配置找到 path 包。
.dart_tool/ 可以理解为仓库管理系统的内部索引。使用者通过商品名称取东西,系统靠索引找到实际货架。这个目录能够重新生成,通常不提交到版本控制中。
九、依赖可以来自哪里
依赖来源决定 pub 到哪里获取一个包。
常见来源有 pub.dev、Git 仓库和本地目录。它们像公共商店、开发中的工作室和自己电脑上的另一个工具箱。
9.1 pub.dev
只写版本约束时,pub 默认从 pub.dev 获取:
dependencies:
path: ^1.9.0
这是使用公开 Dart 包最常见的方式。
9.2 Git 仓库
没有发布到 pub.dev 的代码可以来自 Git 仓库:
dependencies:
shared_tools:
git:
url: https://github.com/example/shared_tools.git
Git 仓库可能继续变化,团队还需要确认分支、标签或提交。准备发布到 pub.dev 的包不能把 Git 包作为发布依赖。
9.3 本地目录
同时开发两个本地包时,可以让一个包直接依赖另一个目录:
dependencies:
shared_tools:
path: ../shared_tools
路径相对于当前 pubspec.yaml 所在目录。shared_tools 的代码变化后,当前项目可以直接使用新代码。
这个路径只在拥有相同目录结构的电脑上有效,所以本地 path 依赖适合本地联合开发,不适合作为公开包的最终依赖来源。
9.4 dependency_overrides
dependency_overrides 可以临时强制整个依赖关系使用指定版本或来源,常用于本地调试冲突:
dependency_overrides:
shared_tools:
path: ../shared_tools
它会覆盖正常解析结果,也可能隐藏真实的兼容问题,因此适合临时验证,不代替正常的 dependencies 配置。
十、常用的 pub 命令
10.1 dart pub add
dart pub add 把依赖写进 pubspec.yaml,并立即获取依赖。
添加运行依赖:
dart pub add path
添加开发依赖:
dart pub add dev:test
它相当于把商品加入清单,并立即执行一次采购。
10.2 dart pub get
dart pub get 根据 pubspec.yaml 和已有锁文件准备依赖。
dart pub get
它会尽量保留 pubspec.lock 中已经选定且仍然符合要求的版本。刚拉取项目、修改 pubspec.yaml 或切换分支后,通常需要运行这个命令。
成功运行后,项目会有或更新 pubspec.lock 和 .dart_tool/package_config.json。前者记住版本,后者记住包名对应的实际位置。
10.3 dart pub upgrade
dart pub upgrade 在版本约束允许的范围内重新选择较新的依赖。
dart pub upgrade
它会忽略已有锁定结果并重新生成 pubspec.lock。get 负责按照当前要求准备项目,upgrade 表示主动更新依赖。更新后的代码仍需要经过测试。
只更新一个包时,可以写:
dart pub upgrade path
10.4 dart pub outdated
dart pub outdated 显示依赖当前使用的版本和可更新情况。
dart pub outdated
有新版本不等于当前项目一定能直接升级。SDK 范围、其他依赖和 pubspec.yaml 中的版本约束,都可能限制最终可用的版本。
十一、包中的目录分别做什么
一个常见的包可能有这些目录:
weather_app/
├── pubspec.yaml
├── lib/
│ ├── weather_app.dart
│ └── src/
│ └── forecast_parser.dart
├── bin/
│ └── main.dart
├── test/
│ └── weather_app_test.dart
├── example/
│ └── main.dart
├── pubspec.lock
└── .dart_tool/
| 目录或文件 | 用途 |
|---|---|
| lib/ | 保存包能够提供给其他代码使用的库 |
| lib/src/ | 保存包内部实现 |
| bin/ | 保存可执行程序入口 |
| test/ | 保存自动化测试 |
| example/ | 保存使用示例 |
| pubspec.yaml | 描述包和依赖要求 |
| pubspec.lock | 记录实际选中的依赖版本 |
| .dart_tool/ | 保存工具生成的项目配置 |
11.1 为什么不直接导入 lib/src
lib/src/ 通常保存包作者可以调整的内部实现。公开入口可以在 lib/weather_app.dart 中导出需要提供的功能:
export 'src/forecast_parser.dart';
使用者导入公开入口:
import 'package:weather_app/weather_app.dart';
直接导入内部文件有时也能运行:
import 'package:weather_app/src/forecast_parser.dart';
但这样会依赖包作者的内部文件布局。作者改名、移动或合并文件时,外部代码就可能失效。
lib/ 像商店柜台,lib/src/ 像仓库。顾客从柜台取得商品,商店可以调整仓库布局,而不影响柜台提供的商品。
src/ 是包的目录约定,不是 Dart 的私有语法。Dart 中以下划线开头的名称仍然遵守库级可见性规则。
十二、常见问题为什么会发生
12.1 已经写了 import,仍然找不到 package
import 只说明当前库想使用什么。项目还必须在 pubspec.yaml 中声明依赖,并运行 dart pub get。命令也要在能够找到 pubspec.yaml 的包目录中执行。
12.2 包能找到,库却找不到
package: 后面的库路径从 lib 目录内部开始计算。lib/weather_label.dart 对应 weather_label.dart,不是 lib/weather_label.dart。文件不在 lib 中时,也不能通过普通的 package: URI 对外导入。
例如写成下面这样就多写了一级 lib:
import 'package:weather_app/lib/weather_label.dart'; // 错误
12.3 包名和项目文件夹名称不同
package: URI 使用 pubspec.yaml 中的 name。修改文件夹名称不会自动修改包名,修改包名也需要同步修改 import。
12.4 依赖版本无法共同工作
两个依赖可能要求互不相容的版本范围,当前 Dart SDK 也可能不满足某个包的 environment。dart pub outdated 可以显示更新状态,错误信息会指出无法满足的约束。
十三、总结
包是带有 pubspec.yaml、能够由 pub 管理的项目目录。一个包可以包含多个库、可执行程序、测试、示例和资源。
pubspec.yaml 描述包和依赖要求,dart pub get 根据这些要求准备依赖,pubspec.lock 记录实际选中的版本,.dart_tool/ 保存工具生成的包位置配置。
dependencies 保存运行依赖,dev_dependencies 保存开发依赖。版本约束规定允许使用哪些版本,依赖来源规定从哪里获取包。
package: URI 定位包中的库。包名来自 pubspec.yaml 中的 name,后面的库路径从 lib 目录内部开始计算。
整个过程可以归纳为一条关系:
pubspec.yaml 声明包
↓
dart pub get 准备包
↓
import 导入包中的库
↓
代码调用库提供的名称
十四、测试题
- 包、库和文件分别是什么?一个包能否包含多个库?
- 只写 import 而不在 pubspec.yaml 中声明依赖,为什么可能找不到包?
- dependencies 和 dev_dependencies 的判断标准是什么?path 和 test 分别应该放在哪里?
- 直接依赖和传递依赖有什么区别?传递依赖由谁处理?
- ^1.9.0 允许哪些版本?为什么不自动接受 2.0.0?
- pubspec.yaml 和 pubspec.lock 分别记录什么?应用项目为什么通常提交锁文件?
- package:weather_app/weather_label.dart 中,weather_app 和 weather_label.dart 分别来自哪里?
- pub.dev、Git 和本地 path 依赖分别适合什么场景?
- dart pub add、dart pub get、dart pub upgrade 和 dart pub outdated 分别解决什么问题?
- lib/src/ 为什么通常不作为外部代码直接依赖的入口?它与下划线私有规则是否相同?
浙公网安备 33010602011771号