Vue Nuxt Dynamic Path Resolution Issue
<img :src="'~assets/cat.png'" />
this one doesn’t work
When I wanted to assign an image path through a variable for the <img>
element, I noticed some inconveniences. At first, I thought about simply putting the images into the public/
folder to solve the issue.
But after encountering the same problem a couple of times, I started to wonder why this was happening. That’s why I decided to write this article.
The Crux
the project structure:
└──assets
├── cat.png
└── dog.png
└── public
├── public-cat.png
└── public-dog.png
..
└── utils
The configuration in .nuxt/tsconfig.json
allows ~
to reach the project root directory:
{
"compilerOptions": {
"paths": {
// ...
"~": [
".."
],
"~/*": [
"../*"
],
// ...
🟢 Both work:
<img src="~assets/cat.png" />
<img src="/public-cat.png" />
❌ Dynamic: When passing variables into :src
, it doesn't work for assets/
<!-- failed -->
<img :src="'~assets/cat.png'" />
<!-- works -->
<img :src="'/public-cat.png'" />
How public/
Works
Resources under public/
are distributed to the root directory and can be accessed directly at runtime.
Static
No issues here, as it's copied to the output directory at build time.
Dynamic :src
This method doesn't resolve the path at build time, but since we're directly accessing the exposed public/, it's not a problem - we can still retrieve it at runtime.
How assets/
Works
Static
Resources are found at build time without issue. Webpack or Vite will compile them into accessible resource paths, usually becoming something like /_nuxt/assets/cat.png
, with the _nuxt
prefix added.
Dynamic :src
- Build time skips this type. When the bundling tool sees
<img :src="'~assets/cat.png'" />
, it thinks"This is decided at runtime, so I'll ignore it."
- At runtime, it starts to see
:src
and tries to access the path, but since~
is determined at compile time, the runtime environment doesn't understand~
. This makes sense, as if it could understand these path rules, it would be able to access resources outside ofpublic/
, which sounds quite inappropriate.
Solutions
First import, explicitly indicating the resource
// way 1: import
import catImg from '@/assets/cat.png'
// way 2: URL
const catImg = new URL('@/assets/cat.png', import.meta.url).href
// then
<img :src="catImg" />
// static path with dynamic importing.
const catImg = await import('@/assets/cat.png?url')
// real dynamic
const catImg = await import(`@/assets/${imageName.value}?url`)
<img :src="catImg.default" />
Move directly to public/
The simplest solution.
Thought
Initially, I thought the difference was caused by the packaging method. My first approach was to switch between AOT/JIT modes to see if it could make a difference.
AOT is easy to understand - it optimizes packaging as much as possible, while JIT does just-in-time compilation at runtime.
This is the usual mode:
Nuxt (manager)
↓
strategy (AOT or JIT?)
↓
tool? (Webpack/Vite)
↓
do the build job
But in fact, it's just the difference in behavior between assets/
& public/
, and Vue's dynamic binding :src
is processed at runtime, so it can't recognize those compile-time path resolution rules.