CodeBuddy 安全删除拦截器背锅记:一次 Vite 构建产物陈旧引发的微前端 404 白屏复盘
微前端子应用上线后白屏,控制台几十个 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 里面可以关掉这个拦截或者调大阈值:
- 点击右下角(或左下角)头像
- 进入 IDE 设置
- 在"对话中"相关设置里,关闭"安全删除",或者把"批量删除阈值"调大(比如调到 5000)

这样终端里跑构建就不会再被拦,也不用改任何构建脚本。适合长期在 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 不对,传上去必炸。这次事故如果有这一步,白屏在上线前就能拦住。
几点复盘
- 构建工具的"隐性环境"值得警惕。 我们通常假设同样的命令在任何终端里跑出来的结果一致,但 IDE 注入的 node shim 打破了这个假设。命令对了,环境不对,结果照样错。
- 判断构建成败要看退出码和关键产物,不能只看日志量。 这次 closeBundle 钩子产生的日志非常有欺骗性。最可靠的办法是检查构建链(
&&连接的后续脚本有没有执行)和产物内容本身。 - "看起来正常"的旧产物是最危险的定时炸弹。 dist 没被清空意味着旧文件和新文件混在一起,问题可能延迟到运行时才暴露。部署前校验产物特征(比如 base 前缀)成本极低,收益极高。
写在最后
这次排查前前后后花了两个小时,其中大部分时间花在错误的方向上:先是怀疑 nginx 路由,再怀疑 micro-app 的资源路径处理,最后才回头怀疑构建本身。如果你也遇到"微前端子应用全量 404 + Unexpected token ‘<’",先看看 dist/index.html 里的资源前缀对不对,再看看构建日志的最后几行有没有被什么东西拦截。
如果你也踩过类似的坑,或者有更好的规避方式,欢迎留言交流。
环境说明:CodeBuddy CN(含安全删除拦截器,阈值默认 500)、Vite 4.3.9、micro-app 微前端方案、Windows + PowerShell。