☰
Vue图片引用路径详解:相对路径、@别名与打包部署踩坑指南
2026/10/1 5:09:58 网站建设 项目流程

Vue 项目里图片引不出来这种事,从新手到老手几乎都遇到过。我见过不少奇怪报错:有的本地起服务一切正常,npm run build之后页面所有图标全裂;有的在某个组件里写../assets/logo.png看着没问题,换两层目录就 404;还有的在<style>里写url('~@/assets/bg.png')能显示,放到<img>标签里就失效。这篇文章就围绕静态资源中最常见的图片引用,把 Vue 里的相对路径、绝对路径、@别名、~符号这几类写法一次理清楚,再结合打包后的路径变化聊透部署场景下的坑。

无论你是刚接触 Vue 的新手,还是写了好几个项目但始终没弄明白静态资源路径的“熟练工”,这篇文章都值得花十分钟看完。我会把开发服务器里的请求情况、构建产物目录都拆开讲,并且准备了一个可以直接在本地跑起来的演示工程,方便你对照验证。毕竟路径问题光看文档容易迷糊,真正在浏览器 Network 面板里看一次就全明白了。

1. 先搞清楚 Vue 项目里的两套静态资源机制

1.1 src/assets 与 public 目录的本质区别

在 Vue CLI 和 Vite 的项目结构里,静态资源默认有两个存放位置:src/assets和public。这两个目录的处理机制完全不同,这也是路径问题最大的根源。

src/assets下的资源会参与打包。以图片为例,它会走一遍构建工具的资源处理链:小体积图片会被转成 base64 字符串直接嵌进代码里,大体积图片会被复制到打包输出目录并重新命名(加哈希值)。这种处理方式的优势是浏览器请求数减少、文件名带哈希方便缓存更新,代价是“文件路径在打包后不再是你写的那个路径了”。

public目录则简单粗暴:里面的文件会被原样复制到打包结果的根目录,不压缩、不哈希、不做任何转换。你在代码里怎么写引用路径,上传服务器后就得按这个路径去访问。这个目录适合放 favicon.ico、外部脚本、某些不想参与构建的静态文件。

理解了这两套机制,再看路径写法就会清晰很多:写在src/assets里的资源,需要考虑“构建工具会怎么处理我写的这个字符串”;写在public下的资源,需要考虑“浏览器最终会请求哪个 URL”。

1.2 路径解析的层级:代码的哪一层在处理你的路径

很多人有个直觉:只要从“当前文件出发”的路径指对了就行。但这个直觉在 Vue 项目里经常失灵,因为一行路径可能被不同层级的处理器解析,而它们遵循的规则不一样。

我们先按代码出现的位置把路径分成三类:模板里的src属性、<script>里的import语句、以及<style>里的url()。在 Vue 的 SFC 单文件组件中,这三部分会被分别交给不同的编译器或加载器处理。模板部分的src会被 vue-loader 或 Vue 插件转换,import语句会被 webpack 或 Vite 的模块解析器处理,样式里的url()则要经过 css-loader 或 Vite 的 CSS 插件。每一层对相对路径、@、~的支持程度都有细微差别。

所以,判断一条路径是否正确,不要只看“从当前文件能不能到目标文件”,还要看“写路径的这行代码最终会被哪一层处理器给吃掉”。这个视角一旦建立起来,很多奇奇怪怪的 404 问题都能自己推断出答案。

2. 四类路径写法逐个拆解:相对路径、绝对路径、@、~

2.1 相对路径与绝对路径的行为差异

相对路径./、../是大家最熟悉的写法,它从当前模块文件出发去寻找目标资源。在<template>的img标签里写<img src="./assets/logo.png">,这个src会被构建工具解析成对./assets/logo.png的依赖请求,并交给资源加载器处理。相对路径最大的问题是“脆弱”:组件目录一旦变深,../就要跟着加,层级一乱就会引用错位置。我自己写项目时,只有在资源文件和组件距离很近、且只有一个组件用到时才会用相对路径,否则一律用别名。

绝对路径在这里需要分两种情况。第一种是网络上的完整 URL,比如https://cdn.example.com/images/a.png,这种浏览器直接请求原样地址,构建工具不会管。第二种是根路径写法,比如/images/logo.png,在 Vue 项目里它默认指向public目录的根,也就是打包后服务器域的根路径。这里就藏着一个常见的部署坑:如果项目部署在域名子目录下,/images/logo.png仍然会去请求域名最顶层的/images/logo.png,结果肯定是 404。这个问题我放到第四章详细说。

2.2 @ 别名的配置与使用边界

@是 Vue 生态里最常见的路径别名,默认指向src目录。在 Vue CLI 创建的项目里,@已经配置好,可以直接使用;在 Vite 项目里需要手动在vite.config.js中配置resolve.alias,通常写法是:

// vite.config.js import path from 'path' import { defineConfig } from 'vite' export default defineConfig({ resolve: { alias: { '@': path.resolve(__dirname, 'src') } } })

配置好之后,在<script>的import语句里写:

import logo from '@/assets/logo.png'

这种写法的好处是无论组件嵌套在哪个目录层级,@永远指向src,从根源上消灭了“../../../数不清”的问题。值得注意的是,在 Vue CLI 项目的<template>里直接写<img src="@/assets/logo.png">是可以被解析的,因为 vue-loader 会把模板里的这个src转成一个require('@/assets/logo.png')请求,再交给 webpack 的模块解析,@别名在这个环节依然有效。但在 Vite 项目里,如果你的alias配置得没问题,@在模板中也能工作,原理是 Vue 插件将模板中的静态资源路径变成了一个 import 声明。不过为了少踩莫名其妙的坑,我更推荐在<script>中用import显式引入,再通过变量去绑定src。

2.3 ~ 符号的真正用途:CSS 模块解析的钥匙

~这个符号看起来神秘,实际上是一个专门为解决 CSS 解析问题而存在的“钥匙”。在 webpack 体系下,当你在 CSS 或预处理器文件里写url()时,css-loader 默认会把相对路径解析成相对于当前样式文件的路径。但如果你写url('@/assets/logo.png'),css-loader 不会主动去识别@别名,它很可能直接把@当成一个相对目录去拼接,结果报找不到模块。

解决办法就是在路径前面加上~:

.background { background-image: url('~@/assets/logo.png'); }

这里的~是在告诉 css-loader:别按普通 url 处理,把后面这段字符串当模块请求扔给 webpack 的模块解析器,这样一来@别名就能识别了。在 Vite 中,~的语义类似:Vite 会剥离~,然后把后面内容当作模块 ID 去解析,所以~@/assets/logo.png也能正常工作。还有一个细节:Vite 里直接写url('@/assets/logo.png')其实也能解析,因为 Vite 的 CSS 处理会自动按 alias 尝试解析。但考虑到项目可能在 Vue CLI 和 Vite 之间迁移,保留~@/这种写法兼容性最高。

3. 图片引入实操:template、script、style 与动态绑定

3.1 template 中直接用 src 引入图片

在模板里引入图片是最直观的场景,我按不同写法整理了一个可以直接运行的示例:

<template> <div class="path-demo"> <!-- 相对路径,相对于当前 .vue 文件 --> <img src="./assets/logo.png" alt="relative" /> <!-- 上级目录,控制好层级就行 --> <img src="../assets/logo.png" alt="parent" /> <!-- 指向 public 目录下的一张图 --> <img src="/images/logo.png" alt="public" /> <!-- @ 别名,推荐写法 --> <img src="@/assets/logo.png" alt="alias" /> </div> </template>

这里最容易被忽略的是:src="./assets/logo.png"这种写法虽然看着像纯字符串,但在 Vue CLI 的构建体系里,它会被 vue-loader 识别并转成require('./assets/logo.png'),所以图片才能被打包处理。如果你在src前面动态绑定:src="'./assets/logo.png'",这个字符串就不会被构建工具转换,运行时就只能按浏览器地址栏的相对位置去找,大概率 404。这个细微差别非常关键,我会在 3.4 节再展开。

演示工程里我建议你重点观察 Network 面板:相对路径和@别名的图片请求,在开发环境下会真实存在;如果你把一张public目录下的图片路径写成/public/images/logo.png,请求会直接 404,因为public目录的文件应该以/images/logo.png这种不带public前缀的方式访问。

3.2 script 中用 import 和 require 引入图片

当图片需要被当作模块使用,比如传给 canvas 或手动设置 DOM 背景时,就要在<script>里引入。最推荐的是静态import:

<script setup> import logo from '@/assets/logo.png' </script> <template> <img :src="logo" alt="imported" /> </template>

这里有一个新手常犯的错误:import的路径必须是固定的字符串,不能是拼接出来的变量。如果你硬写import logo from '@/assets/' + name + '.png',语法上就不被允许,会在编译阶段直接报错。在 webpack 体系里,对应的方法是使用require:

// Vue CLI / webpack 环境 const logo = require('@/assets/logo.png')

require支持动态拼接字符串,比如require('@/assets/' + name + '.png'),webpack 会尝试把能匹配上的资源都打包进来,运行时再按具体请求取出对应文件。但正因为这个“打包所有可能文件”的行为,它会把整个目录下符合条件的图片全部打进产物,导致包体积变大,所以不建议滥用。如果确实有很多图片需要动态加载,更应该用require.context明确筛选范围,不过这部分严格来说已经超出路径问题的范畴了。

在 Vite 里,除了import和require,还有一种官方推荐的方式是new URL:

const logoUrl = new URL('./assets/logo.png', import.meta.url).href

这种写法的特点是基于当前模块的 URL 来计算目标路径,可以结合模板字符串做动态拼接,但在构建时要求相对路径的模式能被静态识别,否则构建会警告或失效。

3.3 style 中 url() 引入背景图的关键细节

在<style>中引用背景图,是~符号出现频率最高的场景。以 Vue 单文件组件为例:

<style lang="scss" scoped> .card { /* 相对于当前 .vue 文件 */ background-image: url('../assets/card-bg.png'); /* 推荐:使用 ~ 加上 @ 别名 */ background-image: url('~@/assets/card-bg.png'); } </style>

如果你在 webpack 环境下老老实实写url('@/assets/card-bg.png'),大概率会碰到 css-loader 解析失败,报“找不到模块 @/assets/card-bg.png”。加上~之后,css-loader 会把后面内容当作模块请求来处理,@别名才能生效。在 Vite 里,我实测过url('@/assets/card-bg.png')也可以出图,但为了项目将来可能切换构建工具,建议保持~@/的写法。

还有一个和预处理相关的细节:在scss或less文件中,如果你把图片路径写在一个变量里再传给url(),比如background-image: url($bg),路径解析的规则会略有不同,因为预处理器在编译阶段就可能把路径改写了。遇到这种情况最简单的方法是把变量值里直接写成完整的~@/assets/xxx.png,并确保构建工具能识别。

3.4 动态 src 的正确姿势:打包处理与运行时解析的界线

动态绑定src是路径问题中翻车率最高的一类,典型错误是:

<template> <!-- 这种写法不行,运行时会拿这个字符串去请求 --> <img :src="`/assets/${imageName}.png`" /> </template>

开发时如果项目跑在根路径,/assets/xxx.png可能碰巧能命中 dev server 里的某个资源;一旦打包上线,资源路径会被加上哈希,比如/assets/photo.a1b2c3.png,你写死的字符串里并没有这些哈希,请求就直接 404 了。正确思路是在构建阶段就把图片“拿进”打包流程,然后在运行时只负责选择文件。

在 Vue CLI 项目中,最朴素的做法是写一个方法:

methods: { getImage(name) { return require(`@/assets/${name}.png`) } }

在 Vite 项目中,require不可用,我更常用import.meta.glob:

<script setup> const modules = import.meta.glob('@/assets/*.png', { eager: true, import: 'default' }) const getImage = (name) => modules[`@/assets/${name}.png`] </script>

这段代码会在构建时把@/assets下的所有.png文件都收集起来,运行时通过name取出对应的打包后 URL。这种方式把路径问题完全收敛到了构建层,是我目前最推荐的动态图片方案。演示工程里我专门放了一个下拉框选择不同图片的页面,你能直观看到切换时 Network 请求如何从同一份打包资源里取图。

4. 打包构建后的路径变化与部署调整

4.1 资源打包后究竟去了哪里

先看一个 Vue CLI 项目打包后的典型目录结构:

dist/ ├── index.html ├── css/ │ └── app.a1b2c3.css ├── js/ │ ├── app.a1b2c3.js │ └── chunk-vendors.a1b2c3.js ├── img/ │ └── logo.c4d5e6.png └── favicon.ico

src/assets/logo.png打包后会进入img目录并被改名为logo.c4d5e6.png,public/favicon.ico则原样复制到dist根目录。打开打包后的index.html,你看到的是:

<script defer src="/js/app.a1b2c3.js"></script> <link href="/css/app.a1b2c3.css" rel="stylesheet" />

这个/js/...前缀来自构建配置里的publicPath(Vue CLI)或base(Vite)。默认是根路径/,表示资源从服务器域名的根开始找。

这样做有个明显问题:如果项目部署到https://example.com/my-app/这个子目录,浏览器加载index.html后,会去请求https://example.com/js/app.a1b2c3.js,而不是https://example.com/my-app/js/app.a1b2c3.js,结果自然就是 404。CSS 加载不出来,页面布局当然整个崩塌。

4.2 部署到服务器子目录时的 base 配置变更

解决子目录部署问题,思路就是告诉构建工具:所有生成的资源路径都要带上前缀。Vue CLI 项目在vue.config.js里设置:

module.exports = { publicPath: process.env.NODE_ENV === 'production' ? '/my-app/' : '/' }

Vite 项目在vite.config.js里设置:

export default { base: process.env.NODE_ENV === 'production' ? '/my-app/' : '/' }

设置后,打包出来的index.html就会变成/my-app/js/app.a1b2c3.js,资源请求路径就和部署目录对上了。还有一种更省心的写法是把publicPath或base直接设为'./',这样打包后所有资源路径都变成相对路径。用相对路径时,index.html无论放在哪个子目录,资源都会基于当前 HTML 文件位置去查找,对部署位置比较包容。缺点也很明显:如果项目里有基于绝对路径写死的路由、或者需要配合 history 路由,这种相对路径方案会带来新的问题。

4.3 打包后布局异常:最常见的 404 资源链

很多人在网上搜“Vue 打包后布局异常”,走进来一看,CSS 没挂上,JS 没加载,页面像纯文本。这种情况十有八九是资源路径没有配上部署目录。排查的第一步是打开浏览器控制台的 Network 面板,看 HTML 请求之外的 JS、CSS 资源返回了什么。如果一堆404,你就把 URL 和实际部署目录对比一下,基本能判断是缺少/my-app/前缀,还是路径多了一截。

第二步是看 HTML 里的资源引用方式。如果你用的是publicPath: './',但 HTML 里有一张图片写成/images/logo.png,这张图在子目录部署时依然会 404,因为它是纯浏览器绝对路径,不受构建配置控制。所以在子目录部署场景里,凡是涉及public目录下的资源引用,我都建议用相对路径或者通过BASE_URL动态拼接:

const assetUrl = `${import.meta.env.BASE_URL}images/logo.png`

Vue CLI 里对应的环境变量是process.env.BASE_URL。用这种方式组合出来的 URL,会跟着构建配置里的publicPath或base一起变化,就不会出现“其他资源都好,就那几张图 404”的诡异情况了。

5. 常见问题与排查技巧实录

5.1 本地正常、打包后图片全无

这个问题的根源通常是开发服务器和打包产物对路径的处理方式不同。开发服务器为了实时编译,会把很多相对路径自动修正;打包产物则是按最终的资源输出路径来写。我遇到过最典型的一个例子:项目里用<img src="/assets/logo.png">,开发时因为 dev server 把某个目录暴露在根路径下所以能显示,以为没问题;打包后在纯静态服务器上一看,/assets/logo.png请求 404。查了半天才发现,资源放在public/assets下,但 server 部署在/web子路径,浏览器去请求域名的根路径/assets/...,当然找不到。

应对方式很简单:先定位资源文件的归属。如果图片在src/assets,尽量用@/assets/...或import引入;如果图片在public,引用时用BASE_URL拼接,别写死/开头的绝对路径。

5.2 动态拼接图片路径不生效

前面已经说过,构建工具只能静态分析固定的路径字符串。import logo from '@/assets/logo.png'这种写死了的路径能被处理;:src="/assets/${name}.png"这种由运行变量拼出来的字符串,构建工具完全无能为力。如果你非要在运行时拼接路径,就得让拼接过程发生在构建层:webpack 用require或require.context,Vite 用import.meta.glob。记住这个分界就能少走弯路:凡是经过构建的资源,路径动作发生在构建阶段;运行时只能“选择已经构建好的资源”,而不是“重新拼一个资源路径”。

5.3 常用排查命令与速查表

排查路径问题时,我通常按这套流程走:

# 先看打包后的资源引用的基础路径 # Vue CLI npx vue-cli-service inspect # Vite npx vite build --debug

直接把打包入口 index.html 和相关 CSS/JS 标签里的路径和浏览器实际请求对比,效率最高。下面是几张速查表,方便你对照判断。

写入场景写法说明
模板 img src./assets/logo.png相对路径,会经过构建处理
模板 img src/images/logo.png指向 public,不参与构建
模板 img src@/assets/logo.pngVue CLI 可用,Vite 需配置 alias
script import@/assets/logo.png最推荐,构建时会正确解析
script require@/assets/logo.png仅 webpack 环境
style url../assets/bg.png相对于当前样式文件
style url~@/assets/bg.png兼容 webpack 与 Vite
运行时动态绑定require或import.meta.glob构建阶段处理
运行时写死字符串不可行会直接 404
部署场景推荐配置
服务器根路径部署publicPath: '/'/base: '/'
子目录部署publicPath: '/my-app/'/base: '/my-app/'
子目录部署且不确定位置publicPath: './'/base: './'
需要 history 路由避免用'./',建议显式子目录路径

最后再补充一个我自己的习惯:在项目初期就约定好图片资源的存放规则,并把这些规则写进 README。优先级从高到低是:静态 import /@路径优先;public 资源统一通过BASE_URL引用;动态资源走构建层工具;所有写死/开头路径的代码必须过评审。养成这个习惯之后,团队里因为图片引用产生的“玄学问题”基本就绝迹了。你可以直接照着这篇文章的演示工程试一遍,把四类路径都跑一遍,浏览器控制台的红色报错会比一千句经验总结记得更牢。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询