防止 vite 构建产物文件名概率触发广告拦截插件

缘起

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 钩子,工作流程:

  1. configResolved 阶段记录 outDir 路径,避免硬编码。
  2. closeBundle 阶段(所有文件已写入磁盘后)扫描 dist/assets/ 目录,找出所有以 ad.js 结尾的文件。
  3. 生成一个每次构建随机的两字母后缀(从不含 a/d 的字母池中取样),将命中文件重命名。
  4. 遍历 assets/ 下所有 .js 文件和 index.html,将旧文件名的每处字符串引用全部替换为新文件名,保证所有动态 import 路径一致。
最后修改:2026 年 04 月 24 日 05 : 46 PM

发表评论