- 前端构建
- 构建工具
- 开发工具
【免费下载链接】farm
Extremely fast Vite-compatible web build tool written in Rust
本文围绕 Farm 仓库中 docs/experiences/rust-plugin-worker-e2e-url-mismatch.md 记录的调试经历展开,完整还原examples/rust-plugin-worker示例在浏览器端 E2E 失败的现象、排查步骤、根因定位与修复方案。读完本文,你将掌握「构建成功但浏览器运行失败」这类隐性缺陷的排查套路,理解 Farm Worker 插件动态入口命名与模块图去重的底层机制,并学会用单元测试与 E2E 双重手段验证修复。
问题背景:构建成功,浏览器却报错
examples/rust-plugin-worker是 Farm 仓库中用于演示 Rust Worker 插件(@farmfe/plugin-worker)的示例工程。它的 E2E 失败有一个非常迷惑的特点:Farm 的构建与 dev server 都能正常启动、编译产物完整,但页面在浏览器中运行 Worker 用例时整体失败。
该失败最初在 Windows 与 macOS 的 CI 上通过pnpm run test-e2e观测到,且呈平台相关特征。由于 Worker 动态入口名在多种 Worker 语法引用同一份源文件时可能被选择得不同,这类问题往往只在特定平台、特定引用方式下暴露,本地 Linux 环境难以复现,排查成本较高。
典型症状与关键信号
E2E runner 的默认冒烟测试(由 scripts/test-e2e-worker.mjs 驱动)会报告来自浏览器控制台的错误:
[rust-plugin-worker] rust-plugin-worker › start Browser console error: Worker case failures [Object, Object, Object, Object, Object, Object, Object]这条报错有三个值得注意的信号:
- Farm 构建或 dev server 已成功启动——错误不是编译期问题,而是运行期问题;
- 失败来自页面自身的
console.error("Worker case failures", results)——这是 examples/rust-plugin-worker/src/index.tsx 中runWorkerCases()收尾处主动打出的日志,说明至少有一个 Worker 用例在浏览器里抛了异常; - 浏览器日志只显示
[Object]——失败用例的名称、错误详情被隐藏,必须直接检查页面或产物才能定位。
从 E2E 机制看,e2e/farm-runner.mjs 的visitPage会监听页面console事件,任何msg.type() === 'error'的控制台错误都会触发Browser console error: ...并让测试失败。因此页面代码里那行console.error一出现,整个用例就被判定为失败。
逐步排查清单
1. 先审查 Worker 用例源码
排查的第一步是回到用例源头。examples/rust-plugin-worker/src/index.tsx 针对同一份vue.worker.ts源文件,同时使用了四种不同的引用语法:
// new URL 绝对路径(以 / 开头) new Worker(new URL("/src/worker/vue.worker.ts", import.meta.url)) // new URL 相对路径 new Worker(new URL("./worker/vue.worker.ts", import.meta.url)) // ?worker 查询参数,导入 Worker 构造函数 import WorkerCtor from "./worker/vue.worker.ts?worker" // ?worker&url 查询参数,导入 worker 脚本 URL import workerUrl from "./worker/vue.worker.ts?worker&url"关键疑点就在这里:如果同一份源文件被多种语法引用,理论上应该产生多个公开的 Worker URL,但 Farm 是按真实源ModuleId追踪动态入口的,因此同一源文件只会保留一个动态入口名。若插件生成的各 wrapper 指向的 URL 不一致,就会产生「产物只发出一个、引用却指向多个」的错位。
2. 构建示例并对比 wrapper URL 与产物文件
执行构建:
cd examples/rust-plugin-worker npm run build然后检查dist/index.*.js中生成的 wrapper 代码,以及dist/目录下实际产出的 Worker 文件。一个典型的坏构建长这样:
function WorkerWrapper(options) { return new Worker("/vue.63ddc497.worker.js", options); } var vue_worker_ts_default = "/vue.c1e83ed7.worker.js";而输出目录里只有:
dist/vue.63ddc497.worker.js对比即可发现:?worker&url导出的vue.c1e83ed7.worker.js指向了一个根本未被产出的文件。这就是「构建成功、运行时失败」的直接证据——构建阶段两个 wrapper 都成功生成,但运行时只有被模块图选中产出的那一个 URL 是真实存在的。
3. 不要轻信 preview 返回的 200
排查时最容易踩的坑是:Farm preview 服务器对缺失的 Worker URL 可能返回index.html兜底内容,因此请求即使返回状态码200,拿到的也可能是 HTML 而不是 Worker 脚本。检查响应体,而不是只检查response.ok。
以 PowerShell 为例,直接查看响应内容的前 120 个字符:
$r = Invoke-WebRequest -Uri http://localhost:1911/vue.c1e83ed7.worker.js -UseBasicParsing $r.Content.Substring(0, [Math]::Min(120, $r.Content.Length))如果响应以<!doctype html>开头,说明该 Worker URL 确实缺失,浏览器实际上把 HTML 当作 Worker 脚本加载了。这也是排查文档中反复强调的经验:只断言fetch(url).ok的 E2E 检查,在 preview 服务器存在 HTML 兜底时会漏掉缺失资源的缺陷。
4. 修改 Rust 插件后必须先重建平台二进制
@farmfe/plugin-worker是 Rust 实现的 NAPI 插件,rust-plugins/worker/scripts/index.js 会按平台、架构从npm/<platform>/index.farm加载对应的本地二进制。因此修改rust-plugins/worker/src/*.rs后,必须先重新构建插件,再运行示例,否则farm build仍会加载旧的本地index.farm二进制,改动不会生效:
pnpm --filter @farmfe/plugin-worker build pnpm run test-e2e -- --example rust-plugin-worker其中pnpm run test-e2e -- --example rust-plugin-worker由 scripts/test-e2e.mjs 解析,--example指定只跑单个示例并禁用并行。
根因:动态入口名与模块图去重的错位
Worker 入口名是如何被计算的(源码证据)
从当前已修复的源码看,Worker 插件的核心逻辑集中在 rust-plugins/worker/src/lib.rs 与 rust-plugins/worker/src/codegen.rs:
load钩子:识别带?worker/?sharedworker/?worker&inline的虚拟模块,返回 URL 型 wrapper 模块或 inline 占位模块;transform钩子:用 constants.rs 中定义的WORKER_IMPORT_META_URL_RE正则匹配new Worker(new URL("...", import.meta.url)),将其改写为确定性的输出 URL,并把(importer, worker 源路径, 入口名)记录到transform_worker_deps;analyze_deps钩子:为上述两类发现注入ResolveKind::DynamicEntry { name: entry_name, ... }依赖,让 Farm 把 Worker 源编译为独立 chunk。
问题出在入口名的哈希输入。修复前的实现用「Worker 源文件名 + 导入时带查询参数的module_id字节」作为哈希输入,导致同一个源文件因查询参数不同而算出不同的入口名:
src/worker/vue.worker.ts?worker -> vue.63ddc497.worker.js src/worker/vue.worker.ts?worker&url -> vue.c1e83ed7.worker.js模块图按真实源模块去重
Farm 存储动态入口时按解析后的源模块进行去重。上面这些导入最终都解析到同一个vue.worker.ts源文件,因此模块图对这份源只能保留一个动态入口名。错位由此产生:
- wrapper 模块在生成阶段立即创建,不同语法可能指向不同 URL;
- 模块图只对解析后的源发出一个 Worker 资源;
- 任何指向未被选中 URL 的 wrapper,运行时会 404 / 加载到 HTML,导致用例失败。
此外还有一个平台敏感的细节:解析出的路径在 Windows 上可能包含\,在类 Unix 上使用/,不同 resolve 分支也可能产生差异。若路径字符串未规范化就参与哈希,等价源文件在不同平台或不同调用点仍会算出不同入口名。
修复:让入口名只依赖真实的 Worker 源路径
修复的核心思路是:让 Worker 入口名对真实的 Worker 源文件保持稳定。对应实现见 codegen.rs 的compute_worker_entry_name:
- 以解析后的 Worker 源路径(
resolved_path)作为哈希输入,而不是带查询参数的虚拟module_id(函数签名中module_id已改为_module_id,不再参与计算); - 哈希前规范化路径分隔符:
resolved_path.replace('\\', "/"),把 Windows 反斜杠统一为/; - 从规范化源路径中取文件名 stem(去掉扩展名),作为入口名的可读前缀。
入口名随后经output.assets_filename模板(如asserts/[resourceName].[hash].[ext])生成完整文件名并剥离扩展名,得到vue.xxxx.worker这样的裸入口名;codegen.rs 的compute_worker_url再套用output.entry_filename与public_path得到最终可访问的 URL。示例工程的配置见 examples/rust-plugin-worker/farm.config.ts,其中插件选项里设置了assetsFilename: 'asserts/[resourceName].[hash].[ext]'。
回归覆盖需要包含两类场景:
- 同一源、不同查询参数:
?worker与?worker&url必须算出同一个入口名; - 同一源、不同路径分隔符:
C:\project\...与C:/project/...必须算出同一个入口名。
这两条已固化为 codegen.rs 中的单元测试:
worker_entry_name_is_stable_for_same_source_with_different_queries:用"C:\project\src\worker\vue.worker.ts"分别以?worker、?worker&url两种 module_id 调用,断言入口名相等;worker_entry_name_is_stable_for_same_source_with_different_path_separators:对比反斜杠路径与正斜杠路径,断言入口名相等。
验证与回归
修复后的最小验证流程如下:
cargo test -p farmfe_plugin_worker worker_entry_name_is_stable pnpm --filter @farmfe/plugin-worker build pnpm run test-e2e -- --example rust-plugin-worker期望结果:
cargo test ... 2 passed rust-plugin-worker › start passed rust-plugin-worker › preview passed最后再检查重建后的dist/index.*.js:所有对同一份vue.worker.ts源的引用(?worker、?worker&url、两种new URL写法)都应指向同一个产出的vue.*.worker.js文件,不再出现「wrapper 指向多个、产物只发一个」的错位。
经验总结
本次排查沉淀下来四条可复用的工程经验:
- 插件在编译器产出资源之前就创建 URL wrapper 时,wrapper 预测的 URL 必须与编译器动态入口产出路径使用完全相同的身份标识,否则任何一处不一致都会在运行时爆发;
- 如果模块图按解析后的源模块去重,Worker 源的身份标识就不应包含查询参数,
?worker、?worker&url这类语法差异必须被归一化; - 只断言
fetch(url).ok的 E2E 检查可能漏掉缺失资源缺陷——preview 服务器对缺失脚本可能返回 HTML 兜底并给出 200;排查 Worker 失败时务必校验生成 URL 的响应内容(如检查是否以<!doctype html>开头); - 修改 Rust 插件后,验证示例前必须重新构建平台插件二进制,避免
farm build继续加载旧的index.farm本地产物,导致「改了没生效」的假象。
- 前端构建
- 构建工具
- 开发工具
【免费下载链接】farm
Extremely fast Vite-compatible web build tool written in Rust
相关推荐
Unison 中构造器缺失导致的 update 失败:声明一致性检查与 alias.term 修复指南
Unison 中构造器缺失导致的 update 失败:声明一致性检查与 alias.term 修复指南 Unison 的 update 命令在替换类型定义时会对
编程语言编译器语言运行时开发工具oh-my-openagent 的 CI 修复实践:fileURLToPath 修复 Windows 上 file URL 转换导致的 ENOENT 失败
oh my openagent 的 CI 修复实践:fileURLToPath 修复 Windows 上 file URL 转换导致的 ENOENT 失败 本篇
人工智能AI Agent代码智能体多智能体MCP ClientsAgent 编排slime RL Training实战指南:一条命令跑通后训练
slime RL Training实战指南:一条命令跑通后训练 你正准备给 Qwen3 4B 做强化学习后训练,训练引擎、推理引擎、奖励数据要自己接,装环境就能
人工智能大模型强化学习RLHF分布式训练
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考