ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Vue静态资源引用路径详解:从图片404到@别名与打包部署

Vue静态资源引用路径详解:从图片404到@别名与打包部署 最近好几个同事问我同一类问题“我在本地能显示图片怎么打包上线就404了”“为什么我在img的src里写/assets/logo.png有时候行、有时候不行”“CSS里background-image的路径到底该怎么写”这些问题看着零散其实都指向同一个东西——Vue静态资源引用。平时开发环境一切正常是因为dev server帮你兜了底一旦打包部署缺少对相对路径、绝对路径、、~的正确理解各种图片丢失、白屏、背景图消失就会轮番上演。这篇笔记以图片引入为例把这几种路径写法的原理、适用场景、坑位和验证方法从头到尾捋一遍最后附一个可以在线跑通的演示工程思路保证你看完能直接上手排查自己项目里的路径问题。1. 打包后图片集体404先搞清到底是谁在解析这串路径很多人遇到图片404第一反应是“路径写错了”然后开始瞎试加./、删../、改成/运气好试通了但下次换个目录部署又崩。问题的核心在于你写下的路径和浏览器真正发出去的请求路径中间隔了好几层“翻译”。不搞清楚每层翻译是谁做的就永远在猜。1.1 一条src路径从写下来到真正请求经历了两次“解释”举个最简单的例子你在src/views/Home.vue里写template img src./images/cat.png altcat /template在源代码里./images/cat.png的参照物是Home.vue这个文件所在目录。vue-loader和webpack在编译时看到这个src会把它当成模块依赖来处理去src/views/images/下面找cat.png找到后把它打包进静态资源目录然后把HTML里的src重写成一个带哈希的文件名比如/static/img/cat.8f2a1b3c.png。也就是说你写的相对路径./实际上是被webpack“吃掉并重写”的。这层重写让开发体验很爽但也制造了一个幻觉让人觉得HTML里看到的路径就是自己写的路径。等到打包之后你打开dist/index.html看到的src可能长这样img src/static/img/cat.8f2a1b3c.png这时候后面已经没有webpack了只有浏览器。浏览器解析/static/img/cat.8f2a1b3c.png时会从当前域名根目录去找。如果项目部署在https://example.com/app/这个子目录浏览器访问的就是https://example.com/static/img/...而不是https://example.com/app/static/img/...404自然就来了。1.2 开发服务器和静态托管根目录的参照物不一样开发环境下vite/webpack dev server默认把项目根目录作为站点根目录。你访问http://localhost:5173/项目根就是/。所以源码里很多绝对路径/src/assets/xxx.png能正常访问——因为dev server上确实存在/src/assets/这个URL。但部署到服务器后你的静态文件可能被nginx挂在一个子路径下也可能CDN只托管dist目录里的一部分内容。此时/src/assets这个路径在线上根本不存在浏览器只能拿到404。记忆锚点很简单开发时你面对的是一个“源码即站点”的服务器部署后你面对的是一堆构建产物文件。写路径时得先想清楚当前这串路径是给谁看的——给构建工具看的还是给浏览器看的。2. 相对路径和绝对路径不是谁更好而是“看谁在解释”“推荐用相对路径还是绝对路径”这个问题在很多技术群里反复出现。答案其实很朴实二者没有绝对优劣关键是匹配场景。你只需要判断这串路径的解析者是谁。2.1 相对路径的三类解析者同样是相对路径在不同位置出现解析规则完全不同出现位置谁在解析参照物说明.vue文件template里的srcwebpack/vite当前.vue文件所在目录会被构建工具解析并重写script里import img from /assets/a.pngwebpack/vite当前JS模块所在目录或别名会被构建工具解析CSS/SFCstyle里的url(...)webpack/vite当前样式文件所在目录同样被构建工具处理public/index.html里的src浏览器当前页面URL不会被构建重写原样输出运行时动态拼接的:src浏览器当前页面URL构建工具无法静态分析原样输出看清楚这张表你就明白为什么同一个./有时候管用有时候不管用。在.vue文件的template里写img src./cat.pngwebpack解析后能正常打包。在public/index.html里写img src./cat.png它就是字面上的“相对于当前URL路径”打包后路径原样保留。这俩完全相同行为却不同。2.2 绝对路径的坑它不是“从项目根开始”而是“从域名根开始”很多人误以为/src/assets/logo.png是“以项目根目录为起点”的绝对路径。实际上浏览器眼里/永远是域名根不是项目目录。开发时dev server把项目根映射为域名根所以/src/assets/logo.png能访问。部署到https://example.com/app/后项目根是/app/但/src/assets/logo.png仍然指向https://example.com/src/assets/logo.png——项目里根本不存在这个目录。解决方案也很明确如果项目要部署到二级目录要么在构建配置里把base/publicPath设置成相对路径./让构建产物内部的绝对路径变成相对的要么把所有静态资源都改成完整URL带协议、域名、端口。真正的绝对URL是这样的img srchttps://cdn.example.com/images/logo.png这种写法不受部署目录影响适合CDN资源、外部图床。2.3 什么场景选哪种写法给你一套可抄的方案以我自己做项目的习惯组件内部图片跟随组件代码走用import或require引入或直接在template里写/assets/...交给构建工具处理。项目级通用图片favicon、分享图、全局logo放public目录HTML里使用完整URL或相对路径部署时按实际环境调整base。外部资源CDN、对象存储一律完整URL不掺和构建。背景图SFC style里用url(/assets/bg.png)或url(~/assets/bg.png)看构建工具版本。核心原则是能被构建工具处理的就让它处理不能被处理的就写完整URL。两头都不靠的才会踩坑。3.和~别名本身没毛病用错位置才是万恶之源3.1JS和模板里的“路径指北针”在Vue CLIwebpack和Vite里都被默认配置成src目录的别名。它的作用是让模块引用不再写一长串../../../尤其是深层嵌套组件里可读性提升非常明显。// 不用别名 import Header from ../../components/Header.vue // 用别名 import Header from /components/Header.vue在.vue文件的template里同样可用img src/assets/logo.png因为vue-loader在编译模板时会把src当作模块请求交给webpack/vite处理在模块解析阶段被替换成src目录的真实路径。注意并不是在浏览器端被识别的。它只在构建阶段存在打包后会被“翻译”成真实的绝对路径或哈希后的文件路径。如果你在运行时才把/assets/logo.png塞给某个src属性那就晚了——已经没有构建工具帮你翻译了。3.2~CSS世界里通往模块解析的“通行证”~是webpack时代CSS处理的一个特殊标记。在CSS的url()里写上~表示“后面的路径请走模块解析而不是直接当作相对路径”。最典型的是引用node_modules里的资源background: url(~bootstrap/dist/img/sprites.png);如果不加~CSS处理器会把这个路径当成相对路径去相对样式文件的位置找bootstrap/dist/...几乎必挂。加上~webpack就会去node_modules里找。同理在webpack里也可以通过~/组合在CSS中引用src下的资源background: url(~/assets/bg.png);这里~告诉webpack走模块解析再告诉它去src目录。到了Vite环境情况稍有不同。Vite的CSS处理默认支持别名解析很多场景下直接写url(/assets/bg.png)也能生效不一定需要~前缀。但为了兼容性如果你在维护一个老项目建议保留~的写法至少在webpack构建的下它是最稳妥的。3.3 最容易犯的错把/写进浏览器会直接读取的URL我见过很多新手写这种代码script setup const images [/assets/a.png, /assets/b.png] /script template img v-forimg in images :srcimg /template结果就是浏览器把/assets/a.png当作请求路径去问服务器要这个文件。服务器没有叫的目录直接404。这类问题的本质v-for里的img是运行时变量webpack在编译模板时根本看不出来它最终的字符串长什么样自然没法做静态替换。:src绑定的值是运行时赋值给DOM属性的浏览器拿到什么就请求什么。要解决有几个方向预先用import把所有图片引入成URL对象import imgA from /assets/a.png import imgB from /assets/b.png const images [imgA, imgB]用requirewebpack环境const images [a, b].map(name require(/assets/${name}.png))把图片放public目录运行时拼接相对路径或完整URLconst images [a.png, b.png].map(name /static/${name})推荐第3种逻辑最直观也不容易被构建链路坑到。3.4 别指望public目录里的文件能认识或~public目录Vite是publicVue CLI是public下的文件会被原样拷贝到打包输出目录不经过构建工具处理。所以你在public里的HTML或JS中写/assets/...就是字面量不会有人帮你替换。这也意味着public目录适合放那些完全不需要构建逻辑介入的静态文件比如favicon、第三方插件脚本、服务端需要直读的文件。凡是走打包逻辑的资源尽量放src/assets凡是需要固定URL直出的放public。4. 一张表加三段代码把图片引用的六个场景一次说清概念说再多不如直接给结论。下面的速查表是我每次写项目都会对照的建议直接收藏。4.1 图片引用场景速查表场景推荐写法谁在解析备注模板内静态图片img src/assets/logo.pngwebpack/vite推荐打包自动加哈希脚本里导入图片import logo from /assets/logo.pngwebpack/vite适合需要逻辑判断的场景遍历列表的图片预先import成数组或使用public路径取决于实现不要直接拼/字符串style背景图Vue SFCurl(~/assets/bg.png)或url(/assets/bg.png)webpack/vite注意构建工具差异public目录直接引用img src/static/logo.png或相对路径浏览器不经过构建原样输出外部CDN图片完整URLhttps://...浏览器不受部署目录影响4.2 模板内静态src看看构建后发生了什么用一个最小SFC来验证template div img src/assets/logo.png altalias img src../assets/logo.png altrelative /div /template我用Vite构建webpack同理输出目录里会出现类似assets/logo-8f2a1b3c.png的文件。HTML里两处src都被重写成同一个URLimg src/assets/logo-8f2a1b3c.png altalias img src/assets/logo-8f2a1b3c.png altrelative结论不管是/还是相对路径最终都会被构建工具解析成同一个产物。所以源码里选哪种本质是“可读性偏好”不是“能不能用”的区别。两个都试通了之后建议团队统一用/维护成本低得多。4.3 动态图片src为什么拼接字符串永远不靠谱想象一个商品列表图片文件名存在接口返回的字段里。很多人的第一反应是这样写template img :src/assets/products/ product.image alt /template结果通常是404或者打包后图片根本没出现在产物里。原因前面说过webpack做静态分析时需要看到完整、确定的模块路径。/assets/products/ product.image这种拼接字符串无法在编译期确定最终路径构建工具只能放弃处理。可靠方案之一是在script里建立一个文件名到URL的映射script setup import p1 from /assets/products/p1.png import p2 from /assets/products/p2.png import p3 from /assets/products/p3.png const imageMap { p1: p1, p2: p2, p3: p3, } const product { image: p1 } /script template img :srcimageMap[product.image] alt /template另一个方案如果图片数量特别多且名字有规律用import.meta.globVite或require.contextwebpack批量导入// Vite const modules import.meta.glob(/assets/products/*.png, { eager: true, import: default }) // webpack const modules require.context(/assets/products, false, /\.png$/)这两种都是“把运行时动态拼接的活提前到构建期交给工具处理”路径可靠也能享受哈希重命名。4.4 背景图url()Vite和webpack的写法差异要分清SFC的style里写背景图是另一个高频踩坑点。先说Vite别名在CSS里默认可用style scoped .box { background: url(/assets/bg.png) center/cover no-repeat; } /stylewebpack的Vue CLI项目在CSS里通常也能用但老项目更保险的写法是加~style scoped .box { background: url(~/assets/bg.png) center/cover no-repeat; } /style为什么webpack项目推荐~因为CSS的url()默认会被css-loader处理遇到~时会把后续部分交给webpack的模块解析机制。~/相当于明确告诉loader“接下里的是webpack别名按模块规则找。”如果直接写/assets/bg.png某些版本下css-loader会尝试把它当成相对路径处理导致解析失败。Vite在这方面更宽容内置了对别名的支持但我仍然建议在Webpack项目里显式保留~避免升级依赖时被坑。5. 在线演练用一个最小工程亲手验证所有结论理论讲完必须动手。这里给出一套完整的在线演示工程搭建思路用CodeSandbox就能跑不需要本地装环境。整个过程不超过10分钟但能把前面所有结论踩一遍。5.1 创建演示工程打开CodeSandbox选择Vue模板或Vite Vue模板会得到一个src目录下的基础项目。在项目里做两件事在src/assets下放一张图logo.png。在public目录下放一张图static-logo.png。为了验证方便你可以用任意一张测试图片文件大小随意。5.2 逐个场景写入代码看Network面板的真实请求把App.vue改成下面这段涵盖了模板静态引用、脚本引用、背景图、public引用、动态拼接错误示范五种写法template div !-- 写法1模板内使用 别名 -- img src/assets/logo.png altalias !-- 写法2脚本导入后绑定 -- img :srclogoUrl altimport !-- 写法3public 目录直出 -- img src/static-logo.png altpublic !-- 写法4动态拼接 别名故意演示错误 -- img :srcwrongUrl altwrong-dynamic stylewidth: 50px; !-- 写法5CSS 背景图 -- div classbg-box/div /div /template script setup import logoUrl from /assets/logo.png // 这一段故意用字符串拼接演示运行时拿不到 别名 const wrongUrl /assets/logo.png /script style scoped .bg-box { width: 200px; height: 100px; background: url(/assets/logo.png) center/cover no-repeat; } /style运行之后打开浏览器开发者工具的Network面板你会看到写法1、写法2、写法5最终请求的都是/src/assets/logo.pngCodeSandbox开发模式下或打包后的/assets/logo-xxx.png状态200图片正常显示。写法3请求的是/static-logo.png200图片正常显示。写法4请求的是//assets/logo.png大概率404图片裂开。这个实验能直观证明只在构建期有效运行时拼出来的/...就是一段普通字符串。5.3 切换生产构建观察路径重写在CodeSandbox里执行npm run build然后预览dist目录打开index.html能看到所有被构建工具处理的图片URL都变成了类似/assets/logo-8f2a1b3c.png的形式而public下的图片还是原文件名/static-logo.png。通过这个观察你会理解为什么部署后图片会404如果服务器把dist放到一个二级子目录且构建配置里的base还是/那么HTML里的/assets/logo-xxx.png从域名根找自然找不到。解决办法Vite项目在vite.config.js里设base: ./。Vue CLI项目在vue.config.js里设publicPath: ./。设置之后再build你会发现HTML里的图片路径变成了相对路径./assets/logo-xxx.png这样不管你部署到哪个子目录浏览器都会以当前页面路径为参照去解析基本不会出错。6. 我踩过的坑和排查一条龙从现象到根因的完整思路最后分享几个我真实遇到过的线上问题以及一套可复用的排查方法论。6.1 坑1assets和public分不清什么图都往assets塞有次维护一个后台项目几十张菜单图标全部塞进src/assets然后用import一张张导入。后来业务方要求图标支持后端配置把图片URL换成接口返回的路径整个项目改动量大得离谱。正确做法是提前区分跟着组件走、不常变的图src/assets走构建。运营可配置、需要后端返回URL、内容会频繁替换的图public目录或对象存储接口返回完整URL。怎么判断问自己一句“这个图会不会在发布后被人为替换”大概率会就放public或CDN完全不会才放assets。6.2 坑2项目部署到子目录绝对路径全线崩溃某个项目一开始部署在域名根路径所有图片都写/static/xxx.png一切正常。后来为了多版本共存运维要求部署到/v2/子目录结果首页图片、favicon、分享图全部404。当时我排查的顺序是打开浏览器Network看失败的请求URL是什么。结果请求的是https://example.com/static/logo.png而正确路径应该是https://example.com/v2/static/logo.png。看构建配置发现base没设置默认/。把vite.config.js里base改成./重新构建。但仍有部分动态拼接的URL是硬编码/static/...这些是运行时逻辑不受构建配置影响必须手动改成相对路径或注入全局base。教训就是如果项目一开始就知道要部署到子目录最好从第一天就用相对路径或base: ./而不是等到线上炸了再补。6.3 排查一条龙按照“路径由谁解析”的顺序检查如果静态资源再次出现404不要乱试按下面顺序过一遍第一步看浏览器Network里真实的请求URL。这一步能立刻区分问题是出在构建期还是运行期。如果请求URL里还有、~、相对路径../这些“源码味”很重的内容说明构建工具没有正确处理。如果URL已经是/assets/xxx.png这种产物路径说明问题在部署配置。第二步判断这串路径是给构建工具看的还是给浏览器看的。模板里写死、脚本里import、CSS url()里写死这些都归构建工具管。运行时拼接、接口返回、后端配置都是浏览器直接请求不能依赖别名和相对路径。第三步检查构建配置里的base/publicPath。尤其部署到子目录时把基础路径改成./几乎能解决一半以上的路径404问题。第四步确认文件本身有没有被打包进产物。在dist目录里搜文件名如果没有说明静态路径分析失败试试import或import.meta.glob。第五步检查public目录是否被正确拷贝。public目录下的文件是原样拷贝不会重命名。如果你部署时只上传了dist里的js/css却忘带public的图片也会404。这套流程我用了很久每次都能比较快地定位到问题层。现在我写项目已经形成肌肉记忆写完一段涉及图片的代码会下意识问自己“这个路径最终谁在解析”想清楚再动手基本不再有图片404的困扰。
返回列表