Nuxt Image 不是 img 的平替:一次本地 assets 图片 404 的排查
<NuxtImg> 很容易被当成一个更高级的 <img>。名字像,写法也像,迁移时看起来只是把标签换一下。
这次问题就出在这里:一批组件私有图片从 <img> 换成 <NuxtImg> 后,本地开发环境里图片突然打不开了。原始图片 URL 明明可以直接访问,但页面里经过 Nuxt Image 处理后变成了 404。
结论先放前面:<NuxtImg> 不是原生 <img> 的无脑替代。它是一个图片优化组件,会把 src 交给 provider 处理。默认 IPX provider 的本地图源目录是 public/;通过 Vite import 得到的组件私有资源 URL,例如 /_nuxt/components/foo/assets/icon.png,不是 IPX 的源图路径,直接传给 <NuxtImg> 很容易被改写成 /_ipx/.../_nuxt/... 并报 IPX_FILE_NOT_FOUND。
现象
项目里有一些组件私有图片,原本是这样写的:
import topCoinSrc from './assets/top-coin.png'<img :src="topCoinSrc" alt="" />后来为了统一图片组件,把它改成了:
<NuxtImg :src="topCoinSrc" alt="" />在 Vite dev 环境里,topCoinSrc 会被编译成类似这样的运行时 URL:
/_nuxt/components/home/RightIcons/assets/top-coin.png这个 URL 直接访问是通的:
curl -sS -o /tmp/direct.png \
-w '%{http_code} %{content_type} %{size_download}\n' \
http://localhost:3000/_nuxt/components/home/RightIcons/assets/top-coin.png
# 200 image/png 3248但页面里的 <NuxtImg> 最终请求变成了:
/_ipx/_/_nuxt/components/home/RightIcons/assets/top-coin.png这条请求会失败:
curl -sS -o /tmp/ipx.json \
-w '%{http_code} %{content_type} %{size_download}\n' \
http://localhost:3000/_ipx/_/_nuxt/components/home/RightIcons/assets/top-coin.png
# 404 application/json 134返回体里的关键信息是:
{
"error": {
"message": "[404] [IPX_FILE_NOT_FOUND] File not found: /_nuxt/components/home/RightIcons/assets/top-coin.png"
}
}同一个项目里,public/ 下的图片走 IPX 是正常的:
curl -sS -o /tmp/public.svg \
-w '%{http_code} %{content_type} %{size_download}\n' \
http://localhost:3000/_ipx/_/home/sidebar/v3/nav-home.svg
# 200 image/svg+xml 2370这说明问题不在 Nuxt Image 本身坏了,而在图片来源不适合交给默认 IPX provider。
assets 和 public 是两条链路
Nuxt 里本地图片大致有两类来源。
第一类是 public/。这里的文件会作为运行态静态资源暴露出来,例如:
public/home/sidebar/v3/nav-home.svg页面里可以直接用:
<NuxtImg src="/home/sidebar/v3/nav-home.svg" />Nuxt 的 public/ 目录说明里也明确提到,这个目录用来放需要通过应用根路径公开服务的静态资源。
第二类是 assets/ 或组件目录里的私有资源。它们会进入 Vite / bundler 管线:
import iconSrc from './assets/icon.png'这种 import 得到的是一个被构建工具处理过的 URL。开发环境里可能长得像 /_nuxt/...,生产环境里可能带 hash,也可能被内联。它适合直接给原生 <img>、CSS background 或其他前端资源消费方使用。
这两类图片都叫“本地图片”,但处理链路不一样。public/ 是运行态可访问的静态文件;assets import 是构建产物 URL。
Nuxt Image 在这里做了什么
Nuxt Image 的 <NuxtImg> 文档里,基础示例用的是这样的路径:
<NuxtImg src="/nuxt-icon.png" />这类路径默认来自 public/。
Nuxt Image 的配置文档里有一个关键配置项 dir。它用于指定 IPX / IPX Static 的本地图源目录,默认值就是 public。
也就是说,默认情况下:
<NuxtImg src="/home/sidebar/v3/nav-home.svg" />会被 provider 处理成类似:
/_ipx/_/home/sidebar/v3/nav-home.svgIPX 会去它认识的本地图源目录里找 home/sidebar/v3/nav-home.svg。
但如果传进去的是 Vite import 产物:
/_nuxt/components/home/RightIcons/assets/top-coin.pngNuxt Image 仍然会把它当成需要交给 provider 处理的 src,于是请求就会变成:
/_ipx/_/_nuxt/components/home/RightIcons/assets/top-coin.png这时 IPX 会去源图目录里找 /_nuxt/components/... 这条路径。这个路径实际属于 Vite dev server 暴露出来的构建资源 URL,不在 IPX 的源图目录里,所以它找不到。
Nuxt Image providers 文档也把这条边界说得很清楚:需要被处理的本地图片应该放在项目 public 目录;assets 目录里的图片由项目 bundler 管理,不由 Nuxt Image 处理。
Astro 的 Image 为什么看起来不一样
这个问题容易和 Astro 的 <Image /> 混在一起。
Astro 的图片能力有一条很典型的写法:
---
import avatar from '../assets/avatar.png'
---
<Image src={avatar} alt="avatar" />这里的 avatar 是 Astro 图片管线产出的 metadata。组件能拿到源文件、尺寸、格式等信息,再做构建期优化。
Nuxt / Vue 里的普通 Vite import 更接近:
import avatar from './avatar.png'得到一个 URL 字符串。<NuxtImg> 看到的是这个 URL,不是 Astro 那种图片 metadata。它可以处理 URL,但不会自动把散落在组件目录里的 import 资源重新变成 IPX 源图。
所以不能把 Astro Image 的心智直接套到 Nuxt Image 上。
正确用法怎么选
如果图片在 public/ 下,并且希望 Nuxt Image 做尺寸、格式、质量或 provider 优化,用 <NuxtImg>:
<NuxtImg src="/profile/badges/vip/ic_vip_level_9.png" alt="VIP9" />如果图片来自远程 OSS / CDN,也可以考虑 <NuxtImg>,但要看 provider、domains 和远程优化规则是否已经配置好。没有配置时,它可能只是原样输出或走默认处理,不能默认等于“已经优化”。
如果图片是组件私有 import 资源,优先保留原生 <img>:
import iconSrc from './assets/icon.png'<img :src="iconSrc" alt="" />这类图片通常是小图标、徽章碎片、装饰图。它们本来就应该交给 Vite 的资源管线处理,不需要再绕一层 IPX。
如果团队真的想让某个本地目录里的图片统一走 Nuxt Image,也可以配置 image.dir:
export default defineNuxtConfig({
image: {
dir: 'assets/images',
},
})但这条路适合集中管理的内容图、营销图或文章图。引用时要按 image.dir 的源目录路径来写,而不是继续把组件私有 import URL 传进去。部署时还要确认这个目录能被运行时 IPX 读取,否则静态生成、serverless 或特定平台上仍然可能 404。
provider="none" 不是常规答案
Nuxt Image 里有一个 none provider,它本质上会原样返回 URL。理论上这样能让 import 出来的 /_nuxt/... URL 正常显示:
<NuxtImg provider="none" :src="iconSrc" alt="" />但这不是一个好默认。
它保留了 <NuxtImg> 的组件名,却绕开了图片优化。review 的人看到这行代码时,容易误以为它仍在走 Nuxt Image 优化。团队后面继续迁移图片时,也会把“能显示”和“被优化”混成一件事。
除非项目已经明确要求所有图片标签都统一成 <NuxtImg>,并且团队接受 provider="none" 只是一个薄包装,否则组件私有资源直接用 <img> 更清楚。
排查这类问题的顺序
遇到 <NuxtImg> 后图片打不开,可以按这个顺序查:
- 打开 DevTools,看真实请求是不是走了
/_ipx/...。 - 如果 URL 里出现
/_ipx/.../_nuxt/...,优先怀疑把 Vite import asset 传给了 IPX。 - 直接请求原始
/_nuxt/...URL,确认 bundler 资源本身能不能访问。 - 再请求
/_ipx/.../_nuxt/...URL,看是否出现IPX_FILE_NOT_FOUND。 - 检查
nuxt.config.ts里的image.dir、provider、domains。 - 按图片来源决策:组件私有资源改回
<img>;需要优化的稳定图片迁到public/或明确的image.dir体系。
浏览器里也可以快速找出坏图:
Array.from(document.images)
.filter((image) => !image.complete || image.naturalWidth === 0)
.map((image) => ({
src: image.getAttribute('src'),
currentSrc: image.currentSrc,
naturalWidth: image.naturalWidth,
naturalHeight: image.naturalHeight,
}))我后面会怎么定规则
这次之后,我会把 Nuxt 项目里的图片先按来源分清楚,而不是批量把 <img> 换成 <NuxtImg>。
小图标、组件私有装饰图、徽章碎片,继续用原生 <img> 或 CSS。它们的价值是稳定、贴近组件、路径清楚。
public/ 下的运行态图片、远程用户图片、内容图、需要响应式尺寸和格式优化的图片,再考虑 <NuxtImg> 或 <NuxtPicture>。这些图片才是 Nuxt Image 更擅长处理的对象。
Nuxt Image 仍然有价值,只是它解决的是“把图片交给 provider 优化”的问题,不是“替换所有 img 标签”的问题。