编程 Vite 8 升级核对清单:CJS default 导入和 browser/module 嗅探会先坏

2026-10-29 20:04:53

Vite 8 升级核对清单:CJS default 导入和 browser/module 嗅探会先坏

信源是 Vite 官方《Migration from v7》(vite.dev/guide/migration,当前文档版本 v8.3.4)。Vite 8 用 Rolldown + Oxc 取代 esbuild + Rollup,官方基准给出的构建提速在 10–30x 量级;但升级真正花时间的是语义变化。下面按「哪类项目会先坏」排序,不按文档目录。

前提条件先确认:Node 要求 20.19+ / 22.12+(与 Vite 7 相同)。build.target 默认值随 'baseline-widely-available' 抬高到 chrome111 / edge111 / firefox114 / safari16.4,对齐 2026-01-01 的 Baseline Widely Available,都是约两年半前发布的浏览器。如果项目的 target 是从 Chrome 107 一路兼容上来的,产物会直接变。

1. 导入 CommonJS 包的项目:default 语义统一

这一类最先坏,而且坏得最隐蔽:以前「dev 正常、build 坏」的差异,现在会变成两边一致地坏或一致地好。

CJS 的 default 导入现在只有一套规则。满足任一条件时,default 就是 module.exports:

  • 导入方是 .mjs 或 .mts
  • 导入方最近的 package.json 的 type 为 module
  • 被导入 CJS 的 module.exports.__esModule 不为 true

否则 default 是 module.exports.default。

旧行为分两套:dev 下上述条件只对「被纳入依赖优化」的导入方成立;build 下走的是另一套判断——__esModule 不为 true 且 module.exports 没有 default 属性(假设 build.commonjsOptions.defaultIsModuleExports 为默认的 'auto')。所以那些只在某一种模式下成立的写法,会开始报 undefined is not a function。

  • 用 import pkg from 'cjs-pkg' 的地方逐个确认拿到的到底是 module.exports 还是 module.exports.default
  • 临时逃生口:legacy.inconsistentCjsInterop: true(已弃用),恢复旧的不一致行为
  • 同时向包作者反馈,参考 Rolldown 的「Ambiguous default import from CJS modules」文档

2. 依赖同时声明 browser 和 module 字段:内容嗅探没了

package.json 里同时有 browser 和 module 时,Vite 过去会按文件内容判断格式,并且倾向给浏览器挑 ESM 文件。现在这个启发式被移除,始终严格按 resolve.mainFields 的顺序解析。

症状是某些依赖在浏览器里解析到了 CJS 版本,或直接解析失败。

  • 先看这些包的 mainFields 结果是否符合预期
  • 绕过用 resolve.alias 直接映射到具体文件
  • 或者用 patch-package / pnpm patch 给依赖打补丁

3. 用了 manualChunks 或 watch.chokidar 的构建配置:直接失效

  • output.manualChunks 的对象形式已被移除,函数形式弃用;改用 Rolldown 的 codeSplitting
  • build.rollupOptions.watch.chokidar 移除,改用 build.rolldownOptions.watch.watcher

这两项属于配置层面直接报错或静默不生效,打包分块策略会整体变形,需要重新核对产物体积。

4. 配置改名与自动转换:能自动转的,和转不了的

大部分旧字段仍能用,但都已弃用并自动转换。核对方式是在 configResolved 里打印转换结果:

export default {
  plugins: [{
    name: 'print-config',
    configResolved(config) {
      console.log(config.optimizeDeps.rolldownOptions)
    },
  }],
}

改名映射:

旧新
build.rollupOptionsbuild.rolldownOptions
worker.rollupOptionsworker.rolldownOptions
optimizeDeps.esbuildOptionsoptimizeDeps.rolldownOptions
esbuildoxc

optimizeDeps.esbuildOptions 的自动转换:minify→output.minify、treeShaking→treeshake、define→transform.define、loader→moduleTypes、preserveSymlinks→!resolve.symlinks、resolveExtensions→resolve.extensions、mainFields→resolve.mainFields、conditions→resolve.conditionNames、keepNames→output.keepNames、platform→platform、plugins→plugins(部分支持)。

esbuild 选项转 Oxc 的对应关系:jsxInject→oxc.jsxInject、include/exclude 保留、jsx: 'preserve'→oxc.jsx: 'preserve'、'automatic'→{ runtime: 'automatic' }、jsxImportSource→oxc.jsx.importSource、'transform'→{ runtime: 'classic' }、jsxFactory→jsx.pragma、jsxFragment→jsx.pragmaFrag、jsxDev→jsx.development、jsxSideEffects→jsx.pure、define→oxc.define。

没有等价物、必须自己处理的几项:

  • esbuild.banner / esbuild.footer:要自己写一个在 transform hook 里拼内容的插件
  • esbuild.supported:Oxc 不支持(oxc-project/oxc#15373)
  • 原生装饰器无法降级(oxc-project/oxc#9170):需要 @rolldown/plugin-babel + @babel/plugin-proposal-decorators,或用 @rollup/plugin-swc 变通
  • minify 的属性 mangle 系列(mangleProps、reserveProps、mangleQuoted、mangleCache):Oxc 不支持(oxc-project/oxc#15375)

另外 esbuild 已不是 Vite 的直接依赖,变成可选依赖。用了 transformWithEsbuild 的插件要自己把 esbuild 装进 devDependencies;该函数已弃用,改用 transformWithOxc。

压缩相关:JS 压缩交给 Oxc Minifier,可用已弃用的 build.minify: 'esbuild' 回退(同样要自己装 esbuild)。esbuild.minify* 改为 build.rolldownOptions.output.minify,esbuild.drop 改为 output.minify.compress.drop*。CSS 压缩默认 Lightning CSS,可用 build.cssMinify: 'esbuild' 回退(需装 esbuild);Lightning CSS 的语法降级更好,CSS 体积可能略增。

5. 运行时行为变化:报错形态、HMR、import.meta.url

  • build() 抛 BundleError(只影响 JS API 用户)。类型是 Error & { errors?: RolldownError[] },单个错误在 errors 数组里:
try {
  await build()
} catch (e) {
  if (e.errors) {
    for (const error of e.errors) console.log(error.code)
  }
}
  • import.meta.hot.accept 不再接受 URL(NRV),要改传 id(rolldown-vite#21382)
  • UMD / IIFE 里的 import.meta.url 不再 polyfill,默认替换为 undefined;要恢复旧行为用 define 配合 build.rolldownOptions.output.intro
  • 外部化模块的 require 保持为 require,不再转成 import 以保持语义。要转成 import 用 Rolldown 内置的 esmExternalRequirePlugin(从 vite 再导出):
import { defineConfig, esmExternalRequirePlugin } from 'vite'

export default defineConfig({
  plugins: [
    esmExternalRequirePlugin({ external: ['react', 'vue', /^node:/] }),
  ],
})

6. 插件作者:hook 顺序、bundle 对象、moduleType

  • Rollup 的并行 hook 在 Rolldown 里顺序执行,原来靠并发省时间的插件会变慢,也可能暴露隐式顺序依赖
  • bundle 对象不能再给 bundle[foo] 赋值,改用 this.emitFile();引用不跨 hook 共享
  • structuredClone(bundle) 会报 DataCloneError,要写 structuredClone({ ...bundle })(rolldown-vite#128)
  • 在 load / transform 里把非 JS 内容转成 JS 时,返回值可能需要加 moduleType: 'js'(Rolldown 按解析后 id 的扩展名自动设 module type)
  • plugin-legacy 不再支持降到 ES5 及以下(rolldown-vite#452)
  • Rolldown 不支持、Vite 也随之不支持:output.format 的 'system'(rolldown#2387)与 'amd'(rolldown#2528),以及 shouldTransformCachedModule(rolldown#4389)、resolveImportMeta(rolldown#1010)、renderDynamicImport(rolldown#4532)、resolveFileUrl

7. 少数场景才会踩到的

  • Extglobs 暂不支持(rolldown-vite#365)
  • TypeScript legacy namespace 只部分支持
  • define 传对象时不再共享引用,每个变量各自一份副本
  • "use strict"; 有时不再注入
  • 给 build.target 传同一浏览器的多个版本现在直接报错(esbuild 时期会挑最新版)
  • 注释在 renderChunk 之前就被移除(Rollup 是之后);除列出的类型外其它注释会被移动,而 Rollup 只在相邻代码被移除时才删注释
  • parseAst / parseAstAsync 弃用,改用 parseSync / parse
  • 其它弃用:build.commonjsOptions 变成 no-op;build.dynamicImportVarsOptions.warnOnError 变成 no-op;resolve.alias[].customResolver 弃用,改用带 resolveId hook 且 enforce: 'pre' 的插件

8. 渐进迁移路径

rolldown-vite 包实现的是「Vite 7 + Rolldown」,不含 Vite 8 的其它改动,适合先在 Vite 7 上把 Rolldown 的问题隔离出来:

{ "vite": "npm:rolldown-vite@7.2.2" }

确认无问题后再升 8,改回:

{ "vite": "^8.0.0" }

如果是从 rolldown-vite 起跳,只有标题带 NRV 的小节适用(本文对应第 1 节的 HMR accept、build.target 默认值、移除的弃用特性)。

安装体积上,官方说明 Vite 8 比 Vite 7 大约多 15MB,其中 lightningcss 约 10MB、Rolldown 约 5MB。

复制全文 生成海报 Vite Rolldown 前端构建 升级迁移 Vue

推荐文章

程序员茄子在线接单