Vue.js

Vue 3で画像を表示する方法|Viteのassets・public・動的srcを解説

Vue 3 + Viteで画像を表示する方法を解説します。src/assetsの静的import、public、動的な:src、new URL、import.meta.glob、CSS背景画像、@ alias、require・~@が動かない理由、Nuxt 4との違いと本番buildの404対策まで整理します。

この記事の目次
  1. Vue 3 + Viteの画像folder構成
  2. src/assetsの画像をstatic importする
  3. @ aliasを使う
  4. templateへ静的なsrcを書く
  5. srcsetもbuild対象にする
  6. publicの画像を表示する
  7. 動的な:srcへimportしたURLを渡す
  8. 文字列連結だけではassets画像が解決しない理由
  9. import.meta.glob()で複数画像を読み込む
  10. new URL()とimport.meta.urlを使う
  11. require()を使わない
  12. @・~@・相対pathの違い
  13. CSSのbackground-imageでassets画像を使う
  14. 動的なbackground-imageを設定する
  15. Nuxt 4のassetsとpublic
  16. alt・width・height・loadingを設定する
  17. 本番buildで画像URLを確認する
  18. 画像が表示されないときのチェックリスト
  19. pathと大文字・小文字を確認する
  20. @ aliasの設定を確認する
  21. require is not definedが出る
  22. 開発中は見えるがbuild後に404になる
  23. new URL()でalias pathが動かない
  24. import.meta.glob()でundefinedになる
  25. Git LFSの画像だけ壊れる
  26. まとめ

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になる可能性があります。

解決方法は次のいずれかです。

  1. 候補をstatic importしたmapから選ぶ
  2. import.meta.glob()で対象folderを明示する
  3. 分析可能なrelative patternでnew URL()を使う
  4. 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 importnew 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.pnglogo.pngが別fileです。folder名、extension、caseを実fileと一致させます。

@ aliasの設定を確認する

@が解決できない場合、vite.configresolve.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/assetspublicを使い分ける

最後にproduction buildとNetwork panelでURLを確認し、alt・寸法・loadingも含めて画像componentの品質を整えます。