URL-импорты через суффикс ?url

В Vite любой импорт файла по умолчанию обрабатывается как ресурс сборщика. Однако в ряде случаев требуется получить не содержимое файла и не модуль, а именно итоговый URL до ресурса. Для этого используется специальный суффикс ?url.

Импорт с ?url заставляет Vite вернуть строку с адресом файла:

import imageUrl from './images/logo.png?url'

console.log(imageUrl)

Результатом будет строка:

'/assets/logo.8d7f3a.png'

Во время разработки URL указывает на dev-сервер Vite, а после production-сборки — на итоговый файл в каталоге dist/assets.


Базовый принцип работы

Без ?url Vite пытается определить тип ресурса автоматически:

import logo from './logo.svg'

Для SVG возможны разные сценарии:

  • возврат URL;
  • обработка плагином;
  • преобразование в компонент;
  • inline-встраивание.

Суффикс ?url отключает подобную неоднозначность и явно сообщает:

требуется строковый URL до файла.

Пример:

import fileUrl from './document.pdf?url'

Теперь переменная содержит только путь:

'/assets/document.a1b2c3.pdf'

Когда используется ?url

Получение URL для DOM API

Некоторые браузерные API требуют именно строковый путь:

import workerUrl from './worker.js?url'

const worker = new Worker(workerUrl, {
    type: 'module'
})

Передача пути сторонним библиотекам

Многие библиотеки работают исключительно с URL:

import textureUrl from './textures/wood.jpg?url'

engine.loadTexture(textureUrl)

Динамическое создание элементов

import videoUrl from './video/demo.mp4?url'

const video = document.createElement('video')

video.src = videoUrl
video.controls = true

document.body.append(video)

Работа с аудио

import soundUrl from './audio/click.mp3?url'

const audio = new Audio(soundUrl)

audio.play()

Передача URL в CSS-переменные

import bgUrl from './bg.jpg?url'

document.documentElement.style.setProperty(
    '--bg-image',
    `url(${bgUrl})`
)

Отличие от обычного импорта ресурсов

Обычный импорт

import image from './photo.png'

Vite самостоятельно решает:

  • инлайнить файл;
  • вынести в assets;
  • обработать плагином;
  • преобразовать содержимое.

Импорт с ?url

import imageUrl from './photo.png?url'

Результат всегда предсказуем:

  • файл становится отдельным ресурсом;
  • импорт возвращает строку URL.

Работа во время разработки

В dev-режиме Vite не выполняет полноценную production-сборку. URL формируются напрямую через сервер разработки.

Пример:

import imageUrl from './logo.png?url'

Результат:

'/src/logo.png'

или:

'http://localhost:5173/src/logo.png'

в зависимости от конфигурации.


Работа после сборки

После выполнения:

vite build

файл получает хэш:

/assets/logo.2af61d.png

Импорт автоматически обновляется:

import imageUrl from './logo.png?url'

становится:

'/assets/logo.2af61d.png'

Это обеспечивает:

  • корректное кеширование;
  • защиту от устаревших ресурсов;
  • автоматическое обновление браузерного кеша.

Использование с JavaScript-файлами

?url работает не только с медиафайлами.

Пример:

import scriptUrl from './external.js?url'

Теперь можно динамически подключать скрипт:

const script = document.createElement('script')

script.src = scriptUrl

document.head.append(script)

Использование с Web Workers

Один из наиболее распространённых сценариев.

Без ?url

new Worker('./worker.js')

Такой код часто ломается после сборки.


С ?url

import workerUrl from './worker.js?url'

const worker = new Worker(workerUrl, {
    type: 'module'
})

Vite корректно обработает:

  • путь;
  • хэширование;
  • перенос worker-файла в assets.

Использование с WASM

WebAssembly-файлы часто импортируются именно через ?url.

import wasmUrl from './math.wasm?url'

Загрузка:

const response = await fetch(wasmUrl)

const bytes = await response.arrayBuffer()

const result = await WebAssembly.instantiate(bytes)

Использование с PDF

import pdfUrl from './manual.pdf?url'

window.open(pdfUrl)

Использование с архивами

import archiveUrl from './backup.zip?url'

Создание ссылки:

const a = document.createElement('a')

a.href = archiveUrl
a.download = 'backup.zip'

a.click()

Отличие ?url от ?raw

?url

Возвращает путь:

import file from './text.txt?url'

Результат:

'/assets/text.a12f.txt'

?raw

Возвращает содержимое файла:

import text from './text.txt?raw'

Результат:

'Hello world'

Отличие ?url от inline-импорта

Inline-режим

import img from './small.png?inline'

Файл преобразуется в Base64.


URL-режим

import img from './small.png?url'

Файл остаётся отдельным ресурсом.


Комбинирование с new URL()

В современном JavaScript часто используется конструкция:

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

Vite умеет обрабатывать такой код автоматически.

Однако между подходами есть различия.


new URL()

const imageUrl = new URL(
    './image.png',
    import.meta.url
).href

Плюсы:

  • нативный браузерный API;
  • совместимость с ESM.

Минусы:

  • более многословный синтаксис;
  • сложнее динамически комбинировать.

?url

import imageUrl from './image.png?url'

Плюсы:

  • лаконичность;
  • удобство;
  • предсказуемость;
  • интеграция с Vite.

Динамический импорт URL

Можно загружать ресурсы динамически:

const imageModule = await import(
    './images/banner.png?url'
)

console.log(imageModule.default)

Поведение TypeScript

TypeScript не всегда понимает импорты с суффиксами.

Иногда требуется декларация:

declare module '*?url' {
    const url: string
    export default url
}

Обычно Vite уже предоставляет необходимые типы через:

{
    "compilerOptions": {
        "types": ["vite/client"]
    }
}

Использование внутри Vue

<script setup>
import logoUrl from './logo.png?url'
</script>

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

Использование внутри React

import iconUrl from './icon.svg?url'

export default function App() {
    return <img src={iconUrl} />
}

Использование внутри Svelte

<script>
    import imageUrl from './image.jpg?url'
</script>

<img src={imageUrl}>

Использование с Canvas

import textureUrl from './texture.png?url'

const image = new Image()

image.src = textureUrl

image.onl oad = () => {
    ctx.drawImage(image, 0, 0)
}

Использование с Three.js

import textureUrl from './wood.jpg?url'

const texture = new THREE.TextureLoader().load(
    textureUrl
)

Использование в конфигурациях

Иногда URL нужен в runtime-конфигурации:

import workerUrl from './worker.js?url'

export const config = {
    worker: workerUrl
}

Обработка файлов в build-процессе

При использовании ?url файл:

  • попадает в dependency graph;
  • участвует в сборке;
  • получает хэш;
  • оптимизируется Vite;
  • копируется в dist/assets.

Влияние на tree shaking

Если импортированный URL нигде не используется:

import imageUrl from './image.png?url'

то Vite и Rollup могут удалить его из итоговой сборки.


Использование с alias

import iconUrl from '@/assets/icon.svg?url'

Alias корректно разрешается через конфигурацию:

resolve: {
    alias: {
        '@': '/src'
    }
}

Ограничения ?url

Суффикс не подходит для случаев, когда требуется:

  • содержимое файла;
  • AST-преобразование;
  • SVG как компонент;
  • inline-данные;
  • обработка loader-плагинами.

Типичные ошибки

Попытка использовать как модуль

Неверно:

import worker from './worker.js?url'

worker.postMessage()

worker — это строка URL, а не объект Worker.


Ожидание содержимого файла

Неверно:

import text from './file.txt?url'

console.log(text.includes('hello'))

Импорт возвращает путь, а не текст файла.


Использование без new Worker

Неверно:

import workerUrl from './worker.js?url'

workerUrl.postMessage('hello')

Правильно:

const worker = new Worker(workerUrl, {
    type: 'module'
})

Сравнение способов импорта

Подход Результат
import './img.png' зависит от обработки
import './img.png?url' URL-строка
import './img.png?raw' содержимое файла
import './img.png?inline' Base64
new URL(..., import.meta.url) URL через ESM API

Практический пример структуры проекта

src/
├── assets/
│   ├── logo.svg
│   ├── bg.jpg
│   ├── worker.js
│   └── manual.pdf
├── main.js

main.js:

import logoUrl from './assets/logo.svg?url'
import bgUrl from './assets/bg.jpg?url'
import workerUrl from './assets/worker.js?url'
import manualUrl from './assets/manual.pdf?url'

document.querySelector('#logo').src = logoUrl

document.body.style.backgroundImage =
    `url(${bgUrl})`

const worker = new Worker(workerUrl, {
    type: 'module'
})

document.querySelector('#manual').href =
    manualUrl

Внутренний механизм обработки

Во время анализа модулей Vite:

  1. обнаруживает импорт с ?url;
  2. помечает ресурс как asset;
  3. исключает модульную обработку;
  4. генерирует URL;
  5. подставляет итоговую строку в bundle.

После сборки импорт:

import imageUrl from './logo.png?url'

может превратиться примерно в:

const imageUrl = "/assets/logo.8f2c1d.png"

Преимущества ?url

Явное поведение

Разработчик сразу показывает намерение:

import fileUrl from './file.bin?url'

Надёжность после сборки

Пути автоматически обновляются после hash-переименования файлов.


Совместимость с browser API

Большинство нативных API работают именно со строковыми URL.


Простая интеграция

Суффикс одинаково работает:

  • в React;
  • Vue;
  • Svelte;
  • Vanilla JS;
  • TypeScript;
  • Web Workers;
  • WASM;
  • Canvas API;
  • сторонних библиотеках.