AIGC标识 Dart 之包与 pubspec.yaml

mind-map

前言

组装书架时,我们会直接使用螺丝刀和卷尺,很少为了拧一颗螺丝先做一把工具。写程序也是一样:处理文件路径、格式化日期、编写测试,这些常见功能已经有人做好,可以直接放进自己的项目使用。

在 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 导入包中的库
        ↓
代码调用库提供的名称

十四、测试题

  1. 包、库和文件分别是什么?一个包能否包含多个库?
  2. 只写 import 而不在 pubspec.yaml 中声明依赖,为什么可能找不到包?
  3. dependencies 和 dev_dependencies 的判断标准是什么?path 和 test 分别应该放在哪里?
  4. 直接依赖和传递依赖有什么区别?传递依赖由谁处理?
  5. ^1.9.0 允许哪些版本?为什么不自动接受 2.0.0?
  6. pubspec.yaml 和 pubspec.lock 分别记录什么?应用项目为什么通常提交锁文件?
  7. package:weather_app/weather_label.dart 中,weather_app 和 weather_label.dart 分别来自哪里?
  8. pub.dev、Git 和本地 path 依赖分别适合什么场景?
  9. dart pub add、dart pub get、dart pub upgrade 和 dart pub outdated 分别解决什么问题?
  10. lib/src/ 为什么通常不作为外部代码直接依赖的入口?它与下划线私有规则是否相同?
posted @ 2026-10-15 08:47  好的佩奇  阅读(6)  评论(0)    收藏  举报