- 编程语言
- 编译器
- 语言运行时
- 标准库
- 开发工具
【免费下载链接】sdk
The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
reload_test 是 Dart SDK 内置的、用于验证 DDC(Dart Dev Compiler)热重载(Hot Reload)与热重启(Hot Restart)能力的专用测试框架。它通过“同一文件多代际(generation)”的测试组织方式,让同一测试用例可以在 VM、Chrome 与 D8 三个运行时后端上验证热重载行为是否符合预期。阅读本文后,你将掌握 reload_test 的文件命名约定、config.json 配置规则、运行时辅助 API 的使用方法,以及前端编译器与内存文件系统在底层如何驱动多代际编译。
框架定位:为什么需要专门的 Hot Reload 测试包
热重载与热重启是 Dart 开发体验的关键能力:修改代码后无需重新启动程序即可看到效果。但要在 CI 中自动化验证这一行为,需要解决一个根本问题——如何让一个测试程序“演进”成另一个版本的自己。
pkg/reload_test/README.md 给出了答案:该包包含一组工具,通过测试多个代际(generations)的文件来验证热重载与热重启。其核心思路是:
- 每个测试目录内保留同一逻辑文件的多个版本(如
main.0.dart、main.1.dart); - 测试程序运行第 0 代,然后由测试框架调用热重载/热重启 API,切换到第 1 代、第 2 代……;
- 每一代代码内可以自行断言状态是否符合预期(例如旧静态变量被重置、新字段被正确初始化);
- 整个流程在 VM 与 Web(Chrome/D8)两个运行时上分别执行,验证跨后端行为一致性。
该包在 pubspec.yaml 中声明为publish_to: none,仅供 SDK 内部使用,依赖dev_compiler、frontend_server与vm_service,SDK 约束为^3.12.0-0。
测试文件代际约定(核心规范)
README 中明确定义了测试文件的命名规则,这是编写任何 reload_test 用例的前提:
代际编号:file_name.N.dart
不同代际的同一文件,在文件名后附加代际数字。例如file_name.1.dart就是该文件的第 1 代版本。第 0 代通常没有编号后缀(如main.0.dart中的0是代际号),后续代际依次递增。
以 tests/hot_reload/top_level_parse_error 为例,该测试目录包含:
main.0.dart:第 0 代,程序正常,helper()返回 4;main.1.reject.dart:第 1 代,文件内注入了语法错误的代码行kjsadkfjaksldfjklsadf;;config.json:声明第 1 代预期被拒绝及其错误信息。
第 0 代代码调用hotReload(expectRejection: true),期望第 1 代被编译期拒绝(详见下文“拒绝代际”)。
拒绝代际:.reject
如果某一代际的代码有意不可热重载(例如包含语法错误),该代际的每个文件名都应包含.reject后缀。同时,测试目录下的config.json必须包含"expectedErrors"键,其值是一个“代际数字字符串 → 错误信息字符串”的映射,用于在运行时校验拒绝原因。
{ "expectedErrors": { "1": "Variables must be declared using the keywords 'const', 'final', 'var' or a type name" } }上述是 top_level_parse_error/config.json 的真实内容:第 1 代包含顶层无类型声明导致的解析错误,错误文案在编译期由前端编译器产出并被框架校验。
重启代际:.restart
如果某一代际执行的是热重启而非热重载,则该代际的每一个文件名都必须包含.restart。例如 tests/hot_reload/hot_restart_static 测试目录中,main.0.dart之后依次有main.1.restart.dart、main.2.restart.dart、main.3.restart.dart三代重启文件。
这一代际用于验证热重启会完整重置程序状态。在main.0.dart中,程序为无初始化器的静态字段赋值;到main.1.restart.dart中,代码断言所有静态字段都已恢复为初始值(Expect.equals(null, noInitializer)),随后再次赋值并断言,接着继续调用hotRestart()进入下一代际。
互斥约束
同时指定.reject与.restart属于错误用法。一个代际要么被拒绝(停留在上一版本),要么触发重启,二者语义冲突,测试框架会将其视为非法配置。
运行时辅助 API:在测试代码中触发热重载
测试代码本身通过 pkg/reload_test/lib/reload_test_utils.dart 暴露的辅助函数来触发热重载/热重启。该文件利用 Dart 的条件导入机制,按运行时自动选择实现:
export 'src/_reload_utils_api.dart' if (dart.library.io) 'src/_vm_reload_utils.dart' if (dart.library.js_interop) 'src/_ddc_reload_utils.dart';即:在 VM(dart.library.io)上使用 _vm_reload_utils.dart,在 Web(dart.library.js_interop)上使用 _ddc_reload_utils.dart。公共 API 声明在 _reload_utils_api.dart 中,包括:
| API | 说明 |
|---|---|
hotReload({bool expectRejection = false}) | 触发一次热重载;expectRejection: true表示期望本次重载被拒绝 |
hotRestart() | 触发一次热重启 |
hotReloadGeneration | 当前应用的重载代际号 |
hotRestartGeneration | 当前应用的重启代际号 |
在测试用例中典型用法如下(摘自top_level_parse_error/main.0.dart):
import 'package:expect/expect.dart'; import 'package:reload_test/reload_test_utils.dart'; helper() { return 4; } Future<void> main() async { Expect.equals(4, helper()); await hotReload(expectRejection: true); }注意:hotRestart与hotReloadGeneration在 _reload_utils_api.dart 中均以throw Exception('Not implemented on this platform.')作为默认实现占位,真正的行为由条件导入的 VM/DDC 实现提供,因此测试代码不应依赖平台无关的默认实现。
VM 后端实现:通过 VM Service 协议驱动重载
在 Dart VM 上,热重载通过 VM Service 协议实现。核心逻辑位于 _vm_reload_utils.dart 的HotReloadHelper类:
- 建立连接:
HotReloadHelper.create()调用Service.controlWebServer(enable: true)获取 VM Service URI,再通过vm_service_io.vmServiceConnectUri(wsUri)建立 WebSocket 连接;若未启用 VM Service,会打印提示并以io.exit(1)退出。 - 定位代际目录:从当前 isolate 的
name(即 dill 文件 URI)中解析generation前缀,并推导errorDillName(将.dill替换为.error.dill),用于查找“含编译期错误的代际产物”。 - 重载下一代际:
_reloadNextGeneration()使代际计数器generation += 1,构造generation$generation/$dillName路径后调用vmService.reloadSources(_id, rootLibUri: ...)。若reloadReport.success为 false,则抛出异常说明拒绝原因。 - 拒绝验证:
_rejectNextGeneration()先检查generation$generation/$errorDillName是否存在——若存在,说明该代际的编译期错误已在编译阶段校验通过,直接返回Status.rejected与固定的compileTimeErrorMessage;否则发起reloadSources并断言其失败,若意外成功则抛出“未被拒绝”的异常。 - 收尾:当
hasNextGeneration为 false(下一个代际目录不存在)时,调用cleanUp()断开 VM Service,让 VM 正常结束测试。
VM Service 的ReloadReport并未在公共 API 中暴露“取消原因”,因此源码通过扩展(extension)解析 JSON 中的notices字段,寻找ReasonForCancelling或ReasonForCancellingReload类型的通知来获取拒绝消息。
DDC 后端实现:基于 module loader 与 dartDevEmbedder
在 Web 侧,_ddc_reload_utils.dart 通过dart:js_interop直接对接 DDC 运行时的 JavaScript 对象,其结构与 DDC 的ddc_module_loader.js中定义的接口一一对应:
_dartLoader.loader($dartLoader):暴露hotReload()、hotRestart()、hotReloadGeneration、hotRestartGeneration、intendedHotRestartGeneration;dartDevEmbedder:暴露hotReload(files, ids)、hotRestartBegin(filesToRequest)、hotRestartEnd()及对应的代际计数器;$injectedFilesAndLibrariesToReload(fileGeneration):按代际返回“库 ID + JS URL”的二维数组,用于重载;$injectedReloadedSourcesHelper():返回重启所需加载的文件描述符数组,无后续代际时返回null。
hotReload()的实现先递增本地文件代际计数器,再调用injectedFilesAndLibrariesToReload查询该代际的编译产物;若产物为null,说明该代际在编译期已被拒绝(对expectRejection: true而言是预期行为),返回Status.rejected;否则调用_dartDevEmbedder.hotReload(files, ids)并返回Status.accepted。
hotRestart()的实现先递增intendedHotRestartGeneration,调用injectedReloadedSourcesHelper()获取待加载文件,随后hotRestartBegin→ 打印重启 receipt →hotRestartEnd。特别地,receipt 必须在重新执行main之前打印,以保证代际信息可被测试框架采集。
代际产物如何注入:内存文件系统与引导脚本
Web 端的编译产物并非直接落盘运行,而是由 hot_reload_memory_filesystem.dart 中的HotReloadMemoryFilesystem管理。它实现了FileResolver接口,将前端编译器按调用输出的“拼接式”代码与 sourcemap 通过 manifest 中的字节偏移切分还原为单个文件:
update(codeFile, manifestFile, sourcemapFile, generation:):读取 manifest 中各文件的code/sourcemap偏移区间,切出对应字节块,并登记该代际变更的库列表(generationChanges),同时把packages/...路径还原为package:URI,其余文件映射为hot-reload-test:///虚拟 URI;writeToDisk(outputDirectoryUri, generation:):把内存中的文件写入generation$generation/目录,供 VM 侧的 dill 加载或 DDC 侧引导脚本引用;scriptDescriptorForBootstrap:输出第 0 代所有库的{id, src}描述符,供模块系统启动时加载。
真正的 JS 引导逻辑由 ddc_helpers.dart 生成,分为两套:
generateD8Bootstrapper:面向 D8(无 DOM 的 JS 引擎)的引导脚本,它加载ddc_module_loader.js与dart_sdk,注入$injectedFilesAndLibrariesToReload、$injectedReloadedSourcesHelper、$dartReloadModifiedModules三个辅助函数,并针对 D8 不支持原生 Timer 的特性,用“代际比对 + 取消回调”的方式包裹setInterval/setTimeout,防止重启后旧定时器误触发;generateChromeBootstrapper/generateChromeMainEntrypoint:面向 Chrome 的引导脚本,通过forceLoadModule与loadHotRestartScript动态注入<script>标签完成重载/重启,并适配 CSP nonce 与 Trusted Types 策略。
D8Configuration会依据Abi.current()自动定位third_party/d8/<platform>/<arch>/d8二进制;ChromeConfiguration则按 Windows / macOS / Linux 解析 Chrome 可执行文件路径。注意 Web 后端仅支持单一 app 名——$dartReloadModifiedModules中若出现与entrypointModuleName不符的 app 名会直接抛错,多 subapp 场景不受支持。
Frontend Server 控制器:按代际增量编译
多代际的代码如何变成可重载的产物?答案是复用 Dart SDK 的 Frontend Server。 frontend_server_controller.dart 中的HotReloadFrontendServerController以进程内方式启动前端服务器(starter(...)),并通过流式输入/输出与之同步。它支持以下指令:
> compile <input.dart> # 全量编译 > recompile [<input.dart>] <boundary-key> # 增量重编译 > <dart file> # 失效文件列表 > <dart file> > <boundary-key> > recompile-restart <input.dart> <boundary-key> # 供热重启使用的重编译 > accept # 接受编译结果 > reject # 拒绝(存在编译错误) > quit # 退出前端服务器的完成输出格式为:
result <boundary-key> <boundary-key> [<错误文本或 +前缀的变更文件 / -前缀的失效文件>] <boundary-key> <output.dill> <error-count>控制器通过状态机(FrontendServerState)逐行解析输出,并将每次 compile/recompile 的结果封装为CompilerOutput(含 dill 路径、错误计数、变更源文件列表)。由于前端编译器会累积历史错误计数,控制器在计算“本次实际新增错误数”时会减去totalErrors;而在每次recompile前又将其归零,以匹配前端编译器“重编译前清空错误”的行为。
测试结果与配置:config.json 与 Receipt
config.json 的结构
test_helpers.dart 中的ReloadTestConfiguration.fromJsonFile负责解析测试目录下的config.json,支持两个键:
| 键 | 类型 | 含义 |
|---|---|---|
"exclude" | 字符串数组 | 跳过该测试的运行时平台名,取值必须与RuntimePlatforms枚举一致:chrome、d8、vm |
"expectedErrors" | {代际数字字符串: 错误字符串} | 声明哪些代际预期在编译期被拒绝,以及对应的错误文案 |
例如hot_restart_static/config.json仅含"exclude": ["vm"],表示该测试只验证 Web 后端的重启语义(VM 上.restart代际的重启行为由其他测试覆盖);而async_call_top_level_function_parameters_change_vm/config.json则排除chrome与d8,只在 VM 上运行。
RuntimePlatforms枚举同时记录是否产出 JS(chrome/d8为emitsJS: true,vm为false),测试运行器据此决定走 DDC 流水线还是 VM dill 流水线。
HotReloadReceipt:运行期结果回传
测试程序执行hotReload()/hotRestart()后,会以固定前缀打印一条 JSON 记录,即 hot_reload_receipt.dart 中定义的HotReloadReceipt:
enum Status { accepted, rejected, restarted } class HotReloadReceipt { final int generation; final Status status; final String? rejectionMessage; // 被拒绝时的原因 }其序列化格式为{"generation":1,"status":"accepted"}或{"generation":1,"status":"rejected","rejectionMessage":"..."},且每行以标记_!# HOT RELOAD RECEIPT #!_:开头(见hotReloadReceiptTag)。测试运行器通过扫描标准输出来收集这些记录,与config.json中的预期逐一代际比对,从而判断热重载的“接受/拒绝/重启”行为是否与声明一致。对于编译期已确认的错误,rejectionMessage使用固定的compileTimeErrorMessage('Correct error verified at compile time.'),避免与运行期拒绝信息混淆。
结果汇总
同一文件中还提供TestResultOutcome,用于把每个测试用例的运行结果(配置、耗时、预期/实际结果、日志)以 JSON 形式输出给上层测试框架(如hot_reload/<test>形式的结果记录),便于 CI 汇总与失败排查。
编写一个热重载测试的完整流程
综合以上组件,编写并运行一个 reload_test 用例的流程可归纳为:
- 创建测试目录与代际文件:在 tests/hot_reload 下新建目录,按“代际编号 + 可选
.reject/.restart”规则编写main.0.dart、main.1.dart(或main.1.reject.dart、main.1.restart.dart); - 声明预期:在
config.json中填写exclude(限定运行平台)与expectedErrors(声明被拒绝代际的错误文案); - 编写断言:在每代代码内通过
Expect.equals(...)等校验状态,并调用hotReload()、hotReload(expectRejection: true)或hotRestart()推进代际; - 驱动编译与运行:测试运行器启动 Frontend Server,对每个代际执行 compile / recompile(Web 侧产物经
HotReloadMemoryFilesystem写入代际目录,并生成 D8/Chrome 引导脚本); - 校验 receipt:运行器采集程序输出的
HOT RELOAD RECEIPT记录,与expectedErrors及调用顺序比对,汇总为TestResultOutcome。
小结
reload_test 是 Dart SDK 中高度工程化的热重载验证框架:它用“文件代际 +.reject/.restart后缀 + config.json 预期声明”这一套轻量约定,把复杂的热重载语义测试收敛为可读的文件布局;再通过 VM Service 协议(VM 后端)与 DDC module loader / dartDevEmbedder(Web 后端)两套运行时实现,以及 Frontend Server 增量编译与内存文件系统的支撑,让同一份测试代码跨vm、chrome、d8三个平台验证一致行为。理解这套约定与实现,是深入 Dart SDK 热重载机制、乃至为 SDK 贡献新的热重载测试用例的起点。相关实现可继续参阅 README(约定)、reload_test_utils.dart(公共 API)与 tests/hot_reload(真实测试用例)。
- 编程语言
- 编译器
- 语言运行时
- 标准库
- 开发工具
【免费下载链接】sdk
The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
相关推荐
Dart SDK:Dart VM 热重载(Hot Reload)语义与实现原理详解
Dart SDK:Dart VM 热重载(Hot Reload)语义与实现原理详解 本文基于 Dart SDK 仓库中的官方文档 hot reload.md h
编程语言编译器语言运行时标准库开发工具Compose Hot Reload:实现多平台UI快速迭代的利器
Compose Hot Reload:实现多平台UI快速迭代的利器 Compose Hot Reload:项目的核心功能/场景 Compose Hot Relo
RunAnywhere Flutter SDK 深度解析:基于 Dart FFI 的端侧 AI 多后端架构与实战指南
RunAnywhere Flutter SDK 深度解析:基于 Dart FFI 的端侧 AI 多后端架构与实战指南 RunAnywhere Flutter S
AI模型推理服务推理引擎本地部署多模态
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考