Vue 3 Vite - dynamic img src

Viewed 23900

I'm using Vue 3 with Vite. And I have a problem with dynamic img src after Vite build for production. For static img src there's no problem.

<img src="/src/assets/images/my-image.png" alt="Image" class="logo"/>

It works well in both cases: when running in dev mode and after vite build as well. But I have some image names stored in database loaded dynamically (Menu icons). In that case I have to compose the path like this:

<img :src="'/src/assets/images/' + menuItem.iconSource" />

(menuItem.iconSource contains the name of the image like "my-image.png"). In this case it works when running the app in development mode, but not after production build. When Vite builds the app for the production the paths are changed (all assests are put into _assets folder). Static image sources are processed by Vite build and the paths are changed accordingly but it's not the case for the composed image sources. It simply takes /src/assets/images/ as a constant and doesn't change it (I can see it in network monitor when app throws 404 not found for image /src/assets/images/my-image.png). I tried to find the solution, someone suggests using require() but I'm not sure vite can make use of it.

9 Answers

Update 2022: Vite 3.0.9 + Vue 3.2.38

Solutions for dynamic src binding:

1. With static URL

<script setup>
import imageUrl from '@/assets/images/logo.svg' // => or relative path
</script>

<template>
  <img :src="imageUrl" alt="img" />
</template>

2. With dynamic URL & relative path

<script setup>
const imageUrl = new URL(`./dir/${name}.png`, import.meta.url).href
</script>

<template>
  <img :src="imageUrl" alt="img" />
</template>

3.With dynamic URL & absolute path

Due to Rollup Limitations, all imports must start relative to the importing file and should not start with a variable.

You have to replace the alias @/ with /src

<script setup>
const imageUrl = new URL('/src/assets/images/logo.svg', import.meta.url)
</script>

<template>
  <img :src="imageUrl" alt="img" />
</template>


2022 answer: Vite 2.8.6 + Vue 3.2.31

Here is what worked for me for local and production build:

<script setup>
const imageUrl = new URL('./logo.png', import.meta.url).href
</script>

<template>
<img :src="imageUrl" />
</template>

Note that it doesn't work with SSR


Vite docs: new URL

Following the Vite documentation you can use the solution mentioned and explained here:

vite documentation

const imgUrl = new URL('./img.png', import.meta.url)

document.getElementById('hero-img').src = imgUrl

I'm using it in a computed property setting the paths dynamically like:

    var imagePath = computed(() => {
      switch (condition.value) {
        case 1:
          const imgUrl = new URL('../assets/1.jpg',
            import.meta.url)
          return imgUrl
          break;
        case 2:
          const imgUrl2 = new URL('../assets/2.jpg',
            import.meta.url)
          return imgUrl2
          break;
        case 3:
          const imgUrl3 = new URL('../assets/3.jpg',
            import.meta.url)
          return imgUrl3
          break;
      }
    });

Works perfectly for me.

require() or import() used to solve this scenario in Webpack based Vue apps are indeed Webpack only constructs

Behind the scenes Webpack handles such dynamic imports by making every single file in fixed part of the path (/src/assets/images/) part of the app bundle and available for import. In the case of images, that usually means creating some kind of internal map like this:

{
 'src/assets/images/image1.png' : 'assets/img/image1.hash.png',
  ...
}

The key is the path of the image in the source tree. The value is the path created by the loader responsible for handling images during build (path of image in bundled app). This map is used by Webpack at runtime to "resolve" dynamically generated paths to real paths of images

In Vite there is no require() but you can do something similar by using Glob Import to create a "map" similar to the one described above and the use this map yourself

Or you can use this plugin to do something similar in less transparent way...

More details in this GitHub issue

UPDATE

As Vite is using Rollup behind the scenes, using plugin-dynamic-import-vars might be another solution, with the usage very similar to the Webpack...

BUT this currently have a limitation of only allowing relative paths which makes the Glob Imports probably better choice

Please try the following methods

const getSrc = (name) => {
    const path = `/static/icon/${name}.svg`;
    const modules = import.meta.globEager("/static/icon/*.svg");
    return modules[path].default;
  };

The simplest solution I've found for this is to put your images in the public folder located in your directory's root.

You can, for example, create an images folder inside the public folder, and then bind your images dynamically like this:

<template>
  <img src:="`/images/${ dynamicImageName }.jpeg`"/>
</template>

Now your images should load correctly in both development and production.

Use Vite's API import.meta.glob works well, I refer to steps from docs of webpack-to-vite. It lists some conversion items and error repair methods. It can even convert an old project to a vite project with one click. It’s great, I recommend it!

  1. create a Model to save the imported modules, use async methods to dynamically import the modules and update them to the Model
// src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
const assets = import.meta.glob('../assets/**')
Vue.use(Vuex)
export default new Vuex.Store({
  state: {
    assets: {}
  },
  mutations: {
    setAssets(state, data) {
      state.assets = Object.assign({}, state.assets, data)
    }
  },
  actions: {
    async getAssets({ commit }, url) {
      const getAsset = assets[url]
      if (!getAsset) {
        commit('setAssets', { [url]: ''})
      } else {
        const asset = await getAsset()
        commit('setAssets', { [url]: asset.default })
      }
    }
  }
})
  1. use in .vue SFC
// img1.vue
<template>
  <img :src="$store.state.assets['../assets/images/' + options.src]" />
</template>
<script>
export default {
  name: "img1",
  props: {
    options: Object
  },
  watch: {
    'options.src': {
      handler (val) {
        this.$store.dispatch('getAssets', `../assets/images/${val}`)
      },
      immediate: true,
      deep: true
    }
  }
}
</script>

In the context of vite@2.x, you can use new URL(url, import.meta.url) to construct dynamic paths. This pattern also supports dynamic URLs via template literals. For example:

<img :src="`/src/assets/images/${menuItem.iconSource}`" />

However you need to make sure your build.target support import.meta.url. According to Vite documentation, import.meta is a es2020 feature but vite@2.x use es2019 as default target. You need to set esbuild target in your vite.config.js:

  // vite.config.js
  export default defineConfig({
    // ...other configs

    optimizeDeps: {
      esbuildOptions: {
        target: 'es2020'
      }
    },

    build: {
      target: 'es2020'
    }
  })

My enviroment:

  • vite v2.9.13
  • vue3 v3.2.37

In vite.config.js, assign @assets to src/assets

'@assets': resolve(__dirname, 'src/assets')

Example codes:

<template>
    <div class="hstack gap-3 mx-auto">
        <div class="form-check border" v-for="p in options" :key="p">
            <div class="vstack gap-1">
                <input class="form-check-input" type="radio" name="example" v-model="selected">
                <img :src="imgUrl(p)" width="53" height="53" alt="">
            </div>
        </div>
    </div>
</template>

<script>

import s1_0 from "@assets/pic1_sel.png";
import s1_1 from "@assets/pic1_normal.png";

import s2_0 from "@assets/pic2_sel.png";
import s2_1 from "@assets/pic2_normal.png";

import s3_0 from "@assets/pic3_sel.png";
import s3_1 from "@assets/pic3_normal.png";

export default {
    props: {
        'options': {
            type: Object,
            default: [1, 2, 3, 4]
        }
    },
    data() {
        return {
            selected: null
        }
    },
    methods: {
        isSelected(val) {
            return val === this.selected;
        },
        imgUrl(val) {
            let isSel = this.isSelected(val);
            switch(val) {
                case 1:
                case 2:
                    return (isSel ? s1_0 : s1_1);

                case 3:    
                case 4:
                    return (isSel ? s2_0 : s2_1);
                    
                default:
                    return (isSel ? s3_0 : s3_1);
            }
        }
    }
}
</script>

References:

enter image description here

Memo:

About require solution.

  • "Cannot find require variable" error from browser. So the answer with require not working for me.
  • It seems nodejs >= 14 no longer has require by default. See this thread. I tried the method, but my Vue3 + vite give me errors.

If you have a limited number of images to use, you could import all of them like this into your component. You could then switch them based on a prop to the component. enter image description here

Importing images

Related