Vue3 + Vite 中 require is not defined 报错:三种解决方案与避坑指南
1. 先搞清楚这个报错到底在闹什么脾气require is not defined这行红字但凡是从 Vue2 或者 Webpack 时代迁移到 Vue3 Vite 的人几乎都被它拦过一次路。我第一次在控制台看到它的时候心里第一反应是我明明装了依赖啊第二反应才是哦Vite 里压根没有 require 这个东西。这个报错不是你的代码写错了而是运行环境换了底层逻辑——Vite 在开发阶段走的是原生 ESMES Module浏览器直接通过script typemodule加载模块而require是 CommonJS 的语法浏览器根本不认识它。所以只要你的代码里、或者你引用的某个第三方包里残留了require(...)构建工具就会当场翻脸。这篇文章想解决的就是这一类问题在 Vue3 Vite 的项目里遇到require is not defined该怎么定位、怎么改、怎么防。适合的人群很明确——刚从 Vue2/Webpack 迁过来的开发者、在 Vite 项目里引用了老牌 CommonJS 库的同学、以及被这个报错卡住但不想无脑降级回 Webpack 的朋友。我会把三种最实用的破局钥匙拆开讲透每一种都告诉你为什么这么选、具体怎么落地、以及我踩过的坑。看完你至少能做到看到这个报错不再慌能快速判断它来自自己的代码还是依赖包然后对症下药。先说清楚一个前提认知这是很多人绕不过去的坎Vite 不是 Webpack 的升级版它是另一套思路。Webpack 在构建时会把 CommonJS 和 ESM 都处理掉所以require在 Webpack 项目里能跑Vite 开发时不做打包直接把源码当 ESM 丢给浏览器生产构建才用 Rollup。这个差异决定了require在开发阶段必然报错。理解了这一点后面的所有方案你都能自己想明白。2. 三把破局钥匙的总览与选型逻辑在动手之前我习惯先把问题分类因为不同来源的require要用不同的钥匙用错了就是白费功夫。实际项目里这个报错通常来自三个地方一是你自己写的业务代码里用了require比如动态引入图片、加载 JSON二是你引入的第三方依赖内部用了require三是构建配置或插件层面需要做兼容处理。对应地我总结了三把钥匙。钥匙适用场景核心手段改动成本推荐优先级钥匙一语法替换自己代码里的 require改用 ESM 的 import / import.meta.glob低最高钥匙二依赖预构建与别名第三方 CommonJS 包optimizeDeps 按需转换中中钥匙三插件兜底转换顽固的老库、动态 require引入转换插件做 CJS 兼容中高兜底选型逻辑其实很简单能改自己代码就绝不动依赖能靠 Vite 原生能力解决就不引插件。因为每多一层转换构建速度和可维护性就多一分损耗。我见过不少项目一上来就装一堆兼容插件结果构建时间翻倍最后发现真正需要转换的只有一个库里的两行require。所以顺序永远是先排查自己的代码再处理依赖最后才上插件兜底。这里要特别提醒一句很多人看到报错第一反应是去搜Vite 支持 require 的配置然后找到一个define: { require: ... }之类的偏方就往里塞。这类做法在简单场景下可能看起来能跑但本质是给全局注入了一个假的require遇到真正的模块解析、路径处理就会露馅属于治标不治本。我个人的原则是不伪造 require而是把 require 消灭掉或者正确转换掉。下面三把钥匙都是围绕这个原则展开的。3. 钥匙一把自己代码里的 require 彻底换成 ESM 写法3.1 为什么自己的代码必须优先改自己写的代码是可控的改起来最干净也最不容易留隐患。Vue2 时代很多人习惯用require做动态引入比如根据变量拼路径加载组件或图片这些写法在 Vite 里全部失效。与其想办法让require复活不如直接拥抱 ESM因为 Vite 对 ESM 的支持是最原生的改完之后不仅不报错还能享受 tree-shaking 和更快的热更新。我遇到最多的三个场景是动态引入图片资源、动态加载组件、读取本地 JSON。下面逐个给替换方案都是可以直接抄的。3.2 动态引入图片用 import.meta.glob 替代 require老写法通常长这样const getImage (name) require(/assets/images/${name}.png)在 Vite 里正确姿势是用import.meta.glob。它会在构建时把匹配到的文件都登记下来返回一个路径到加载函数的映射const imageModules import.meta.glob(/assets/images/*.png, { eager: true }) const getImage (name) { const path /src/assets/images/${name}.png return imageModules[path]?.default }这里有个关键参数eager。不加它返回的是返回 Promise 的懒加载函数适合大图按需加载加了eager: true会同步引入适合小图标这种要立即用的场景。我一般图标类用 eager大图用懒加载兼顾首屏速度和开发便利。注意import.meta.glob的路径匹配是静态分析的不能用完全动态拼接的变量必须保留通配符部分这也是它和require最大的行为差异得提前适应。3.3 动态加载组件defineAsyncComponent 配合 glob组件动态加载的老写法const comp require(./components/${type}.vue).default换成 ESM 后配合 Vue3 的defineAsyncComponentimport { defineAsyncComponent } from vue const componentModules import.meta.glob(./components/*.vue) const loadComponent (type) { const loader componentModules[./components/${type}.vue] return defineAsyncComponent(loader) }这样写的好处是组件真正按需加载不会一股脑打进主包。实测下来首屏体积能明显下降尤其是那种组件库很大的后台项目。要注意import.meta.glob返回的 key 是相对于当前文件的路径拼接时路径前缀必须和 glob 模式严格一致差一个./都会取不到值这是新手最容易翻车的点。3.4 读取 JSON直接 import 或 fetchrequire(./data.json)在 Vite 里直接改成import data from ./data.jsonVite 原生支持 JSON 导入还支持具名导入import { field } from ./data.json。如果是运行时才确定要读哪个文件那就用fetch走网络请求把 JSON 放到public目录下按 URL 取。我个人的经验是构建时已知的配置用 import运行时动态的用 fetch别混着来。注意改完自己的代码后一定要全局搜一遍require(包括.vue、.js、.ts文件有时候它藏在某个工具函数里不搜根本想不起来。4. 钥匙二搞定第三方依赖里的 CommonJS4.1 先判断报错到底来自哪个包自己的代码改干净了还报错那八成是依赖的锅。定位方法很直接看控制台报错的堆栈找到报错文件路径通常在node_modules/某个包/...里。如果堆栈被压缩得看不清可以在vite.config.js里临时关掉预构建缓存或者用--debug启动看详细日志。我一般会先看报错文件属于哪个包再去那个包的package.json里看main、module、exports字段判断它是不是纯 CommonJS 包。判断标准很简单如果package.json里只有main指向一个.js文件没有module字段那它大概率是 CommonJS内部可能用了require。这类包在 Vite 里需要特殊照顾。4.2 用 optimizeDeps 让 Vite 提前转换Vite 的依赖预构建optimizeDeps本身就会用 esbuild 把 CommonJS 依赖转成 ESM大多数情况下它是自动生效的。但有些包因为入口判断、动态 require 等原因没被正确预构建就会漏网。这时候可以手动把它加进optimizeDeps.include// vite.config.js export default defineConfig({ optimizeDeps: { include: [some-cjs-package, another-old-lib] } })加进去之后Vite 会在启动时强制对这个包做 CJS 到 ESM 的转换。我实测过一个老的图表库加进 include 之后报错直接消失连插件都不用装。这个方案的成本最低应该作为处理依赖问题的第一选择。4.3 处理 exports 字段导致的解析失败有些包虽然提供了 ESM 入口但exports字段配置得比较刁钻Vite 解析时可能走到了 CommonJS 分支。这种情况可以在resolve.alias里手动指向它的 ESM 产物export default defineConfig({ resolve: { alias: { some-lib: some-lib/dist/some-lib.esm.js } } })具体指向哪个文件得去node_modules/some-lib/dist里翻找带.esm.js或.mjs后缀的。这个操作有点像手动指路告诉 Vite 别自己猜了走这条道。需要提醒的是别名指向的产物版本要和主包版本对应升级依赖后记得回来检查否则容易出现 API 对不上的诡异问题。4.4 依赖实在没有 ESM 版本怎么办如果翻遍dist都找不到 ESM 产物说明这个包就是纯 CJS 且没做兼容。这时候有两个选择一是找有没有社区维护的 ESM 替代包二是上第三把钥匙用插件转换。我的建议是优先找替代品因为纯 CJS 老包往往还伴随其他兼容问题换掉一劳永逸。实在换不掉再进下一节。5. 钥匙三用转换插件做最后的兜底5.1 什么情况下才需要插件插件是兜底手段不是首选。我给自己定的触发条件是依赖包纯 CJS 且无法替换、或者项目里有大量动态require无法静态改写、又或者构建产物里确实需要保留 CJS 兼容。满足这些再上插件否则就是过度工程。常见的做法是引入社区里的 CJS 转换插件在vite.config.js的plugins里配置让它针对特定文件或特定依赖做转换。配置时要注意作用范围别一股脑全量转换否则构建时间会肉眼可见地变长。5.2 插件配置的实操要点配置这类插件的核心是精准打击。一般会提供include/exclude选项把范围限定在出问题的那个包上import commonjs from some-cjs-plugin export default defineConfig({ plugins: [ commonjs({ include: /node_modules\/(some-cjs-package)\// }) ] })这里的关键是正则要写准include范围越小越好。我曾经图省事写成/node_modules/结果整个依赖树都被转换了一遍冷启动从 3 秒涨到 20 多秒热更新也变卡。后来收窄到具体包名速度立刻回来了。这个教训值得记一下转换插件的性能代价和匹配范围成正比。5.3 动态 require 的兼容处理有些库会在运行时根据变量拼路径做require这种连静态分析都救不了。插件通常能处理一部分但更稳妥的做法是看这个库有没有提供配置项让你传入模块映射或者干脆在业务层做一层适配把动态加载改成显式的 import 映射表。我处理过一个老的文件上传库它内部用require加载不同格式的解析器最后是在业务代码里预先 import 所有解析器再通过配置传给它绕开了动态 require。这种业务层适配虽然多写几行但比全局转换干净得多。提示上插件之前先确认 Vite 和插件版本匹配。不同大版本的插件 API 差异不小装错版本可能引入新的报错反而更难排查。6. 排查与避坑我踩过的那些坑6.1 常见问题速查表现象可能原因排查方向解决手段开发环境报 require is not defined自己代码或依赖用了 CJS看堆栈定位文件改 ESM 或加 optimizeDeps构建成功但运行时报错产物里残留 CJS检查构建日志和产物插件转换或换依赖加了 optimizeDeps 仍报错包有动态 require看包源码业务层适配或插件热更新后突然报错缓存未失效清 node_modules/.vite重启并清缓存别名指向后 API 报错版本不匹配核对 dist 产物版本对齐版本或换方案6.2 清缓存这件事别嫌烦Vite 的依赖预构建结果缓存在node_modules/.vite里改了optimizeDeps配置后如果不生效十有八九是缓存没更新。我的习惯是每次动完依赖相关配置先删掉这个目录再重启。虽然官方说配置变更会自动触发重新预构建但实际项目里因为锁文件、软链接等原因自动失效偶尔会失灵。手动清一下省得怀疑人生。6.3 别用 define 伪造 require前面提过网上有些偏方用define: { require: ... }给全局塞一个假 require。我强烈不建议。这种做法会让静态分析彻底失效打包工具不知道你到底依赖了什么tree-shaking 直接报废产物体积暴涨而且遇到路径解析、循环依赖时行为完全不可预期。短期看着能跑长期是技术债。正确的思路永远是让代码符合 ESM 规范而不是给旧语法打补丁。6.4 迁移期的渐进策略如果是大项目从 Vue2 迁过来别想着一次性把所有require改完。我的做法是按模块推进先把入口和公共工具改掉保证项目能跑起来再逐个模块替换每替换一个就跑一遍相关页面。这样出问题时范围可控回滚也容易。同时在新代码里立规矩code review 时看到require(直接打回从源头堵住。7. 我个人的几条实操心得第一报错先看堆栈别猜。require is not defined的堆栈里通常能直接看到是哪个文件哪一行顺着找比盲目搜配置快十倍。第二改依赖之前先确认它是不是真的需要改很多时候只是某个子依赖的问题用optimizeDeps.include点名处理就行别动大手术。第三import.meta.glob 的路径一定要用相对当前文件的写法用别名有时在 glob 里不生效稳妥起见用相对路径。第四转换插件是最后手段用了就要在项目文档里记一笔说明为什么用、范围是什么不然半年后接手的人看到一堆插件配置会一头雾水。这套东西我在几个迁移项目里反复用过从最初被这个报错卡半天到现在基本能在几分钟内定位并解决。核心就一句话Vite 的世界里没有 require 的位置要么把它翻译成 ESM要么把它挡在门外。把这三把钥匙的顺序记牢——先改自己、再调依赖、最后上插件——大部分场景都能顺利通关。