
接手过一个跑了三年的Vue2项目技术栈是Vue CLI Webpack代码量不算小图片资源更是散落各处。最近决定往Vue3 Vite迁移本以为换掉构建工具只是配置改改的事结果刚把项目跑起来就开始怀疑人生npm run dev一切正常npm run build之后传到服务器首页图片全挂favicon也丢了控制台里一串404。排查到最后发现根子全出在public目录和路径配置上——Webpack和Vite对“public目录里的文件怎么引用”这件事的理解压根儿不是一个逻辑。这篇文章就把这次迁移踩过的坑拆开揉碎聊一聊。不管你是准备从Vue2迁到Vue3还是新项目刚上手Vite只要涉及public目录、静态资源、子路径部署这几点必须搞清楚。写这篇的定位很明确不扯原理大山全部来自实际项目里的配置和实践适合正在迁移老项目、或者部署时被资源路径折磨的团队参考。1. Webpack的public目录是“拷贝”Vite的public目录是“服务器根路径映射”1.1 两个工具对public目录的底层理解完全不同在Webpack项目里public目录的本质是“复制粘贴”。你用Vue CLI脚手架新建项目public/目录里的文件会被原样复制到dist/的根目录不经过任何loader处理不参与打包也不会有hash后缀。这个逻辑很直观像个搬运工把public文件夹整体搬到dist文件夹里就完事。Vite的public目录看起来也是这个作用官方文档写得也很像——public目录下的文件会被原样复制到构建输出目录的根目录。但实际使用时的行为差异很大在Vite项目中public目录更像是“服务器根路径的映射”。开发环境里public/logo.png直接等于http://localhost:5173/logo.png构建之后public/logo.png等于部署服务器根路径下的/logo.png。这一个细微的思维差异后面会引发一串连锁问题。我之前习惯性地在Webpack项目里这么写!-- Vue2 Webpack 项目里的老写法 -- img src/img/logo.png /然后public/img/logo.png放在那里一切安好。同样一行代码放进Vite项目本地开发也没问题但等你把base一改、部署到子路径下这行代码就变成指向服务器根路径的“死链”。1.2 能不能被模块系统处理是第一个分水岭Webpack项目中public目录下的文件有一个重要特点不能通过import引入。你在组件里写import logo from /public/img/logo.pngWebpack会试图把它当模块处理要么走file-loader要么走url-loader最后生成的文件会被重新命名、加上hash路径也会变。想要原样引用就必须用/img/logo.png这种绝对路径或者动态拼接process.env.BASE_URL。Vite这边也差不多public目录下的文件同样不能通过import方式引入。如果你试图import logo from /public/img/logo.pngVite会直接报错或者把它当成普通静态资源处理行为非常别扭。Vite的推荐做法是用根路径引用/img/logo.png或者用new URL(./img/logo.png, import.meta.url)这种方式把资源交给构建器处理。但这里有个关键区别需要重点划一下操作场景Webpack项目Vite项目public下文件的引用方式绝对路径/img/logo.png绝对路径/img/logo.png能否import public下的文件不能不能绝对路径会跟随publicPath/base变化吗index.html中会JS中不一定不会自动加base前缀原样保留建议放public的文件favicon、robots.txt、外部配置文件同上代码中资源走import相对路径会加hash并自动处理会加hash并自动处理最坑的就是标红的那一行Vite构建时代码里手写的/xxx.png这种绝对路径不会被加上base前缀。Webpack中你还能靠publicPath在某些场景下兜底Vite直接不惯着你写在哪就是哪。2. 迁移时最先炸掉的三个资源图片、favicon、CSS背景图2.1 图片资源少用“/”开头的绝对路径能import就import如果你只是从Vue2迁移到Vue3且构建工具还是Webpack那路径可能不会出大问题。但既然换成了Vite第一个要改的习惯就是把“资源全放public里、路径直接写死”的思路抛弃掉。我的建议很简单**项目内使用的图片、图标、小体积静态资源尽量放在src/assets目录下通过import或相对路径引入交给Vite做打包处理。**这样做的好处是Vite会给文件加hash解决缓存问题同时会依据base自动生成正确的路径前缀你根本不用关心最后部署到哪。只有这几种文件建议放publicfavicon、robots.txt、manifest.json这类入口文件需要被外部系统直接通过固定URL访问的文件比如第三方对接的xml、txt、html页面体积大且不频繁变动的二进制文件比如某些部署后还会手工替换的包打个比方public目录就像你家门口的公共邮箱谁都能用固定地址看到里面的东西src/assets里打包的文件就像快递柜里的包裹地址是动态分配的外人没法预知。能用快递柜的东西就别堆在公共邮箱门口。2.2 faviconindex.html里别再用死路径favicon是迁移时命中率最高的404资源。Vue2的Vue CLI项目里你大概率见过这种写法link relicon typeimage/png href% BASE_URL %favicon.icoVue CLI通过EJS模板把BASE_URL替换成publicPath的值。如果publicPath是/那结果就是/favicon.ico如果部署到子路径/admin/这个值会自动变成/admin/favicon.ico。换成Vite之后很多人直接改成link relicon typeimage/png href/favicon.ico本地开发没事因为dev server根路径就是项目根但一旦base设置为/admin/构建后的HTML里还是这个/favicon.ico浏览器会请求http://你的域名/favicon.ico404是必然的。Vite其实提供了和Webpack的BASE_URL位置相近的用法就是HTML环境变量替换。在index.html里可以这么写link relicon typeimage/png href%BASE_URL%favicon.ico%BASE_URL%会在构建时被替换成base配置的值注意Vite的base值会保证以斜杠开头和结尾这样不管是开发还是子路径部署favicon都能正确加载。2.3 CSS背景图绝对路径和相对路径待遇不同CSS里引用背景图也是重灾区。许多人喜欢在全局CSS里写.login-page { background-image: url(/img/login-bg.png); }在WebpackVue CLI里这个路径在构建时会被尝试解析成模块通常也能正确打包。但Vite对CSS的处理有一个我很早就发现的行为差异以/开头的绝对路径在构建时不会自动加base前缀而相对路径会被Vite当作静态资源处理构建后加上hash和base前缀。我实测过Vite 4的一个项目CSS里写url(/fonts/iconfont.woff2)构建后的CSS文件里原样保留了这个绝对路径改了base也纹丝不动改成url(../fonts/iconfont.woff2)之后构建产物里文件名带了hash路径也自动加上了base前缀和资源目录。所以CSS里面引用资源尽量用相对路径。这里说的相对路径是相对于CSS文件所在位置。如果你非要写绝对路径就得做好“永远挂在服务器根路径下”的心理准备。3. base配置Webpack的publicPath到Vite的base改一处动全身3.1 怎么改base才不影响本地开发Vite中控制所有资源路径前缀的核心配置是server.base也就是base。webpack那边对应的叫output.publicPath在Vue CLI里则是publicPath。两者职责接近给构建出的静态资源URL加统一前缀。默认情况下Vite的base是/所以本地开发完全不需要管它。但你要把项目部署到http://example.com/admin/这种子路径下就得改// vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ base: /admin/, // 注意首尾都要有斜杠 plugins: [vue()] })改完base之后构建产物的JS、CSS、图片等资源引用都会带上/admin/前缀。但你很快会发现光是改这个还不够——public目录里的资源引用、路由的history配置、nginx的转发规则每一环都得跟上这个下面实操章节细说。很多人在开发环境也会被base干扰明明base: /admin/是给生产环境用的本地一跑起来整个项目乱了。这里有个常见的经验做法是区分环境export default defineConfig(({ mode }) { const isProd mode production return { base: isProd ? /admin/ : /, } })开发时保持根路径构建时使用子路径两不耽误。3.2 三个“base”必须分开记别混淆迁移过程中我见过太多人把静态资源的base、路由的base、接口请求的baseURL混为一谈结果改了vite.config.js里的base发现接口也变了、路由也乱了最后全盘推翻重来。这里必须用一张表把三者的定位钉死名称所在位置控制范围典型值静态资源basevite.config.js中的baseHTML、JS、CSS中由构建器生成的资源路径前缀/admin/路由basecreateWebHistory()的第一个参数前端路由的history模式根路径/admin/接口baseURLaxios等请求库的baseURL所有HTTP请求的URL前缀/api或完整域名路由base的作用是告诉Vue Router“当前应用挂在哪个路径下”。你部署在/admin/路由的history模式就得用/admin/作为根基否则刷新页面时路由会直接404。而接口baseURL纯粹是请求层的拼接和静态资源、路由没有直接关系。有过一次惨痛经历我同事把vite.config.js里base配成了/api以为这样前端请求/api/user就顺理成章拿到数据了。结果构建后页面上的JS和CSS全变成了/api/assets/xxx.js页面整个白屏。改静态资源base的时候一定要意识到它会影响所有构建产物的加载路径不是单纯的“请求前缀”别拿它当代理用。3.3 别把public目录当成后端返回路径的替代品还有一种常见误区是把后端需要动态读取的配置文件塞进public目录里比如public/config.json然后前端代码用fetch(/config.json)去加载。开发时没问题但一旦部署到子路径这个请求就会打到域名根路径上去404没跑。如果只是需要一份配置文件正确的做法是在代码里拼接import.meta.env.BASE_URLconst config await fetch(${import.meta.env.BASE_URL}config.json).then(res res.json())import.meta.env.BASE_URL在开发环境默认是/构建时取的是vite.config.js里base的值。Vite官方保证这个值始终以斜杠开头和结尾拼接的时候不用顾虑多斜杠少斜杠的问题。另外提一句process.env.VUE_APP_XXX在Vue3 Vite里也别用了Vite自定义环境变量需要以VITE_开头访问方式改用import.meta.env.VITE_XXX。BASE_URL本身也是挂在import.meta.env下的算是迁移时最不起眼但最容易报错的差异点之一。4. 实操案例把Vue2后台管理系统迁移到Vue3部署到/admin/子路径4.1 迁移前先理清项目现状用我实际迁移的一个后台系统来说它原先是Vue2 Vue CLI 4登录后进入一个控制台图片资源一部分放在public/img/下一部分放在src/assets下。打包部署的路径打算改成/admin/因为服务器上这个域名下面还要跑另外一个项目。迁移前我先把现状盘了一遍核心文件有这些public/ ├── config.json ├── favicon.ico └── img/ ├── logo.png └── login-bg.png src/ ├── assets/ │ ├── icons/ │ └── avatar.png ├── router/index.js ├── views/ └── main.js之前Webpack项目里路由用的是createWebHistory()不传参数图片到处都是/img/logo.png这种死路径。要平滑迁移到Vite并部署到子路径下面几步缺一不可。4.2 six步迁移配置法可直接套用第一步vite.config.js里配置base和alias先安装vitejs/plugin-vue然后把基础配置写好。这里除了base还要把别名配好因为Vue CLI默认自带指向srcVite可不会自动给你加。import { defineConfig } from vite import vue from vitejs/plugin-vue import path from path export default defineConfig(() { const isProd process.env.NODE_ENV production return { base: isProd ? /admin/ : /, plugins: [vue()], resolve: { alias: { : path.resolve(__dirname, src) } } } })第二步路由base跟着改Vue Router 4的createWebHistory接收一个可选的base参数。最省心的写法是直接传入import.meta.env.BASE_URL这样它会自动同步vite.config.js里的base设置import { createRouter, createWebHistory } from vue-router const router createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes })这一步不配的话本地开发看不出问题一上线刷新/admin/login这种二级路由页面nginx会直接返回404因为服务器不知道应该把请求回退到index.html。第三步index.html里的资源引用换成%BASE_URL%回到favicon的例子!DOCTYPE html html langzh-CN head meta charsetUTF-8 / link relicon typeimage/png href%BASE_URL%favicon.ico / meta nameviewport contentwidthdevice-width, initial-scale1.0 / title后台管理系统/title /head body div idapp/div script typemodule src/src/main.js/script /body /html第四步组件和CSS里的死路径清理项目里搜一遍/img/、/assets/这种“根绝对路径”能改成import的就改掉。组件里动态拼接路径的统一用import.meta.env.BASE_URL拼接const logoUrl ${import.meta.env.BASE_URL}img/logo.pngCSS里的背景图改成相对路径或者挪到src/assets里用import引入。第五步接口请求baseURL单独设置这一步不归Vite管但很容易被连带误改。我的建议是axios实例单独维护部署环境用VITE_API_BASE这个环境变量控制# .env.production VITE_API_BASE/admin/apiconst request axios.create({ baseURL: import.meta.env.VITE_API_BASE || /api })注意这里VITE_API_BASE是给代理或后端网关用的跟Vite的base完全无关不要写进vite.config.js。第六步nginx配置配合这是最后一块拼图。子路径部署时nginx要保证两点一是/admin/下的请求能精准转发到前端二是history模式下没有匹配到物理文件的请求全部回退到index.html。配置示例location /admin/ { alias /var/www/admin/; try_files $uri $uri/ /admin/index.html; }这里用alias而不是root是因为root会把/admin/拼接到磁盘路径后面很容易出现/var/www/admin/admin/index.html这种多一层目录的问题。用alias能直接映射到部署目录。4.3 迁移时最容易翻车的几个配置点在实际操作里我发现很多人改完vite.config.js就急着打包结果往往在几个不起眼的地方翻车。第一个是base值首尾斜杠写成admin直接不行Vite要求要么以/开头结尾的绝对路径要么是./这种相对路径第二个是路由的history modeVue2迁移过来时容易把mode: history这套老写法照搬进Vue Router 4然后报错其实Vue Router 4里直接用createWebHistory就行第三个是nginx缓存改了静态资源后浏览器还是旧的CSS和JS排查时先做一次强制刷新或者让前端资源带hash。这里还要提醒一个困扰过我的点部署到子路径后vite preview本地预览的路径和服务器不一定一致。vite preview默认还是从/去访问如果你base配了/admin/构建产物用vite preview预览时直接访问http://localhost:4173/反而可能404得访问http://localhost:4173/admin/才行。第一次遇到时以为构建有问题排查了半天其实只是预览命令对base的处理方式不同。5. 常见路径问题速查表与排查思路5.1 一张表对清楚症状和处理办法迁移过程中我把自己踩过的坑整理成了一张速查表基本覆盖了public目录和路径配置的绝大多数问题问题症状根本原因解决办法本地正常打包后图片全挂代码里写死了/img/xxx.png绝对路径改成import方式或用import.meta.env.BASE_URL拼接部署到子路径后favicon丢失index.html里href/favicon.ico没有跟随base改用%BASE_URL%favicon.ico刷新页面404路由history模式没有传入basecreateWebHistory(import.meta.env.BASE_URL)CSS背景图404CSS里用了url(/images/xx.png)绝对路径改成相对路径或移入src/assetspublic下配置文件请求404fetch(/config.json)不支持子路径用fetch(import.meta.env.BASE_URL config.json)process.env直接报错Vue2迁移代码没有改完替换为import.meta.env对应写法别名找不到模块Vite没有自动配置指向srcresolve.alias手动配置vite preview打开白屏base为/admin/但预览时直接访问/访问/admin/地址或临时改base为/这些坑不是一个个孤立的事件它们背后就是同一个逻辑**Vite对绝对路径的“容忍度”比Webpack低得多构建器能帮你的地方都预设了你走模块系统。**你只要写死一个斜杠开头的手工路径Vite默认它指向服务器根路径不会做任何加法。5.2 一条高效的排查路径如果你现在项目里已经有资源404按这个顺序排查最快第一步打开浏览器DevTools的Network面板看那个失败请求的URL是什么。如果URL里看不到/admin/或你设置的base说明这是手写的绝对路径Vite没管它直奔代码里搜索对应字符串。第二步看URL里有没有hash。如果文件名带了?hash之类的内容说明资源走了构件器那么问题大概率出在nginx没把文件配好比如静态资源目录指向不对。如果URL里没有hash大概率是public目录下手工引用的东西检查文件在不在dist根目录下。第三步检查index.html开头部分看link relstylesheet引用的CSS路径前缀对不对。这里是Vite正确性的第一道门如果这里都错后面JS、图片全免谈。第四步有nginx先看nginx配置没有就本地起一个vite preview配合子路径访问排除服务端干扰。第五步翻构建日志。vite build输出的文件列表里能看到public下文件是否有被拷贝也有助于确认是否删错文件。5.3 写路径的三个长期习惯最后分享三个我实践下来能大幅减少路径问题的习惯。第一个代码里所有“以斜杠开头的URL字符串”都要引起警惕不管是img的src、link的href、fetch的url还是a标签的to属性写之前问自己一句这个路径要不要跟随base如果答案是要就别写死。第二个能用import/import.meta.url方式就不要手动拼字符串。Vite原生支持new URL(./xxx.png, import.meta.url)它会自动帮你把URL转成正确的资源地址。尤其是动态拼接图片URL的场景这个写法比${import.meta.env.BASE_URL}images/${name}.png可靠得多。第三个创建一个工具函数统一处理资源路径。项目里如果实在避免不了拼接public目录下的资源建议抽一个函数// src/utils/asset.js export function asset(url) { return new URL(../public/${url}, import.meta.url).href }或者简单一点export function asset(url) { return ${import.meta.env.BASE_URL}${url.replace(/^\//, )} }这样至少全项目的资源路径处理逻辑是统一的后期迁移、改部署路径时不用满世界找散落的斜杠字符串。从Vue2的Webpack到Vue3的Vite这项迁移真正颠覆的不是语法而是你对“资源路径”这件事的心智模型。Webpack给开发者留了很多“手工拼绝对路径”的空间Vite则坚定地把一切交给模块系统。我在多次迁移过程中最深的一点体会就是当你在Vite项目里想手写一个以斜杠开头的URL时一定要停下来多问一句这个路径在子路径部署下还能不能成立。把这个习惯养成了public目录、base、路由三者的协同关系自然就顺了。