Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Assets with a dynamic URL are ignored #1265

Closed
SimonSapin opened this issue Dec 26, 2020 · 24 comments · Fixed by #8184
Closed

Assets with a dynamic URL are ignored #1265

SimonSapin opened this issue Dec 26, 2020 · 24 comments · Fixed by #8184
Labels
enhancement New feature or request

Comments

@SimonSapin
Copy link

SimonSapin commented Dec 26, 2020

Is your feature request related to a problem? Please describe.

When something like <img src=example.png> is used in a Vue template, vite build handles it by copying the PNG file into the dist directory with a hash in the filename etc. However this doesn’t happen when the image filename is dynamic. For example:

<ul>
  <li v-for="item in items">
    <img v-bind:src="`icons/${item.slug}.png`" />
    {{ item.name }}
  </li>
</ul>

The src attribute in the browser’s DOM are exactly the result of template interpolation, which works out with Vite’s own development server but not in "production" since image files are missing.

Describe the solution you'd like

I sort of understand why this doesn’t Just Work, but it’d be nice if it did. Alternatively, is there some other ways to tell Vite about which images exist? The value of item.slug is always in some finite set, although there are more of them that I’d rather hard-code in a template. Or, am I doing something very wrong and shouldn’t use reactive data for this? I’m very new to Vue.

Describe alternatives you've considered

Moving these images to the public directory would probably work, but Vite’s README describes this as an escape hatch that is best avoided.

@nandin-borjigin
Copy link

nandin-borjigin commented Dec 27, 2020

I'd say this is not a problem of vite. You cannot achieve same behavior even in other bundlers/scaffolders. As you said, if the value of item.slug is always in a fixed set, then you may import them explicitly into an object/arrary/variables and refer to them dynamically.

// images.js
import foo from 'xxx/foo.png'
import bar from 'xxx/bar.png'
export default {
  foo,
  bar
}

// Component.vue
<template>
    <img :src="images[item.slug]">
</template>


<script>
import images from 'images'

export default {
  data() {
    return {
      images
    }
  },
  created() {
    // alternatively, if you dont want images to be reactive
    this.images = images
  }
}

Or if images are too many to be imported explicitly, you may want to use rollup-plugin-dynamic-import-vars

@SimonSapin
Copy link
Author

Repeating the name of each image three times in a module that does nothing but reexport gets verbose quickly, and too easy to become out of date when there are a few hundred images.

https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars looks relevant, thanks! (Although the concept of "importing" PNG images into JS modules feels… unusual.)

@nandin-borjigin
Copy link

nandin-borjigin commented Dec 30, 2020

Repeating the name of each image three times in a module that does nothing but reexport gets verbose quickly
I don't see there is any unnecessary repetition. You need to import all images you are gonna use at least once somewhere.

And for the template context to refer to imported images' url, you need to bind that urls to vue component instance once (either using data or created).

Although the concept of "importing" PNG images into JS modules feels… unusual.

Nah, it's not importing images into JS modules, it's importing image url as a string into JS modules. What happens behind the scene is basically:

  1. rollup (which is used by vite under the hood) sees that foo.png is imported in somewhere (i.e., foo.png is in the depedency graph)
  2. rollup runs all plugins (which are pre-configured in vite source code) agains the module id (which is foo.png), giving plugins a chance to do some work.
  3. The image file request comes to the plugin that is designed to handle image requests. And the plugin finds out that this request should be taken care of it (via testing if the module id matches some regular expression like /*.(png|jpg|gif)/) and tells rollup it will handle this request and there is no need to call other plugins.
  4. The plugin reads the image file, generates a hash from the image file content, composes a file name (like foo-[hash].png), and tells rollup to copy foo.png to build directory and name the copy as foo-[hash].png.
  5. The plugin also tells rollup to resolve foo.png as a virtual JS module with content export default 'dist/foo-[hash].png'

So if you write some code like import foo from 'assets/foo.png', it actually means "Import the file name of the build copy of 'assets/foo.png' as a string variable called foo".

@SimonSapin
Copy link
Author

This helps understand how everything ties together, thanks.

Given that rollup needs to know about all URLs it is to manage it makes sense that URLs cannot be truly dynamic, for example based on user input. What I’m looking for is URLs that are dynamic as far as human-authored code (JS modules, Vue components, …) is concerned, but known at "project build" time. In my case they are derived from identifier in a (static) JSON file.

If import statements in JS source files were the only way to make rollup include a PNG file in the build I’d consider making a script that parses my JSON file and generates a JS file like you suggested:

import foo from 'xxx/foo.png'
import bar from 'xxx/bar.png'
// Dozens more…
export default {
  foo,
  bar,
  // Dozens more…
}

Does Vite support this kind of meta-programming, automatically re-generating generated code when its build script changes?

But since rollups has plugins I assume imports are not the only way and I could instead configure rollupInputOptions to use a plugin, perhaps a custom one. Though that plugin would probably still need to generate JS code, if only with an object literal that contains the generated URLs so I can access them in Vue components, right?

By the way, does Vite use rollup at all in dev mode?

@nandin-borjigin
Copy link

nandin-borjigin commented Jan 3, 2021

But since rollups has plugins I assume imports are not the only way and I could instead configure rollupInputOptions to use a plugin, perhaps a custom one.

Yes, partially. You can write a plugin to resolve your image requests as an in-memory virtual JS module (they dont even need to be emitted to the file system). However, you still need an import statement some where in your code to "trigger" your plugin to do that.

By the way, does Vite use rollup at all in dev mode?

Yes

@SimonSapin
Copy link
Author

The images are already on the filesystem, I just need them copied to dist/. I’m not even attached to the hash-in-filenames thing since the images contents are unlikely to change. (New ones might be added later.) Maybe using public/ is what I need after all?

If a plugin triggered by a single JS import can then programmatically emit many "requests" during build mode, that sounds like what I want. I look into it further, thanks!

Feel free to close as "scenario can (probably) be supported by a custom plugin", or leave this open as a feature request. This seems to me like a not-so uncommon pattern that could potentially be supported directly by Vite, but I’m not sure yet what that feature would look like.

@nandin-borjigin
Copy link

nandin-borjigin commented Jan 4, 2021

Maybe using public/ is what I need after all?

Yes, if copying static assets to output directory is the only thing you want.

leave this open as a feature request

I'm not really sure about what the missing feature is actually. If you want to import some images programmatically (of course from a fixed set), it is supported already, but you need to some how write the code to import them (that's ... programmatically); If you want to have an array or an object holding all the hashed urls to refer to certain asset by code (like images[item.slug]), it is supported already, but you need to somehow construct the array or the object; If you want to just copy some images to output directory, as said above, public directory is what you may need.

BTW, I'm not from the team so cannot close issues :)

@yyx990803 yyx990803 added the enhancement New feature or request label Jan 5, 2021
@NiaMori
Copy link

NiaMori commented Jan 10, 2021

@SimonSapin

Does Vite support this kind of meta-programming, automatically re-generating generated code when its build script changes?

After vite 2.0.0-beta.17 you can try glob-import to import all resources at once.

@sampullman
Copy link

@SimonSapin I wrote a plugin to help transition a few medium sized vue2/webpack projects to vue3/vite that solves a similar problem, though I didn't have your exact use case in mind.

It's probably not a good long term solution, but was convenient for quickly converting our old require(img) style of code. It allows you to do something like this:

    <img
      v-for="image in [
        { title: 'Test1 title', src: Test1 },
        { title: 'Test2 title', src: Test2 },
        { title: 'Test3 title', src: Test3 },
      ]"
      :src="image.src"
      :title="image.title"
    >

which imports and exposes e.g. src/assets/img/test1.png, etc. according to some basic rules. The glob-import might be cleaner for you, and for what it's worth, I'm still looking for a cleaner way to simplify our image imports.

@amir20
Copy link

amir20 commented Mar 15, 2021

As mentioned in #1265 (comment), I tried using glob like const images = import.meta.globEager("/src/images/*.png");. It wasn't working at first, then I realized that you need to use .default like images["/src/images/image.png"].default.

Just FYI for any one else trying to use globs with static assets.

@ParadiseLayal
Copy link

vite 引入图片资源
html:

script:

import on from '@/assets/voice.png'
import off from '@/assets/no_voice.png'

const state = reactive({
voiceSrc: off
})

@GhimpuLucianEduard
Copy link

Based on @amir20 I ended up doing something like this:

export default function useAssets() {
  const svgs = import.meta.globEager('/src/assets/*.svg');
  const pngs = import.meta.globEager('/src/assets/*.png');
  const jpegs = import.meta.globEager('/src/assets/*.jpeg');

  return {
    aboutImage: svgs['/src/assets/aboutImage.svg'].default,
    search: svgs['/src/assets/search.svg'].default,
    info: pngs['/src/assets/info.png'].default,
  }; 
}

Then in any file:

<template>
    <div>
      <img :src="assets.info">
    </div>
</template>
  
<script lang="ts">
import { defineComponent } from '@vue/runtime-core';
import useAssets from '../composable/useAssets';
  
export default defineComponent({
setup() {
  const assets = useAssets();
    return {
      assets,
    };
  },
});
</script>

This way you can keep all your assets in one place and avoid magic strings.
It's a little bit more work to add each asset in the composable return, but in the long run it's quite nice,

tianzhou added a commit to bytebase/bytebase that referenced this issue Aug 19, 2021
…c instead

See vitejs/vite#1265

The discussion provides some workaround, but author couldn't figure it out properly.
@yannxaver
Copy link

As mentioned in #1265 (comment), I tried using glob like const images = import.meta.globEager("/src/images/*.png");. It wasn't working at first, then I realized that you need to use .default like images["/src/images/image.png"].default.

Just FYI for any one else trying to use globs with static assets.

Did you chose globEager() instead of glob() on purpose? If I understood the Vite docs correctly globEager() imports all assets directly (no lazy-loading). Depending on the number of assets this could have a performance impact.

Here is an example using glob() if you want to lazy-load an asset based on a prop in your component.

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

<script lang="ts">
import { defineComponent, onMounted, ref } from "vue";

export default defineComponent({
  props: {
    slug: {
      type: String,
      required: true,
    },
  },
  setup(props) {
    const allImages = import.meta.glob("../assets/img/*.jpg");
    let chosenImageSource = ref("");

    onMounted(() => {
      allImages[`../assets/img/${props.slug}.jpg`]().then((mod) => {
        chosenImageSource.value = mod.default;
      });
    });

    return { chosenImageSource };
  },
});
</script>

Would this be a good way to handle it or is there a more simple or straightforward way of solving this?

@valc93
Copy link

valc93 commented Feb 3, 2022

Using vite 2.7.2
Hi! I was actually trying to solve this same problem, but being relatively new to vue I was having trouble using any of the solutions listed above. After googling for a little more, I stumbled on this component example by tony19, and it worked wonderfully for loading an image URL dynamically. https://stackoverflow.com/a/69701353

From the example, my Image component ended up looking like this:

<script setup>
import { ref, watchEffect } from 'vue'

const props = defineProps({
  path: { type: String },
  alt: { type: String },
  css: { type: String },
})

const image = ref()

watchEffect(async () => {
  image.value = (await import(/* @vite-ignore */ `../assets/${props.path}.jpg`)).default
})
</script>

<template>
  <img :src="image" :alt="alt" :class="css" />
</template>

And then using it this way:

<div v-for="series in leadership" :key="series.title" class="flex flex-col gap-6 bg-sky-50 rounded-lg border-4 border-sky-500">
  <Image :path="series.banner" :alt="`${series.title} Banner`" css="hidden sm:block rounded-xl w-full h-auto px-4" />
</div>

Probably the only caveat was hard-coding the type of image, in this case jpg. The build command wouldn't continue if that part wasn't static.

[plugin:rollup-plugin-dynamic-import-variables] invalid import "import(/* @vite-ignore */ `../assets/${props.path}`)". A file extension must be included in the static part of the import. For example: import(`./foo/${bar}.js`).

From the error I also understood that, as @nandin-borjigin commented, using rollup-plugin-dynamic-import-vars was the way to go. I didn't have to add it, it was already included in vite 2.7.2. Also noticed the default at the end of the line, just like @amir20 mentioned as well.

Thanks, everyone, for the help in figuring this out. 💪💪

@Utitofon-Udoekong
Copy link

Utitofon-Udoekong commented Feb 9, 2022

This works perfectly fine during local development but after deploying to a service like netlify, I get errors like
Screenshot_9

Any Ideas on how we can work around this?

@valc93
Copy link

valc93 commented Feb 9, 2022 via email

@Utitofon-Udoekong
Copy link

Utitofon-Udoekong commented Feb 9, 2022

Okay. That was quick
Here's the image component
Screenshot_10

I used it this way in a v-for
Screenshot_11

Then the composables where the v-for was generated just a simple array of objects

Screenshot_12

And I'm pretty sure that URL is correct
Screenshot_13

@valc93

@Utitofon-Udoekong
Copy link

Utitofon-Udoekong commented Feb 9, 2022

Here are the latest errors from my browser, I'm using Firefox developers edition
Screenshot_14

Thing is it works perfectly locally but after deployment no way

@Utitofon-Udoekong
Copy link

Any idea what's wrong?

@valc93
Copy link

valc93 commented Feb 10, 2022

After testing a few things, I believe the problem has to do with the path you used: "companies/catalog". You have to use the filename alone. "Companies" would have to be hard-coded. Obviously, this is already a hacky method of doing things, so I haven't thought up a more elegant solution. You can solve this in two different ways: (1) Create an image component for just images in your catalog folder (i.e. image.value = (await import(/* @vite-ignore */ `../assets/images/companies/${props.path}.jpg`)).default) or (2) create a component that assumes there will be one level down directory as well.

Here's an image component, like I mentioned in the second solution. As well as a few updates that I did to my own image component.

//Two-levels Image Component
<script setup>
const props = defineProps({
  path: { type: String },
  alt: { type: String },
})

const fileType = props.path.match(/\.[0-9a-z]+$/i)
const cleanPath = props.path.replace(fileType,'')
const pathArray = cleanPath.split('/')
const folderPath = pathArray[pathArray.length - 2]
const imagePath = pathArray[pathArray.length - 1]

const image = ref()

watchEffect(async () => {
  switch (fileType[0]) {
    case '.jpg':
      image.value = (await import(/* @vite-ignore */ `../assets/images/${folderPath}/${imagePath}.jpg`)).default
      break;
    case '.jpeg':
      image.value = (await import(/* @vite-ignore */ `../assets/images/${folderPath}/${imagePath}.jpeg`)).default
      break;
    case '.png':
      image.value = (await import(/* @vite-ignore */ `../assets/images/${folderPath}/${imagePath}.png`)).default
      break;
    case '.svg':
      image.value = (await import(/* @vite-ignore */ `../assets/images/${folderPath}/${imagePath}.svg`)).default
      break;
    default:
      console.log(`Sorry, the image component can't recognize the ${fileType} file type just yet.`);
  }
})
</script>

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

A few things that differ in this new component from the old one:

  1. You can now include the file type. It will be automatically removed from the path and then used to decide which import statement to use. Not super elegant because it has much repetition, but it's the best I have come up with so far to avoid making several image components.
  2. I've deleted the CSS props simply because you can use class on the Vue template anyway, so it was unnecessary.
  3. This template assumes that you will use a folder name and an image name, so just keep that in mind.

@Utitofon-Udoekong
Copy link

Utitofon-Udoekong commented Feb 11, 2022 via email

@Utitofon-Udoekong
Copy link

It works now forgot to add the file type 😅😅

@Utitofon-Udoekong
Copy link

Utitofon-Udoekong commented Feb 11, 2022 via email

@anupal-cpu
Copy link

thanks man

@github-actions github-actions bot locked and limited conversation to collaborators May 30, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.