extension dev 会话都会把状态写到磁盘上:机器可读的契约、日志、一条控制通道,以及一个受管理的浏览器配置文件。知道每个文件放在哪里,你就知道哪些东西能在清空 dist/ 后幸存、哪些可以安全解析,以及哪些绝对不能进入提交。
双根目录契约
会话状态分布在两个生命周期不同的根目录中:
这样拆分是有意为之。浏览器配置文件的寿命可能比
dist/ 更长,而缓存在其中的扩展会记住它当初拿到的控制坐标。当这些坐标放在 dist/ 下时,清空该目录就会让配置文件里缓存的 service worker 变成孤儿,一直去拨打一个已经失效的端口。凡是配置文件可能已经写死记住的东西,现在都放在 .extension-js/ 里,清空 dist/ 碰不到它们。
控制令牌文件以 0600 权限写入。它们会授权诸如 extension eval 这类会改变状态的动作,因此只有你自己的用户能读取。
按浏览器划分的键
每个产物的名称里都嵌入了浏览器:control-token-chrome、dist/extension-js/firefox/ready.json。如果每个项目只有一个槽位,那么一旦同一个项目上启动第二个浏览器会话就会出问题:第二个会话会覆盖第一个会话的令牌,而任何一方退出时都会把它删掉,两边都用不了。
正因为每个槽位都带键,你可以在同一个项目上同时运行 extension dev --browser chrome 和 extension dev --browser firefox。每个会话都保有自己的配置文件、自己的调试端口和自己的控制通道。
在 dist/extension-js/ 内部,每个浏览器都有自己的产物目录:
调试端口的推导
每个会话都会推导出属于自己的 DevTools 调试端口,而不是共用同一个:- 从基数开始:你的
--port值加上固定偏移100;若没有有效的基数,则用默认的9222。 - 再加上一个按实例区分的偏移,它由实例 id 的前 8 位十六进制字符对 1000 取模得到。
--port 0 表示由操作系统分配开发服务器端口,因此 Extension.js 拒绝据此推导调试端口。从零推导会得到一个无法绑定的特权端口,所以此时改用默认值。
实例注册表
连接到某个会话的工具,会通过一张按精确实例 id 建键的实例注册表来解析端口。精确 id 优先。对于已知但未注册端口的实例,返回的是调用方自己给的回退值,绝不会是另一个实例的端口。 当既没有给出实例 id、也没有回退值时,查找会抛出AmbiguousInstanceError,而不是去猜。退而取最近启动的浏览器会把不同实例的数据流串在一起,所以注册表拒绝这么做。
拆除
会话结束时,Extension.js 会分阶段拆除浏览器:- 在信号路径上,浏览器子进程先收到
SIGTERM,经过 5 秒宽限期后再收到SIGKILL。 - 在进程退出时,处理器只有一个同步时间片,因此会同步地用
SIGKILL强制结束子进程。 - 在 Windows 上,两条路径都用
taskkill /PID <pid> /T /F结束整棵进程树。
ECONNRESET、EPIPE、ECONNABORTED 或 ENOTCONN 这几种 socket 错误码会被视为无害。它们来自浏览器正在关闭的 socket,因此优雅关闭仍然是优雅关闭,而不会以退出码 1 结束。
自动写入的 gitignore
Extension.js 会在dist/extension-js/ 里写入一个内容为 * 的 .gitignore,让整个会话根目录都不进入提交。如果你的项目根目录已经有 .gitignore,它还会往里追加 .extension-js,因为处于活动状态的控制令牌同样绝不能进入提交。这两次写入都只是卫生防护:它们绝不会覆盖已有内容,也绝不会让构建失败。
build-summary.json
调用extension build 的宿主程序可以拿到一条结构化的结果通道,而不用去刮取 stdout:
该文件不会在两次运行之间被删除,因此使用方必须自行防范读到过期文件。在信任它之前,请把文件的 mtime 与你启动构建的时间做比较。
hot/ 的清理
在开发期间,热更新 chunk 是在扩展 origin 内部从磁盘取回的,因此过期的代次会不断堆积在最终交付的内容里。每次编译之后,Extension.js 会把hot/ 清理到只剩当前代次加上前一代次。保留前一代次一轮,是为了让正在传输中的请求仍然能取到内容。
下一步
- 通过 ready.json 与事件流 解析会话状态,而不是解析终端输出。
- 在 浏览器配置文件 中了解受管理配置文件的行为。
- 在 读懂 CLI 输出 中了解会话在终端一侧的样子。

