缘起
pnpm build 构建出的 dist 文件,包含一个文件名为 RefreshOutlined-D74sw_ad.js 的图标组件,它以 ad.js 结尾导致被我浏览器的广告拦截插件屏蔽,导致整个页面无法加载,控制台提示 GET /assets/RefreshOutlined-D74sw_ad.js net::ERR_BLOCKED_BY_CLIENT。
分析
Rollup (Vite 8 使用 Rolldown) 的构建产物文件名的哈希是 base64 编码,随机字符序列里有一定概率出现 *_ad.js等敏感字样,落在文件名末尾就会触发 uBlock Origin Lite 等插件的拦截规则(比如 EasyList 里的 *-ad.js$script)。楼主能遇到这个问题也是说明触发概率不低。
踩坑:generateBundle 在 Vite 8 + Rolldown 下无效
直觉上应该在 generateBundle 钩子里修改 bundle 对象(新增 key、删除旧 key),这在 Rollup 时代可以工作。但 Vite 8 底层已切换到 Rolldown,Rolldown 明确禁止插件对 bundle 对象赋新键:
[plugin rename-ad-chunks] Error: This plugin assigns to bundle variable.
This is discouraged by Rollup and is not supported by Rolldown. This will be ignored.修改被静默忽略,文件名原样写入磁盘。
修复
正确方案是在 closeBundle 钩子里操作——此时所有文件已写入磁盘,直接做文件系统级别的重命名 + 内容替换,绕开 Rolldown 对 bundle 对象的限制。
新建 plugins/renameAdChunks.ts:
/**
* Vite 插件:将构建产物中文件名以 `ad.js` 结尾的 chunk 重命名,
* 避免被浏览器广告拦截插件屏蔽,并同步更新所有引用该文件的其他文件内容。
*
* 背景:Vite 8 底层使用 Rolldown,Rolldown 不支持在 generateBundle 中修改
* bundle 对象(新增/删除 key),因此必须在 closeBundle 阶段对磁盘文件直接操作。
*/
import fs from "node:fs";
import path from "node:path";
import type { Plugin, ResolvedConfig } from "vite";
const AD_SUFFIX_RE = /ad\.js$/;
/** 生成一个不含 'a'/'d' 的随机两字母后缀,降低未来再次命中拦截规则的概率 */
function randomSuffix(): string {
const pool = "bcefghijklmnopqrstuvwxyz";
return (
pool[Math.floor(Math.random() * pool.length)] +
pool[Math.floor(Math.random() * pool.length)]
);
}
export function renameAdChunksPlugin(): Plugin {
let outDir: string;
return {
name: "rename-ad-chunks",
enforce: "post",
configResolved(config: ResolvedConfig) {
outDir = path.resolve(config.root, config.build.outDir);
},
closeBundle() {
const assetsDir = path.join(outDir, "assets");
if (!fs.existsSync(assetsDir)) return;
const allFiles = fs.readdirSync(assetsDir);
const adFiles = allFiles.filter((f) => AD_SUFFIX_RE.test(f));
if (adFiles.length === 0) return;
const suffix = randomSuffix();
console.log("\n[rename-ad-chunks] 本次随机后缀:", suffix);
/** 旧文件名 -> 新文件名 的映射(仅 basename) */
const renameMap = new Map<string, string>();
for (const oldName of adFiles) {
const newName = oldName.replace(AD_SUFFIX_RE, `${suffix}.js`);
renameMap.set(oldName, newName);
console.log(` ${oldName} → ${newName}`);
}
// 第一步:重命名磁盘上的 ad.js 文件
for (const [oldName, newName] of renameMap) {
fs.renameSync(
path.join(assetsDir, oldName),
path.join(assetsDir, newName)
);
}
// 第二步:替换 assets/ 下所有 .js 文件中的引用
const jsFiles = fs
.readdirSync(assetsDir)
.filter((f) => f.endsWith(".js"));
for (const jsFile of jsFiles) {
const filePath = path.join(assetsDir, jsFile);
let content = fs.readFileSync(filePath, "utf-8");
let changed = false;
for (const [oldName, newName] of renameMap) {
if (content.includes(oldName)) {
content = content.replaceAll(oldName, newName);
changed = true;
}
}
if (changed) {
fs.writeFileSync(filePath, content, "utf-8");
console.log(` [updated refs] assets/${jsFile}`);
}
}
// 第三步:替换 index.html 中的引用(如有)
const htmlPath = path.join(outDir, "index.html");
if (fs.existsSync(htmlPath)) {
let html = fs.readFileSync(htmlPath, "utf-8");
let changed = false;
for (const [oldName, newName] of renameMap) {
if (html.includes(oldName)) {
html = html.replaceAll(oldName, newName);
changed = true;
}
}
if (changed) {
fs.writeFileSync(htmlPath, html, "utf-8");
console.log(" [updated refs] index.html");
}
}
},
};
}在 vite.config.ts 中引入:
import { renameAdChunksPlugin } from "./plugins/renameAdChunks";
export default defineConfig(() => {
return {
plugins: [
// ... 其他插件
renameAdChunksPlugin(),
],
};
});思路
新增了一个独立的 Vite 插件文件 plugins/renameAdChunks.ts,使用 closeBundle 钩子,工作流程:
configResolved阶段记录outDir路径,避免硬编码。closeBundle阶段(所有文件已写入磁盘后)扫描dist/assets/目录,找出所有以ad.js结尾的文件。- 生成一个每次构建随机的两字母后缀(从不含
a/d的字母池中取样),将命中文件重命名。 - 遍历
assets/下所有.js文件和index.html,将旧文件名的每处字符串引用全部替换为新文件名,保证所有动态import路径一致。