跳到主要内容

Libraries & imports(库与导入)

Dart 使用库(library)组织代码和控制名称可见性,使用 import 引入其他库公开的 API。每个 Dart 文件本身都属于一个库,即使文件中没有写 library 指令;多数应用代码只需要合理拆分文件并正确导入,不必显式命名库。

学习 Dart 库与导入时,重点需要掌握以下问题:

  • dart:package: 和相对 URI 分别指向哪里?
  • 如何用前缀解决同名冲突?
  • showhideexport 分别控制什么?
  • 以下划线开头的名称为什么是库私有,而不是文件私有?
  • part 与普通 import 有什么区别?

导入一个库

import 指令接收一个 URI,并把目标库的公开名称引入当前库:

import 'dart:math';

void main() {
print(max(12, 8)); // 12
}

导入指令写在文件顶层,位于普通声明之前。它不会像函数调用一样在执行到某一行时才生效,而是在编译阶段建立库之间的依赖关系。

三类常用 URI

Dart 导入最常见的 URI 有三类。

Dart SDK 库:dart:

dart: URI 指向 Dart SDK 提供的库:

import 'dart:convert';

void main() {
final json = jsonEncode({'language': 'Dart'});
print(json); // {"language":"Dart"}
}

常见示例包括 dart:coredart:convertdart:mathdart:io。其中 dart:core 会被自动导入,使用 StringListprint() 等核心 API 时通常不需要显式导入。

SDK 库并非在所有平台都可用。例如 dart:io 面向能够访问文件、套接字和进程的平台,浏览器应用不能直接依赖它。选择 SDK 库时需要同时考虑目标平台。

包内或第三方库:package:

package: URI 从包的 lib/ 目录开始解析:

import 'package:study_tools/formatters.dart';

这里的 study_tools 是包名,后面的路径对应这个包 lib/ 下的文件。项目需要先在 pubspec.yaml 中声明并解析第三方依赖,才能导入该依赖公开的库。

包自己的 lib/ 目录也可以通过包名导入。跨越多个子目录或提供对外 API 时,package: URI 通常比层层向上的相对路径更稳定。

相邻文件:相对 URI

相对 URI 根据当前文件的位置解析,适合导入附近的实现文件:

import 'models/user.dart';
import '../shared/validator.dart';

相对 URI 只描述文件位置,不包含 Windows 或 Unix 的绝对文件系统路径。URI 中统一使用 /,不要写 C:\... 或反斜杠路径。

同一项目应遵循统一的导入风格,避免同一个库一处使用 package: URI、另一处使用相对 URI。混用可能让读者误判库边界,也可能触发项目启用的 lint 规则。

使用前缀解决名称冲突

两个库导出了同名声明时,可以使用 as 为导入添加前缀:

import 'dart:convert' as convert;
import 'dart:math' as math;

void main() {
print(convert.jsonEncode({'score': math.max(80, 95)}));
}

添加前缀后,需要通过 convert.jsonEncodemath.max 访问对应名称。前缀只在当前库中用于消除歧义,不会修改被导入库中的声明名称。

当前代码即使暂时没有冲突,也可以用前缀表达 API 来源;但不必给每个导入机械地添加前缀,应以可读性为准。

只导入需要的名称

show 只暴露列出的名称,hide 则排除列出的名称:

import 'dart:math' show max, min;

void main() {
print(max(3, 7)); // 7
print(min(3, 7)); // 3
}
import 'dart:math' hide Random;

void main() {
print(sqrt(81)); // 9.0
}

showhide 称为组合器(combinator),用于控制当前库能够通过这条导入访问哪些名称。它们不会修改原库,也不是为了减少最终程序体积而必须使用的优化手段。

组合器也可以和前缀一起使用:

import 'dart:math' as math show max;

void main() {
print(math.max(4, 9)); // 9
}

导出库的公开 API

export 让一个库把另一个库的公开名称重新暴露给自己的使用者。它常用于创建单一入口文件:

// lib/study_tools.dart
export 'src/formatter.dart';
export 'src/validator.dart' show validateEmail;

调用方只需要导入入口库:

import 'package:study_tools/study_tools.dart';

export 同样支持 showhide。入口库可以借此公开稳定 API,同时把 src/ 中的实现细节留在包内部。

importexport 的方向不同:import 决定当前库自己能使用什么,export 决定当前库的使用者还能看到什么。导出某个库并不会自动让导出方自己的代码可以直接使用其中名称;需要使用时仍应添加 import

库私有名称

Dart 中以下划线 _ 开头的标识符是库私有名称:

class UserRepository {
void save() {
_validate();
print('saved');
}

void _validate() {
print('validated');
}
}

其他库可以使用 UserRepositorysave(),但不能直接访问 _validate()。这里的私有边界是库,而不一定是单个文件。

普通的两个独立 Dart 文件通常各自形成库,因此一个文件不能访问另一个文件中的 _privateName。如果多个文件通过 part 组成同一个库,它们则共享这些私有名称。

TypeScript 对比:TypeScript 的 ES 模块通常通过 export 决定哪些文件级声明可被外部导入,还支持 private 类成员。Dart 的前导下划线直接表示库私有,私有边界可能包含由 part 连接的多个文件,没有与 TypeScript 文件模块完全相同的对应关系。

使用 part 拆分同一个库

part 可以把多个源文件组成同一个库。主文件声明各个部分:

// shapes.dart
part 'src/circle.dart';

String describeCircle(Circle circle) => 'radius: ${circle._radius}';

部分文件使用 part of 指回主库:

// src/circle.dart
part of '../shapes.dart';

class Circle {
Circle(this._radius);

final double _radius;
}

shapes.dartcircle.dart 属于同一个库,所以主文件可以访问 Circle 的私有字段 _radius。调用方只需导入主文件,不应把部分文件当作独立库使用。

对于普通业务模块,优先让每个文件成为独立库,并通过 import 组合公开接口。part 更适合代码生成或确实需要跨文件共享库私有实现的场景,因为它会让文件之间的耦合更紧。

显式 library 指令

文件可以使用 library 指令声明库:

library study_tools;

import 'dart:convert';

String encode(Object? value) => jsonEncode(value);

现代 Dart 代码通常可以省略这条指令;文件仍然会形成库。只有工具约定、生成代码或其他确切需求要求库声明时再添加,避免仅为了给文件起名而重复维护名称。

使用 part 时,部分文件优先通过 URI 写成 part of '../main_library.dart';,这样文件移动或重命名后的关系更容易由工具检查。旧代码也可能通过库名关联 part of,阅读时需要知道两种形式可能同时存在。

延迟导入

延迟导入使用 deferred as,并在首次使用前调用前缀的 loadLibrary()

import 'package:study_tools/report.dart' deferred as report;

Future<void> showReport() async {
await report.loadLibrary();
report.printReport();
}

它允许受支持的编译平台把某个库延后加载。具体平台是否支持、如何拆分产物以及加载失败如何处理,取决于 Dart 工具链和部署目标;使用前应核对当前目标平台的官方支持情况。

loadLibrary() 可以多次调用,库只会实际加载一次。调用完成前不能访问延迟库中的声明,因此加载逻辑通常需要显式处理等待状态和错误。

常见误区

误区一:把文件私有当成库私有

_name 的边界是库。独立文件通常各自是库,但 part 文件与主文件共享同一私有命名空间。

误区二:认为 export 同时完成了 import

导出只影响当前库的使用者。当前库自身要调用目标 API,仍需导入对应库。

误区三:用前缀访问未公开名称

前缀只能限定已经公开且未被 showhide 排除的名称。写成 package._privateName 也不能绕过库私有边界。

误区四:把平台专属库用于所有目标

代码能解析 dart:io 不代表它能在浏览器运行。多平台项目需要隔离平台实现,必要时使用条件导入提供一致接口。

误区五:过度使用 part

part 让文件共享同一个库作用域,也会增加相互依赖。仅为了拆短文件时,独立库加 import 通常更清晰。

小结

  • 每个 Dart 文件都属于一个库,通常不需要显式写 library 指令。
  • dart: 导入 SDK 库,package: 从包的 lib/ 目录解析,相对 URI 根据当前文件解析。
  • as 添加导入前缀,showhide 控制通过导入或导出可见的名称。
  • export 用于组织对外 API,但不会代替当前库所需的 import
  • 以下划线开头的名称是库私有,而不是绝对的文件私有。
  • part 让多个文件组成同一个库并共享私有作用域,应只在确有需要时使用。
  • 延迟导入依赖目标平台和工具链支持,使用前需要核对当前部署环境。