ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Vue3 + Vite 中 require is not defined 报错:三种解决方案与避坑指南

Vue3 + Vite 中 require is not defined 报错:三种解决方案与避坑指南

1. 先搞清楚这个报错到底在闹什么脾气

require is not defined这行红字,但凡是从 Vue2 或者 Webpack 时代迁移到 Vue3 + Vite 的人,几乎都被它拦过一次路。我第一次在控制台看到它的时候,心里第一反应是"我明明装了依赖啊",第二反应才是"哦,Vite 里压根没有 require 这个东西"。这个报错不是你的代码写错了,而是运行环境换了底层逻辑——Vite 在开发阶段走的是原生 ESM(ES Module),浏览器直接通过<script type="module">加载模块,而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 的defineAsyncComponent:

import { 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 或 fetch

require('./data.json')在 Vite 里直接改成:

import data from './data.json'

Vite 原生支持 JSON 导入,还支持具名导入(import { field } from './data.json')。如果是运行时才确定要读哪个文件,那就用fetch走网络请求,把 JSON 放到public目录下按 URL 取。我个人的经验是:构建时已知的配置用 import,运行时动态的用 fetch,别混着来。

注意:改完自己的代码后一定要全局搜一遍require(,包括.vue、.js、.ts文件,有时候它藏在某个工具函数里,不搜根本想不起来。

4. 钥匙二:搞定第三方依赖里的 CommonJS

4.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,要么把它挡在门外。把这三把钥匙的顺序记牢——先改自己、再调依赖、最后上插件——大部分场景都能顺利通关。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进