Vue 3 + Viteで画像を表示する基本は、画像fileをJavaScript moduleとしてimportし、解決済みURLを:srcへ渡す方法です。
<script setup>
import logoUrl from "@/assets/images/logo.svg";
</script>
<template>
<img
:src="logoUrl"
alt="Example社"
width="240"
height="80"
>
</template>
Viteはimportされたassetをbuild graphへ含め、開発中は解決済みURL、本番buildではhash付きfile名などへ変換します。
画像の置き場所と指定方法は、次のように選びます。
| 置き場所 | 参照方法 | 向いている用途 |
|---|---|---|
src/assets |
static import・template URL | componentから使い、hash・build処理したい画像 |
public |
/images/example.png |
file名を維持する画像、source codeからimportしないfile |
| 外部URL | https://... |
CDN・CMSなど別originで管理する画像 |
Vue 3 + Viteの画像folder構成
Vue公式のcreate-vueで作るVite projectを想定します。
project/
├─ public/
│ └─ images/
│ └─ og-image.jpg
├─ src/
│ ├─ assets/
│ │ └─ images/
│ │ ├─ logo.svg
│ │ ├─ hero.webp
│ │ └─ products/
│ │ ├─ apple.jpg
│ │ └─ orange.jpg
│ ├─ components/
│ │ └─ ProductCard.vue
│ └─ App.vue
└─ vite.config.js
assetsというfolder名自体にVue runtimeの特別な意味があるわけではありません。Viteがsource codeから参照されたassetを解析できることが重要です。
src/assetsの画像をstatic importする
最も予測しやすい方法です。
<script setup>
import heroUrl from "../assets/images/hero.webp";
</script>
<template>
<img
:src="heroUrl"
alt="山に囲まれた湖"
width="1280"
height="720"
>
</template>
import結果は画像binaryそのものではなく、browserから参照できる解決済みURLです。小さいassetはVite設定によりdata URLへinline化される場合があります。
@ aliasを使う
@をsrcへ向けて設定しているprojectでは、componentの深さに関係なく次のように書けます。
import logoUrl from "@/assets/images/logo.svg";
Viteそのものがすべてのprojectへ@を自動設定するわけではありません。vite.config.jsを確認します。
import { fileURLToPath, URL } from "node:url";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
});
TypeScriptやeditor補完用のalias設定もprojectのtsconfig構成に合わせます。Viteのruntime解決とTypeScriptの型解決は別設定です。
templateへ静的なsrcを書く
Vue SFC templateの静的asset URLは、Vue用Vite pluginによってimportへ変換されます。
<template>
<img
src="../assets/images/logo.svg"
alt="Example社"
width="240"
height="80"
>
</template>
pathは.vue fileからのrelative pathです。aliasが設定され、pluginがそのURLを変換する構成なら@/assets/...も利用できますが、明示的importのほうがscript・型・再利用との関係を追いやすい場合があります。
srcsetもbuild対象にする
staticなsrcsetのasset URLもpluginが処理できる構成があります。
<template>
<img
src="../assets/images/hero-1280.webp"
srcset="
../assets/images/hero-640.webp 640w,
../assets/images/hero-1280.webp 1280w
"
sizes="(max-width: 720px) 100vw, 1280px"
alt="山に囲まれた湖"
width="1280"
height="720"
>
</template>
使用中のplugin versionとbuild結果でURL変換を確認してください。複雑なresponsive imageはimportしたURLをbindingする方法もあります。
publicの画像を表示する
public/images/og-image.jpgは、source codeからimportせずroot absolute pathで参照します。
<template>
<img
src="/images/og-image.jpg"
alt="記事のメインビジュアル"
width="1200"
height="630"
>
</template>
Viteはpublic内のfileをbuild時にそのままdist rootへcopyします。
- file名へhashを付けない
- source import graphへ含めない
- rootの
/から参照する
favicon、robots.txt、固定file名が必要なOG画像などに向きます。一般的なcomponent画像はimportを優先し、publicを「動かなかったpathの避難場所」にしないほうが管理しやすくなります。
動的な:srcへimportしたURLを渡す
画像候補が少ない場合は、すべてstatic importしてobject mapへまとめるのが安全です。
<script setup>
import appleUrl from "@/assets/images/products/apple.jpg";
import orangeUrl from "@/assets/images/products/orange.jpg";
const imageUrls = {
apple: appleUrl,
orange: orangeUrl,
};
const props = defineProps({
product: {
type: Object,
required: true,
},
});
</script>
<template>
<img
:src="imageUrls[props.product.imageKey]"
:alt="props.product.name"
width="640"
height="480"
loading="lazy"
>
</template>
build toolはimport文から対象fileを確実に発見できます。CMSやAPIからは任意pathではなくappleのような許可済みkeyを受け取ります。
文字列連結だけではassets画像が解決しない理由
統合元記事には次のような例がありました。
<!-- build後に期待どおり解決できない例 -->
<img :src="`../assets/images/${item.image}.jpg`" :alt="item.name">
runtimeで完成するstringだけでは、Viteがbuild時にどの画像fileを含め、どのhash付きURLへ変換すべきか判断できません。開発serverで偶然見えてもproduction buildで404になる可能性があります。
解決方法は次のいずれかです。
- 候補をstatic importしたmapから選ぶ
import.meta.glob()で対象folderを明示する- 分析可能なrelative patternで
new URL()を使う - build処理が不要ならpublicまたはCMSの完全URLを使う
import.meta.glob()で複数画像を読み込む
画像数が多く、folder内のfileをkeyから選びたい場合はViteのimport.meta.glob()を使えます。
<script setup>
const imageModules = import.meta.glob(
"/src/assets/images/products/*.{jpg,jpeg,png,webp}",
{
eager: true,
query: "?url",
import: "default",
},
);
function getProductImage(fileName) {
const key = `/src/assets/images/products/${fileName}`;
return imageModules[key] ?? imageModules[
"/src/assets/images/products/fallback.webp"
];
}
const props = defineProps({
product: {
type: Object,
required: true,
},
});
</script>
<template>
<img
:src="getProductImage(props.product.imageFile)"
:alt="props.product.name"
width="640"
height="480"
loading="lazy"
>
</template>
glob patternはliteralで書く必要があります。使用中のVite versionによりquery optionの書式が異なる場合があるため、projectのversionに対応する公式資料とbuild結果を確認してください。
file名を外部入力から受ける場合でも、glob結果に存在するkeyだけを使います。../を含む任意pathを組み立てないでください。
new URL()とimport.meta.urlを使う
native ESMのimport.meta.urlを基準にrelative URLを解決できます。
const logoUrl = new URL(
"../assets/images/logo.svg",
import.meta.url,
).href;
限定されたdynamic patternもViteがbuild時に展開できます。
function getImageUrl(name) {
return new URL(
`../assets/images/products/${name}.jpg`,
import.meta.url,
).href;
}
ただし、任意の変数だけを渡す次の形はViteがtransformできません。
// Viteが対象fileを静的解析できない
const imageUrl = new URL(imagePath, import.meta.url).href;
new URL("@/assets/...", import.meta.url)ではなく、現在のmoduleからのrelative URLを使います。またVite SSRではbrowserとNode.jsでimport.meta.urlの意味が異なるため、このpatternをそのまま使えません。SSR対応が必要ならstatic import・framework asset API・明示的mapを優先します。
require()を使わない
旧Vue CLI・webpack例では次のような書き方が使われていました。
// Vue 3 + Viteでは推奨しない
const imageUrl = require("@/assets/images/logo.png");
Viteのbrowser sourceはES Modulesを前提にし、CommonJSのrequireが定義されないことがあります。Vue 3だからrequireが必要・不要なのではなく、使用するbuild toolでasset解決方法が変わります。
Viteではstatic import、new URL()、import.meta.glob()、publicを使います。
@・~@・相対pathの違い
| 表記 | 意味 | Viteでの扱い |
|---|---|---|
../assets/image.png |
現在fileからのrelative path | 基本的に利用可能 |
@/assets/image.png |
設定されたalias | resolve.aliasが必要 |
~@/assets/image.png |
webpack loader由来のmodule request表記 | Viteの一般的な書き方ではない |
/images/image.png |
site rootからのURL | public assetで使用 |
古い記事から~@をコピーせず、現在のprojectがVite・webpack・Nuxtのどれか、aliasがどう設定されているかを確認します。
CSSのbackground-imageでassets画像を使う
SFCのstyle blockでは、CSS fileからのrelative URLを使えます。ViteはCSSのurl()もassetとして処理します。
<template>
<header class="hero">
<h1>Vue Image Guide</h1>
</header>
</template>
<style scoped>
.hero {
min-height: 60vh;
background-image: url("../assets/images/hero.webp");
background-position: center;
background-size: cover;
}
</style>
通常のCSS commentは/* ... */です。旧記事の//commentはSCSSなどのpreprocessorなしでは無効です。
CSS background全般はCSS backgroundの使い方で解説しています。
動的なbackground-imageを設定する
画像URLをimportし、style bindingへ渡します。
<script setup>
import heroUrl from "@/assets/images/hero.webp";
const heroStyle = {
backgroundImage: `url("${heroUrl}")`,
};
</script>
<template>
<header class="hero" :style="heroStyle">
<h1>Vue Image Guide</h1>
</header>
</template>
外部入力の任意URLをstyleへ直接入れず、許可済みURL mapやCMSのvalidation済みURLを使います。Vueのstyle bindingはVueの:classと:styleで扱います。
Nuxt 4のassetsとpublic
NuxtはVue + Vite projectとdirectory構成・aliasが異なります。Nuxt 4では主に次の2つを使います。
public/: server rootからそのまま配信app/assets/: Viteまたはwebpackで処理するasset
public画像はroot URLで参照します。
<template>
<img src="/images/nuxt.png" alt="Nuxt">
</template>
build処理する画像はNuxtのaliasを使えます。
<template>
<img src="~/assets/images/nuxt.png" alt="Nuxt">
</template>
Nuxt 2のstatic/はNuxt 4ではpublic/に相当します。画像最適化・responsive size・provider変換が必要なら、Nuxt Image moduleの<NuxtImg>も検討します。
alt・width・height・loadingを設定する
画像pathが解決しても、HTMLとして適切とは限りません。
<img
:src="product.imageUrl"
:alt="product.name"
width="640"
height="480"
loading="lazy"
decoding="async"
>
- 意味のある画像は内容・目的が伝わるaltを書く
- 装飾だけの画像は
alt=""にする - 縦横比が分かるwidth・heightを指定してlayout shiftを減らす
- 最初のviewport外にある画像は
loading="lazy"を検討する - heroなどLCP候補を一律lazy loadingしない
responsive layoutの基本はレスポンシブWebデザインの基礎も参照してください。
本番buildで画像URLを確認する
開発serverだけでなくproduction buildを実行します。
npm run build
npm run preview
確認項目は次のとおりです。
- browser consoleにmodule・URL errorがない
- Network panelで画像がHTTP 200
- distのhash付きassetを正しく参照している
- subdirectory公開時にViteの
base設定とURLが一致する - 未使用画像を意図せず大量にbundleしていない
runtimeでbase pathを組み立てる必要がある場合はimport.meta.env.BASE_URLも使えますが、static importならViteがURLを処理します。
画像が表示されないときのチェックリスト
pathと大文字・小文字を確認する
macOSやWindowsで動いても、Linux本番環境ではLogo.pngとlogo.pngが別fileです。folder名、extension、caseを実fileと一致させます。
@ aliasの設定を確認する
@が解決できない場合、vite.configのresolve.aliasとTypeScript設定を確認します。一度relative importで動くか試すと、aliasとassetの問題を分けられます。
require is not definedが出る
ViteのES ModulesでCommonJSのrequire()を使っています。static import、new URL()、import.meta.glob()へ変更します。
開発中は見えるがbuild後に404になる
runtime文字列だけでsrc/assetsを参照していないか、Viteのbase、publicのroot path、case-sensitive pathを確認します。必ずnpm run previewでdistを配信して試します。
new URL()でalias pathが動かない
new URL()はmoduleからのrelative pathを使います。任意変数や@を含むURLではViteがassetを静的解析できないことがあります。
import.meta.glob()でundefinedになる
globが返すobjectのkeyと、組み立てたkeyが一致しているかconsoleで確認します。leading slash、/src、extension、subfolderの有無がずれやすい点です。
Git LFSの画像だけ壊れる
repositoryへLFS pointerだけがあり、実体を取得していない可能性があります。build環境でもGit LFS objectを取得できているか確認します。
まとめ
Vue 3 + Viteの画像pathは、build toolが対象fileを発見できる形で書くことが重要です。
src/assetsはstatic importを基本にする- 静的template URLはVue pluginがimportへ変換する
- 動的srcは明示的mapまたは
import.meta.glob()を使う new URL()は分析可能なrelative pathで使う- 固定file名が必要ならpublicからroot pathで参照する
- Viteではwebpack由来の
require()・~@を前提にしない - Nuxt 4では
app/assetsとpublicを使い分ける
最後にproduction buildとNetwork panelでURLを確認し、alt・寸法・loadingも含めて画像componentの品質を整えます。