前几天帮一个同事排查 Vue3 + Vite 项目的时候,遇到了一个相当典型的坑:本地开发npm run dev一切正常,页面访问、热更新都好好的,结果一到 CI 里执行npm run build,Rollup 就开始刷红报错。控制台最核心的一行大概是这样的:
Rollup failed to resolve import "/@/layout/index.vue" from "src/main.js".后面再跟一串error during build的堆栈。更折磨人的是,这段代码在本地跑了一天都正常,部署却连第一步都过不去。其实这已经不是第一次看到import "/@/XXX"导致 build 失败的案例了,尤其是从早期 Vite 项目、老模板项目接手过来,或者团队里有人把 dev 工具里看到的路径直接抄进了源码,都会踩中。
这篇文章把这个问题从根上拆一遍:为什么 dev 模式能用、build 模式就挂?配置应该怎么写?存量代码怎么快速清理?最后再给一份排查速查表。刚接触 Vite 的新手可以照着抄,遇到同样问题的老手也能再核对一遍有没有漏掉的盲区。
1. 先看清楚问题现场:开发一切正常,构建一跑就翻车
1.1 典型报错长什么样
这类问题在高发期的表现高度统一,通常不是单个文件报错,而是一旦 Rollup 处理到某个模块图,就会连锁爆出一批“resolve 失败”。我见过最多的两种写法如下。
第一种,业务代码里直接写了/@/开头的路径:
Rollup failed to resolve import "/@/components/HelloWorld.vue" from "src/App.vue". This is most likely unintended because it can break your application at runtime. If you do intend to import this module, set `resolve.alias` in `vite.config.js` to make it work.第二种,项目里约定使用@/作为别名,但vite.config.js里压根没配resolve.alias,于是构建时同样报:
Rollup failed to resolve import "@/utils/request" from "src/api/user.js".有些项目还会在错误列表里夹带一些“周边伤害”,比如某个组件引用的图片、样式资源路径也解析失败,报出failed to resolve import "../assets/grenade.png"这样的提示。表面看是资源路径问题,实际上根源往往还是同一个:项目没有一个统一、可靠的路径解析规则,各种写法混在一起,开发模式把这些问题都“惯着”了,到构建阶段集中爆发。
不管是哪种报错,最终都会让npm run build直接退出,CI/CD 流水线的构建步骤当场失败,部署动作被迫中止。如果你遇到过类似现象,而且只是把这批 import 改成相对路径后勉强通过,但没搞懂原因,那我建议你继续往下看。
1.2 为什么会同一个 import,两种结果
核心原因一句话就能说清:Vite 开发模式和生产构建用了两套底层的模块解析机制。
开发模式跑的是 Dev Server,它基于浏览器原生 ESM,按需加载模块。当浏览器请求/src/main.ts时,Dev Server 会拦截请求,在内存中对模块做转换,再返回给浏览器。这个过程里,Dev Server 自己有一层“路径重写”和“路径兜底”的逻辑,很多不规范的写法会被它顺手修正,所以页面能跑通。
构建模式则完全不同。vite build的主体打包工作交给 Rollup 完成,Rollup 需要扫描整个模块依赖图,把每个 import 都解析成明确的文件路径,然后做 tree-shaking、合并、压缩。这个解析过程没有 Dev Server 那种“浏览器请求进来再动态处理”的容错层,它依赖的是 Vite 插件体系里内置的路径解析规则,以及你在vite.config.js里明确声明的 alias。找不到就是找不到,直接报错、直接中断。
打个比方,开发模式像是小区门口的保安,看见你拿的快递单上写了个“王姐”,他能靠人脸和记忆帮你送到户;构建模式则是快递分拣中心的机器,必须读到标准门牌号,否则直接丢进“无此地址”的筐里。所以“开发正常、构建失败”并不是什么玄学,只是两套系统对同一串字符的处理标准不一样。
2. 根因剖析:开发服务器和 Rollup 用的是两套解析逻辑
2.1 Vite dev server 的路径映射和内部前缀
要理解为什么有人会把/@/写进代码,还得看看 Vite 在开发模式下到底做了什么。
Vite 的 Dev Server 在处理模块请求时,会用到一些内部 URL 前缀,比如/@fs/、/@id/、/@vite/等。这些前缀是用来区分模块来源的:文件系统模块、依赖模块、内部客户端模块。在早期 Vite 版本以及不少模板项目里,/@/也被当作一种映射源码根目录的快捷写法,浏览器里甚至能直接看到类似import xxx from '/@/src/components/xxx.vue'的请求。很多开发者看到这个路径,第一反应就是“这应该是 Vite 提供的默认别名”,于是顺手就复制进了业务代码。
但这里有个致命误区:/@/是开发服务器内部 URL 的呈现方式,不是官方暴露给业务代码写 import 时使用的别名。官方推荐的别名定义方式是resolve.alias,而 dev server 之所以能容忍/@/这种写法,是因为它有能力在请求到达时做二次解析和重写。这个过程对使用者是透明的,于是造成了“这么写能跑”的错觉。
更要命的是,这种错觉很难在开发阶段被识别出来。你本地点开页面,所有模块都加载正常,甚至 Network 面板里看到的请求路径也“有模有样”。直到某一天切到构建部署,Rollup 用另一种标准重新审视这些 import 时,问题才像抽丝一样冒出来。
2.2 Rollup 构建阶段怎么解析 import
如果你在vite.config.js里没配任何 alias,Rollup 处理 import 时大致遵循这样的查找顺序:
- 相对路径:以
./或../开头,直接基于当前文件所在目录去解析。 - 绝对路径:以
/开头,会被当成文件系统根目录下的路径去解析。 - 裸路径(bare import):比如
vue、axios,会去node_modules里找。 - 别名(alias):根据
resolve.alias配置的 key 做前缀匹配替换后再解析。
现在再看import "/@/layout/index.vue"这行代码:它既不是相对路径,也不是 node_modules 依赖,而是以/开头的绝对路径。Rollup 会老老实实去项目根的/@/layout/index.vue找文件。项目根目录下自然不会有一个名叫@的文件夹,于是解析失败,抛出Rollup failed to resolve import。
有人可能会问:那如果我恰好在项目根目录建了一个@文件夹呢?理论上确实能解析到,但你把源码放在一个叫@的目录里,本身就是在给自己埋雷,后续的同事看到代码会更困惑。正确的做法不是“让 Rollup 凑合着能跑”,而是把业务代码里的 import 改成既满足开发、又满足构建的标准写法。
2.3 看清 @、/@/、相对路径的真实区别
很多人分不清@/和/@/,这里直接用一张表把它们的行为差异摆出来。
| 写法 | 开发模式行为 | 生产构建行为 | 是否推荐 |
|---|---|---|---|
import x from '/@/views/a.vue' | 依赖 Dev Server 路径重写,部分版本/场景能跑 | Rollup 不会重写,大概率解析失败 | 不推荐 |
import x from '@/views/a.vue' | 需要resolve.alias配置后生效 | 需要resolve.alias配置后生效 | 推荐,前提是配置好 alias |
import x from '../../views/a.vue' | 相对路径,可正常解析 | 相对路径,可正常解析 | 能用,但层级深时维护成本高 |
import x from 'vue' | 自动去 node_modules 查找 | 自动去 node_modules 查找 | 推荐,官方依赖就用裸路径 |
关键结论:无论开发还是构建,@/本身都不是 Vite 内置的默认别名。很多人是从 vue-cli(webpack)项目迁过来的,webpack 配置里通常会默认把@指向src,于是想当然以为 Vite 也自带这个约定。实际上 Vite 需要你在配置里明确声明后才认识@。这也是为什么迁移项目时,第一波Rollup failed to resolve import "@/..."报错特别密集。
3. 核心修复方案:三处配置一步到位
3.1 最关键一步:vite.config.js 里的 resolve.alias
修复第一步,也是最核心的一步,是在vite.config.js里配置resolve.alias。推荐写法如下:
import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } } })这里有两个点值得说一下。
第一,为什么要用fileURLToPath(new URL('./src', import.meta.url))?因为很多 Vite 项目开了"type": "module",Vite 的配置文件本身是用 ESM 加载的,ESM 环境下没有__dirname这个 CommonJS 变量。如果你是从网上抄了path.resolve(__dirname, 'src'),在 ESM 项目里直接会报__dirname is not defined。new URL('./src', import.meta.url)拿到的是模块的 URL,再用fileURLToPath转成本地文件路径,既跨平台又规避了 ESM 的坑。
第二,如果你确实有很多历史代码用了/@/,想先让项目恢复构建,可以临时加一条兼容映射:
alias: { '@': fileURLToPath(new URL('./src', import.meta.url)), '/@/': fileURLToPath(new URL('./src', import.meta.url)) }注意,这只是饮鸩止渴的临时方案。/@/是 Vite 内部协议的保留前缀,你把它强行改写成业务别名,不仅可能和 Dev Server 的某些内部请求发生冲突,还会让项目长期维持一种“不标准”的状态。我建议你把它当成有期限的过渡方案,后续还是要把代码里的/@/逐步替换成@/,最后把这条兼容映射删掉,让项目回到干净状态。
配置改完之后,一定要重启npm run dev和重新执行npm run build。vite.config.js属于配置文件,Vite 启动时才会加载,热更新不会自动帮你应用修改。
3.2 TypeScript 项目还要同步 tsconfig.json
如果你的项目用了 TypeScript,只改 Vite 配置还不够。Vite 的resolve.alias只负责打包阶段的路径解析,编辑器(VSCode)的智能提示、跳转定义,以及vue-tsc类型检查依赖的却是tsconfig.json里的paths配置。
如果只配了 Vite 不配 tsconfig,你会遇到“构建能过了,但编辑器全部飘红、npm run type-check报一堆Cannot find module '@/xxx'”的尴尬情况。
需要在tsconfig.json的compilerOptions里加上这两项:
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } } }baseUrl告诉 TypeScript“相对路径的基准目录在哪”,paths则定义了类似 Vite alias 的路径映射。两者必须配合使用,而且为了让 Vite 和 TypeScript 的解析结果一致,这里的"@/*": ["src/*"]要和vite.config.js里的alias: { ... }保持同一个映射关系。
有些项目严格模式下可能不允许baseUrl缺省,具体报错可以按提示补上;如果拆了子工程、用了 monorepo,可能还需要在vue-tsc的命令里显式传入合适的tsconfig。这些属于进阶场景,基础用法下面这套已经足够应对绝大多数项目了。
3.3 全局清理代码里的 /@/ 和不规范依赖
配置补完之后,接下来就是清理存量代码。这一步最忌讳“肉眼找文件手动改”,文件一多必然漏。我建议用全局搜索先把问题收敛出来。
在项目根目录执行:
grep -rn "/@/" src --include="*.vue" --include="*.ts" --include="*.js" --include="*.tsx" --include="*.jsx"把任务拉到终端里逐条看,确认每一处都是你想替换的/@/之后,再做全局替换。我一般优先用 IDE 的全局搜索替换功能,比如 VSCode 里按Ctrl+Shift+H,搜'/@/替换成'@/,操作前先备份或提交一次代码,防止误伤。
如果在 Linux/macOS 上习惯用 sed,也可以这样:
sed -i "s#/@/#@/#g" $(grep -rl "/@/" src)但这行命令的缺陷也很明显:macOS 自带 sed 的-i参数写法跟 GNU sed 不同,而且grep -rl的返回结果如果包含大量文件,还可能超出命令行长度限制。所以不是特别推荐,除非你已经很清楚自己平台的 sed 行为。
替换之后,不是跑一次 build 就完事了,还要额外检查两类容易被忽略的文件。
一类是 CSS、SCSS 里的路径。比如某个.vue文件的<style>块里写了background: url('/@/assets/xxx.png'),这种写法同样存在风险。虽然有些版本的 Vite 会尝试解析 CSS 里的 alias,但为了稳妥,建议 CSS 里的资源路径使用相对路径,或者把静态图片放到public目录后用绝对路径引用。
另一类是new URL这种动态资源写法。如果你的代码里有:
const imgUrl = new URL('/@/assets/logo.png', import.meta.url)也需要改成new URL('../assets/logo.png', import.meta.url)或new URL('@/assets/logo.png', import.meta.url)这类的标准形式。Vite 对new URL(..., import.meta.url)有专门的静态分析支持,只要路径可被识别,构建时会自动产出版本化后的资源地址。
4. 实操记录:从最小复现到构建成功
4.1 最小复现案例,先让报错稳定出现
与其在大型项目里边查边改,不如先在一个最小的项目里把问题复现出来,确认自己理解对了,再回头处理仓库。我重构这个问题的完整流程可以拿来参考。
先用 Vite 官方脚手架创建项目:
npm create vite@latest test-alias -- --template vue cd test-alias npm install然后故意不配置resolve.alias,直接去src/App.vue顶部加一行不规范的导入:
import HelloWorld from '/@/components/HelloWorld.vue'接着执行:
npm run build这时候控制台就会稳定复现相似报错。如果你把'/@/components/HelloWorld.vue'改成'@/components/HelloWorld.vue',在没配置 alias 的情况下执行 build,则会看到另一类Rollup failed to resolve import "@/components/HelloWorld.vue"的报错。
这个最小案例很有价值,它能让你在动手改仓库之前,先搞清楚自己项目到底属于哪一种:是用了不该用的/@/,还是只是漏配了@。两种问题对应同样的配置修复,但清理思路稍有不同。
4.2 按步骤修复并完成验证
我实际修复一个项目时的顺序如下,你可以直接抄。
第一步,先把vite.config.js的 alias 配置写好,代码就是 3.1 里的那一段,然后重启一次npm run dev,确认开发模式仍然正常。这时候大概率不会立刻暴露问题,因为很多错误 import 在 dev 下也被兜住了。
第二步,全局搜索并清理/@/,统一成@/。如果项目较大,建议提交一个独立的 commit,方便 review 和回滚。
第三步,重新执行npm run build,观察是否还有残留的 resolve 报错。如果还有,逐个查看错误信息里的from "xxx",去对应源文件检查 import 语句。
第四步,执行npm run preview启动产物预览,或者直接把 dist 目录丢到本地静态服务器里访问一遍,确认页面能正常打开、路由切换无刷新、图片样式没挂。
第五步,回到真实部署环境或者 CI 里再跑一次构建。这一步容易漏,千万不要因为本地 build 通过就以为 CI 一定没问题。CI 环境往往没有本地那份 node_modules,某些问题只有在干净环境里构建才会暴露。
4.3 动态导入、路由懒加载和库模式的变体
清理 alias 问题时,只关注静态import是不够的,动态导入和路由懒加载同样受resolve.alias影响。
Vue Router 里很常见的写法:
const routes = [ { path: '/home', component: () => import('@/views/Home.vue') } ]这段代码在 alias 配置正确的前提下可以正常构建,Rollup 能把动态 import 中的字符串解析出来。但如果你把路径拼成了变量,问题就会升级:
// 这种动态拼接,Rollup 无法静态分析,容易解析失败 const loadPage = (name) => import(`@/views/${name}.vue`)Rollup 面对模板字符串形式的动态导入,只能尽量做模式匹配,一旦匹配不上就会出现更难排查的构建错误。Vite 官方对这种场景给出的方案是import.meta.glob:
const modules = import.meta.glob('../views/*.vue')然后用 modules 里的 key 和 value 去组织加载逻辑。import.meta.glob会把匹配到的模块全部收集起来,构建时生成对应的代码映射,既解决了动态导入无法静态分析的问题,也天然绕开了 alias 解析的不确定性。
另外,如果你是在用 Vite 的库模式(build.lib)开发组件库,也别忽略这个配置。库模式同样走 Rollup 打包,alias 没配好,依赖路径解析失败的结果一模一样。好在我见过的库模式项目里,大家更倾向于用相对路径导入内部模块,因为组件库发布后要面对不同的消费环境,过度依赖别名反而会增加使用者的配置成本。
5. 常见问题与排查技巧实录
5.1 高频报错速查表
把我在实际排查中遇到的高频问题和对应策略整理成一张表,方便你遇到信号时直接对号入座。
| 报错现象 | 可能原因 | 解决方案 |
|---|---|---|
Rollup failed to resolve import "/@/xxx" | 业务代码误用了 Dev Server 内部前缀 | 全局替换为@/,并配置 res.alias |
Rollup failed to resolve import "@/xxx" | 没有配置resolve.alias | 在 vite.config.js 设置@指向 src |
| 编辑器飘红但 build 正常 | 只配了 Vite alias,没配 tsconfig paths | 在 tsconfig.json 增加 baseUrl 和 paths |
| build 正常,运行时资源 404 | base配置不符或new URL用法不对 | 根据部署子路径配置base,检查动态资源写法 |
| monorepo 子包引入后解析异常 | alias 目标指向了包内 src,但 resolve 条件不匹配 | 调整 alias 到包的入口文件,必要时配置resolve.dedupe |
| 图片等静态资源构建报错 | 资源路径用了/@/或错误的绝对路径 | 改成相对路径或使用 public 目录 |
最后一行想多说一句:图片资源报错非常常见,很多人会把它当成独立问题处理,其实很多时候只要 alias 逻辑理顺了,资源路径跟着一起就顺了。所以排查时不要只看报错那一行,要往上游找是“谁引入了这个资源”。
5.2 按报错关键字顺藤摸瓜的排查方法
遇到这类构建报错,我的排查顺序基本是固定的。
首先,区分错误来源。Vite 的构建报错通常会标明是Rollup failed to resolve import,还是Plugin相关错误。前者本质上是路径解析问题,后者往往要结合具体插件去分析。别把两者混在一起查。
其次,盯住from "xxx"。行首的import是要导入的目标,行尾的from是“谁触发这个导入”。报错信息一般都会标明从哪个源文件发起,比如:
Rollup failed to resolve import "@/utils/request" from "src/api/user.js"这句话的意思是src/api/user.js引用了@/utils/request,而当前配置无法解析这个路径。回到源码去检查这一行的上下文,八成能定位问题。
如果报错没有明确指明 from 文件,我建议临时开一下 Vite 的构建调试信息:
vite build --debug它会输出大量模块解析过程的日志,能看到每个 import 的解析尝试和最终失败点。在大型项目里日志噪音很大,但配合过滤关键字,往往能逼出隐藏的路径问题。
5.3 从源头避免这类问题的小习惯
排查解决只是治标,我更想分享一下治本的习惯。
我在新项目初始化时,第一件事就是把resolve.alias和tsconfig.json的paths一起配好,然后在项目 README 或者团队文档里明确规定:源码内所有相对业务模块的导入,一律使用@/开头;/@/这种历史写法不许再出现;跨层级超过两层的相对路径在 Code Review 阶段就会被要求改成别名。
第二,给 ESLint 加上 import 路径相关的规则。虽然 ESLint 官方没有直接校验 alias 的规则,但可以通过eslint-import-resolver-alias配合import/resolver配置,让 lint 阶段就能发现解析不了的路径。这样团队成员不用等 build 失败,写代码的当下就会看到 warning。
第三,定期做一次构建“体检”。哪怕本地没改任何东西,也可以在 CI 里固定跑一次干净环境的npm run build。很多路径问题都是积攒到某天才突然爆发的,与其等部署时的那个报错电话,不如让构建流水线每天都替你盯一遍。
结尾
最后聊一点我自己的习惯。这个 alias 问题解决过太多次以后,我反而越来越喜欢看报错本身了。Rollup failed to resolve import这类信息虽然刺眼,但它非常诚实,它会直接告诉你哪个文件、哪一行、引用了什么路径是解析不了的。与其怕报错,不如把它当成免费的“体检报告”,遇到一次就顺手把周围相关的历史写法也扫一遍,治一个坑往往能避免后面一连串坑。
如果你手头正被 Vite 构建问题卡住,建议按文章第 3 节的配置检查一遍,再用第 4 节的流程做一个最小复现,基本都能在半小时内把项目恢复到可构建状态。配置一次到位之后,再回头看看那些从 webpack 项目带过来的路径习惯,你会发现 Vite 的这套规则其实比想象中简单,它只是要求你在配置文件里把话说明白而已。