CodeBuddy 安全删除拦截器背锅记:一次 Vite 构建产物陈旧引发的微前端 404 白屏复盘

2026-09-20 软件资源 1 次阅读 0 次点赞
一次微前端子应用上线后白屏、静态资源全部404的事故复盘。作者排查后发现,代码与nginx配置均正常,问题出在CodeBuddy IDE的“安全删除”拦截器:它拦截了Vite构建时清空dist目录的操作(dist/admin含617个文件,超过500阈值),导致构建中断。由于Vite先打包、再清空、最后写入,中断后仅closeBundle钩子插件仍有日志输出,造成“构建成功”假象,实际上传的仍是base路径错误的旧产物。文章给出四种解决办法,并强调部署前应校验index.html资源前缀。

微前端子应用上线后白屏,控制台几十个 404。最后发现代码、nginx、微前端配置都没问题,问题出在 CodeBuddy IDE 的"安全删除"功能把 Vite 构建的清空目录步骤拦下来了,构建实际失败,dist 里躺着的还是上一次独立模式打包的旧产物。这篇文章记录完整排查过程和解决办法。

事情的经过

今天早上,我把公司一个业务系统的微前端子应用重新打包,上传到 nginx 的 html/sub-app/ 目录,刷新页面,白屏。

打开控制台,场面很热闹:

GET https://app.example.com/assets/vendor-5ecfbfac.css 404
GET https://app.example.com/assets/index-42ea9f7e.css 404
GET https://app.example.com/cesium/Cesium.js 404
...
Cesium.js:1 Uncaught SyntaxError: Unexpected token '<'
vue.min.js:1  Uncaught SyntaxError: Unexpected token '<'

所有静态资源全部 404,然后 micro-app 把 404 返回的 HTML 页面当成 JS 去执行,报出一串 Unexpected token '<'

奇怪的地方在于:9 月 18 日之前打出来的包都是好的,同一个分支,没改构建配置。为什么今天就不行了?

第一轮排查:都是路径的锅?

这个项目是微前端架构:主应用部署在 /,子应用部署在 /sub-app/,nginx 里用 alias 指到 html/sub-app/

location ^~ /sub-app/ {
    alias html/sub-app/;
    index index.html;
    try_files $uri $uri/ /sub-app/index.html;
}

404 的请求都指向了根路径(比如 /assets/vendor-5ecfbfac.css),而不是 /sub-app/assets/vendor-5ecfbfac.css。资源引用缺少子应用前缀,nginx 自然就到主应用的目录底下找,找不到就是 404。

为什么引用会缺前缀?微前端打包模式(build:micro)下,Vite 的 base 应该是 /sub-app/,产出的 index.html 里所有资源引用都该带这个前缀。我打开本地刚打出来的 dist/index.html 一看:

<script src="/assets/index-8cbb389b.js"></script>
<link rel="stylesheet" href="/assets/index-42ea9f7e.css" />
<script src="/cdn/vue/2.7.14/vue.min.js"></script>

果然,一个前缀都没有。这就是 base 为 / 的独立部署模式的产物。

可是构建命令明明是 npm run build:micro,对应 --mode production.micro,环境文件 .env.production.micro 里也写着 VITE_BASE_URL=/sub-app/。我用 node 直接验证过 loadEnv('production.micro', ...) 的返回值,是正确的。

配置没问题,命令也没敲错。那这份 dist 是怎么来的?

第二轮排查:复现构建

怀疑构建过程本身有问题,我在终端里重新跑了一遍:

npm run build:micro

终端滚了满屏的打包日志、terser 警告、brotli 压缩输出,看起来一切正常。但翻到最后,有这么一段:

error during build:
Error: [safe-delete][SAFE_DELETE_BULK_CONFIRM_REQUIRED]
{"count":617,"threshold":500,"scope":"turn",
 "targets":["<项目路径>\\dist\\admin"],"targetCount":1}
    at checkBulkDeleteGuard
    (C:\\Users\\xxx\\AppData\\Local\\Programs\\CodeBuddy CN\\resources\\app\\
     extensions\\genie\\out\\vendor\\shim\\node-safe-delete-shim.cjs:404:19)
    at tryTrash (...node-safe-delete-shim.cjs:733:5)
    at tryRm (...node-safe-delete-shim.cjs:906:5)
    at Object.wrappedRmSync [as rmSync] (...node-safe-delete-shim.cjs:912:15)
    at emptyDir (...vite/dist/node/chunks/dep-xxx.js:12177:18)
    at prepareOutDir (...dep-xxx.js:46519:13)
    at build (...dep-xxx.js:46473:13)

真相在这里。CodeBuddy CN 的 IDE 扩展会给终端里启动的 node 进程注入一个"安全删除"拦截器(通过 NODE_OPTIONS=--require=...node-safe-delete-shim.cjs),任何一次性的批量删除超过阈值(默认 500 个文件)都会被拦下来,要求人工确认。而 npm run build 是后台进程,没人能去点那个确认框,于是拦截器直接抛异常。

被拦的是谁?是 Vite 构建开始时的 emptyOutDir:它要清空旧的 dist 目录,其中 dist/admin 一个目录就有 617 个文件,超过了 500 的阈值,整次构建在这里就断了。

最迷惑人的一步:为什么看起来构建成功了

这是整个事故里最阴险的地方,值得单独讲。

翻 Vite 4.3.9 的源码,build() 的执行顺序是这样的:

bundle = await rollup(rollupOptions); // 1. 先完成打包
if (options.write) {
  prepareOutDir(outDirs, options.emptyOutDir, config); // 2. 再清空输出目录
}
for (const output of normalizedOutputs) {
  res.push(await bundle[options.write ? "write" : "generate"](output)); // 3. 最后写文件
}

也就是说:rollup 先把所有代码编译好,然后清空 dist,最后才把产物写进磁盘。

当第 2 步被安全删除拦截器打断时,第 3 步(写入产物)根本没执行。但 Vite 在 finally 里还会调用 bundle.close(),这会触发各个插件的 closeBundle 钩子。而项目里恰好有两个插件在 closeBundle 里干活:

  • vite-plugin-cesium 往 dist/sub-app/cesium/ 拷贝 Cesium 资源
  • vite-plugin-compression 给 dist 里的文件生成 .br 压缩包

所以构建中断之后,控制台里仍然会滚出大段大段的 brotli 压缩日志,dist 里也有新时间戳的文件。如果你只看最后有没有报红、有没有新文件生成,很容易得出"构建成功"的结论。实际上 index.html 和 assets 目录里的核心产物一个都没写入,dist 里躺着的还是上一次 npm run build(独立部署模式,base 为 /)的旧产物。

把旧产物传上服务器,就是早上那场白屏事故。

一句话总结这个 bug:构建失败被安全删除拦截器静默吞掉了大半,产物新旧混在一起,最后上传了一份 base 路径错误的旧包。

解决办法

方法一:调整 CodeBuddy IDE 设置(推荐)

CodeBuddy IDE 里面可以关掉这个拦截或者调大阈值:

  1. 点击右下角(或左下角)头像
  2. 进入 IDE 设置
  3. 在"对话中"相关设置里,关闭"安全删除",或者把"批量删除阈值"调大(比如调到 5000)

CodeBuddy IDE 安全删除设置

这样终端里跑构建就不会再被拦,也不用改任何构建脚本。适合长期在 CodeBuddy 里做前端开发的场景。

方法二:构建时清掉注入

拦截器靠 NODE_OPTIONS 环境变量注入,构建前清掉它就行:

$env:NODE_OPTIONS=''
npm run build:micro

临时救急用这个最快,不用动任何配置。

方法三:外部终端构建

用系统自带的 PowerShell 或 CMD(不经 CodeBuddy 启动)去跑 npm run build:micro,环境变量里没有那个 shim,构建不受影响。

方法四:构建前手动清空 dist

拦截器只拦 node 进程里的删除,PowerShell 自己的 Remove-Item 不受影响:

Remove-Item -Recurse -Force dist
npm run build:micro

不过这只解决了 emptyOutDir 这一处,如果构建链路里还有别的插件在 closeBundle 阶段做大量删除(比如某些资源处理脚本),还是可能踩雷。所以我个人更推荐方法一或方法二。

部署前自检

不管用哪种方法,强烈建议把这一步固化成习惯:上传 dist 之前,花 5 秒钟检查一下 index.html 的资源前缀。

Select-String dist\index.html -Pattern '/sub-app/assets/'

有输出,才是微前端模式的包;没有输出,说明 base 不对,传上去必炸。这次事故如果有这一步,白屏在上线前就能拦住。

几点复盘

  1. 构建工具的"隐性环境"值得警惕。 我们通常假设同样的命令在任何终端里跑出来的结果一致,但 IDE 注入的 node shim 打破了这个假设。命令对了,环境不对,结果照样错。
  2. 判断构建成败要看退出码和关键产物,不能只看日志量。 这次 closeBundle 钩子产生的日志非常有欺骗性。最可靠的办法是检查构建链(&& 连接的后续脚本有没有执行)和产物内容本身。
  3. "看起来正常"的旧产物是最危险的定时炸弹。 dist 没被清空意味着旧文件和新文件混在一起,问题可能延迟到运行时才暴露。部署前校验产物特征(比如 base 前缀)成本极低,收益极高。

写在最后

这次排查前前后后花了两个小时,其中大部分时间花在错误的方向上:先是怀疑 nginx 路由,再怀疑 micro-app 的资源路径处理,最后才回头怀疑构建本身。如果你也遇到"微前端子应用全量 404 + Unexpected token ‘<’",先看看 dist/index.html 里的资源前缀对不对,再看看构建日志的最后几行有没有被什么东西拦截。

如果你也踩过类似的坑,或者有更好的规避方式,欢迎留言交流。


环境说明:CodeBuddy CN(含安全删除拦截器,阈值默认 500)、Vite 4.3.9、micro-app 微前端方案、Windows + PowerShell。

最后更新于刚刚

评论 (0)

登录 后发表评论

暂无评论,快来发表第一条评论吧!