记一次离谱的 Vite 构建报错:The symbol "bem" has already been declared,真相竟是 Rollup 升级惹的祸!

106 阅读4分钟

最近在项目执行流水线时,突然遇到一个诡异的构建错误:

[vite:esbuild-transpile] Transform failed with 1 error:
assets/vant-!~{00b}~.js:289:6: ERROR: The symbol "bem" has already been declared

第一眼看到这个错误,我本能地以为是自己代码里重复定义了 bem 变量,或者是 Vant 组件库的问题。但排查一圈后发现——问题根源竟然是 rollup 包的版本升级导致的构建作用域异常!

本文将带你完整还原问题现场、分析根本原因,并提供可落地的解决方案,希望能帮助到同样被“幽灵报错”困扰的你。


🔍 一、问题现象

项目使用的是:

  • vite@5.4.1
  • vant@3.4.5

执行 npm run build 后(或 CI 环境重新安装依赖并构建),控制台报错:

[vite:esbuild-transpile] Transform failed with 1 error:
assets/vant-!~{00b}~.js:289:6: ERROR: The symbol "bem" has already been declared

奇怪的是:

  • 本地昨天还好好的;
  • 其他同事也遇到了同样的问题;
  • 错误文件名看起来像虚拟生成的(vant-!~{00b}~.js);
  • 搜索项目代码,根本没有手动定义过 bem

直觉告诉我:这不是代码问题,而是构建工具链的依赖出了问题


🕵️‍♂️ 二、初步排查方向

我首先怀疑以下几个常见原因:

排查项结果
是否自己定义了 const bem = ...❌ 没有
是否多次引入 Vant 组件?❌ 正常按需引入
清除 .vite 缓存是否解决?❌ 无效
升级 vite 和 vant 到最新版?❌ 仍报错

此时,我开始怀疑是不是 Vite 预构建(pre-bundling)阶段出错了


🔬 三、深入分析:从 node_modules/.vite 找线索

进入 node_modules/.vite 目录,找到报错文件 vant-!~{00b}~.js,定位到第 289 行附近,发现了这样的代码片段:

Js
深色版本
const bem = (block, element, modifier) => { /* ... */ };
// ... 中间省略 ...
const bem = (block, element, modifier) => { /* ... */ }; // ← 这里报错!

两个同名的 const bem 出现在同一作用域中,语法上确实非法!

但问题是:Vant 的源码中每个 bem 都是在独立模块中定义的啊,为什么会被合并成一个文件并暴露为同名变量?

这时我意识到:这已经不是 Vant 的问题,而是 打包器如何处理模块作用域 的问题 —— 而 Vite 生产构建用的是 Rollup


💡 四、真相大白:Rollup 版本升级引发的作用域 bug

运行以下命令查看当前项目中实际使用的 rollup 版本:

npm ls rollup

结果让我震惊:

my-project@0.1.0
└─┬ vite@5.4.1
  └── rollup@4.50.1    # ← 新版本!

而我之前稳定的环境用的是:

rollup@4.50.0

🔍 关键点来了

  • Rollup v4 引入了更激进的“静态提升”和“作用域分析”机制;
  • 在某些情况下(尤其是处理多个同名局部变量时),它会尝试复用变量名或将函数提升到外层作用域;
  • 而像 Vant 这样的 UI 库,多个组件内部都定义了一个私有的 bem() 函数,原本应保留在各自模块作用域内;
  • 但在新版本 Rollup 的优化逻辑下,这些 bem 被错误地合并到了同一作用域,导致 SyntaxError

🎯 结论
这不是语法错误,也不是代码错误,而是 Rollup v4 在特定场景下的作用域处理缺陷,影响了基于它的构建工具(如 Vite)对第三方库的正确打包。


✅ 五、解决方案

方案 1️⃣:锁定 Rollup 版本(临时推荐)

如果你使用的是 Yarn,在 package.json 中添加:

"resolutions": {
  "rollup": "4.50.0"
}

然后重新安装依赖:

yarn install

如果你使用的是 pnpm,创建 .pnpmfile.cjs 文件:

function readPackage(pkg) {
  if (pkg.name === 'rollup') {
    pkg.version = '4.50.0';
    delete pkg.dependencies;
    delete pkg.optionalDependencies;
    delete pkg.peerDependencies;
  }
  return pkg;
}

module.exports = { hooks: { readPackage } };

再运行 pnpm install

✅ 效果:强制所有依赖共用旧版 Rollup,避免作用域合并 bug。


方案 2️⃣:排除 Vant 预构建(临时 workaround)

vite.config.js 中配置:

Js
深色版本
export default defineConfig({
  optimizeDeps: {
    exclude: ['vant']
  }
})

这样 Vite 不会对 vant 做预构建,绕过 esbuild 处理环节。

⚠️ 缺点:可能影响启动速度,且治标不治本。


方案 3️⃣:等待官方修复或降级插件

关注 Rollup 官方仓库Vite 仓库,看是否有相关 issue 被修复。

同时检查哪些插件引入了高版本 Rollup:

npm ls rollup

如有必要,暂时降级或替换该插件。


🛡️ 六、如何预防此类问题?

  1. 锁版本很重要:使用 package-lock.json / yarn.lock / pnpm-lock.yaml 并提交到 Git;
  2. 谨慎升级插件:尤其是一些小众 Vite 插件,可能引入不稳定依赖;
  3. CI 环境保持一致性:确保本地与线上构建环境一致;
  4. 监控依赖树变化:可通过 npm ls rollup 等命令定期检查关键依赖版本。

📣 七、写在最后

这次问题看似只是一个简单的“变量重复声明”,实则牵扯出前端工程化中一个深层话题:构建工具链的稳定性与兼容性

我们常常只关注业务逻辑和框架升级,却忽略了底层工具(如 Rollup、Esbuild、Babel)的版本变动也可能带来毁灭性的影响。

🌟 经验总结一句话
当你遇到“莫名其妙”的构建报错时,别急着改代码,先看看是不是 依赖版本变了