Виртуальные модули в плагинах
## Природа виртуальных модулей в экосистеме Vite
Виртуальные модули представляют собой способ генерации модулей «на лету» без физического присутствия файлов в файловой системе. В контексте Vite они тесно связаны с Rollup-плагин системой, поскольку Vite использует Rollup на этапе production-сборки и совместимую модель плагинов в dev-режиме.
Основная идея заключается в том, что модуль может быть определён как строковый идентификатор, который перехватывается плагином и преобразуется в реальный JavaScript-код. Такой подход позволяет:
* инкапсулировать генерацию кода внутри плагина
* избегать создания временных файлов
* динамически формировать API, конфигурации или данные
* подменять ресурсы в зависимости от окружения
## Базовый механизм работы виртуальных модулей
В основе реализации лежат два ключевых хука плагина:
* resolveId
* load
Хук resolveId отвечает за перехват импорта, а load — за предоставление содержимого модуля.
Типичный поток выглядит следующим образом:
1. В коде встречается импорт виртуального идентификатора
2. resolveId определяет, является ли он виртуальным
3. возвращается уникальный id (часто с префиксом)
4. load по этому id возвращает сгенерированный код
Простейшая схема:
```js
export default function myPlugin() {
const VIRTUAL_ID = 'virtual:example-module'
return {
name: 'vite:virtual-example',
resolveId(id) {
if (id === VIRTUAL_ID) {
return '\0' + VIRTUAL_ID
}
},
load(id) {
if (id === '\0' + VIRTUAL_ID) {
return `
export const message = "Hello from virtual module"
`
}
}
}
}
```
Использование нулевого байта `\0` является соглашением Rollup для маркировки внутренних модулей, которые не должны участвовать в стандартном разрешении путей.
## Идентификаторы виртуальных модулей и соглашения
В Vite нет единственного стандарта именования виртуальных модулей, но сложились устойчивые практики:
* префикс `virtual:` для пользовательских модулей
* префикс `\0` для внутренних идентификаторов Rollup
* комбинация `virtual:plugin-name:resource` для избежания конфликтов
Пример:
```js
const ID = 'virtual:env-config'
```
или более строго:
```js
const ID = 'virtual:my-plugin:env-config'
```
После перехвата:
```js
return '\0virtual:my-plugin:env-config'
```
Такой подход позволяет избежать коллизий между несколькими плагинами, которые могут генерировать виртуальные ресурсы.
## Генерация динамического кода
Наиболее частый сценарий использования виртуальных модулей — генерация кода на основе конфигурации или окружения.
Пример генерации конфигурационного объекта:
```js
export default function envPlugin(options = {}) {
const ID = 'virtual:env'
return {
name: 'vite:env-plugin',
resolveId(id) {
if (id === ID) return '\0' + ID
},
load(id) {
if (id === '\0' + ID) {
const env = {
mode: options.mode || 'development',
debug: options.debug ?? false
}
return `export default ${JSON.stringify(env)}`
}
}
}
}
```
Использование:
```js
import env from 'virtual:env'
```
Такой модуль становится источником конфигурации, не зависящей от файловой структуры проекта.
## Виртуальные модули и HMR
В dev-режиме Vite поддерживает Hot Module Replacement, и виртуальные модули могут участвовать в этом механизме.
Чтобы обеспечить корректный HMR, плагин должен:
* фиксировать зависимости виртуального модуля
* при изменении внешних данных триггерить обновление
* использовать handleHotUpdate или собственные invalidate-механизмы
Пример интеграции:
```js
handleHotUpdate(ctx) {
if (ctx.modules.some(m => m.id.includes('virtual:env'))) {
ctx.server.moduleGraph.invalidateAll()
ctx.server.ws.send({ type: 'full-reload' })
}
}
```
Более точечный подход заключается в invalidation конкретного модуля через moduleGraph:
```js
const mod = server.moduleGraph.getModuleById('\0virtual:env')
if (mod) {
server.moduleGraph.invalidateModule(mod)
}
```
## Виртуальные модули с параметрами
Расширенный сценарий — параметризованные виртуальные модули. Они позволяют передавать данные через query-параметры в идентификаторе.
Пример:
```js
import data from 'virtual:data?source=users'
```
В плагине:
```js
resolveId(id) {
if (id.startsWith('virtual:data')) {
return '\0' + id
}
}
load(id) {
if (id.startsWith('\0virtual:data')) {
const url = new URLSearchParams(id.split('?')[1])
const source = url.get('source')
const dataset = source === 'users'
? [{ id: 1, name: 'Alex' }]
: []
return `export default ${JSON.stringify(dataset)}`
}
}
```
Такой подход превращает импорт в декларативный запрос данных.
## Разделение dev и build поведения
Vite разделяет поведение между dev-сервером и production-сборкой. Виртуальные модули должны учитывать это различие.
Часто используются проверки:
* `config.command === 'serve'`
* `config.command === 'build'`
Пример:
```js
configResolved(config) {
this.isProd = config.command === 'build'
}
```
И далее:
```js
load(id) {
if (id === '\0virtual:api') {
if (this.isProd) {
return `export const mode = "production"`
} else {
return `export const mode = "development"`
}
}
}
```
Такое разделение позволяет оптимизировать поведение модуля под конкретный этап сборки.
## Интеграция с Rollup pipeline
Так как Vite использует Rollup для сборки, виртуальные модули участвуют во всех этапах графа модулей:
* dependency resolution
* transformation
* tree-shaking
* bundling
Важно учитывать, что:
* виртуальный модуль должен быть детерминированным
* одинаковый id должен давать одинаковый output
* побочные эффекты должны явно указываться через `moduleSideEffects`
Пример:
```js
load(id) {
if (id === '\0virtual:constants') {
return {
code: `export const VERSION = "1.0.0"`,
moduleSideEffects: false
}
}
}
```
## Использование виртуальных модулей для API-обёрток
Частый архитектурный приём — генерация API-клиентов.
Например, на основе OpenAPI-спеки:
```js
load(id) {
if (id === '\0virtual:api-client') {
const endpoints = [
{ name: 'getUsers', path: '/users' },
{ name: 'getPosts', path: '/posts' }
]
const methods = endpoints.map(e => {
return `
export async function ${e.name}() {
const res = await fetch("${e.path}")
return res.json()
}
`
}).join('\n')
return methods
}
}
```
Результатом становится автоматически сгенерированный клиент без ручного написания кода.
## Взаимодействие с TypeScript
Виртуальные модули требуют дополнительной декларации типов, поскольку TypeScript не знает о их существовании.
Обычно создаётся d.ts файл:
```ts
declare module 'virtual:env' {
const env: {
mode: string
debug: boolean
}
export default env
}
```
Это обеспечивает типовую безопасность при импорте виртуального модуля.
## Кэширование и производительность
При работе с виртуальными модулями важно учитывать стоимость генерации кода.
Основные техники оптимизации:
* кэширование результата load по id
* мемоизация вычислений
* хранение промежуточных данных в Map
* использование хешей входных параметров
Пример:
```js
const cache = new Map()
load(id) {
if (cache.has(id)) return cache.get(id)
const result = generateExpensiveModule(id)
cache.set(id, result)
return result
}
```
## Типовые ошибки при проектировании виртуальных модулей
На практике часто встречаются следующие проблемы:
* отсутствие уникальности id между плагинами
* игнорирование префикса `\0`
* недетерминированная генерация кода
* отсутствие HMR-инвалидации
* смешивание бизнес-логики и генерации кода
* утечки состояния между вызовами load
Особенно критична недетерминированность: если виртуальный модуль генерирует разные значения при одинаковом id, это нарушает кэширование и приводит к неконсистентности графа модулей.
## Композиция виртуальных модулей
В крупных проектах виртуальные модули часто комбинируются:
* один модуль предоставляет конфигурацию
* другой генерирует API
* третий — runtime-константы
Пример связки:
```js
import config from 'virtual:config'
import api from 'virtual:api'
import meta from 'virtual:meta'
```
Каждый из них может быть реализован отдельным плагином или частью одного комплексного плагина.
## Виртуальные модули и SSR
В режиме SSR виртуальные модули могут вести себя иначе, чем в браузере.
Особенности:
* различие окружений node/browser
* необходимость сериализации данных
* отсутствие DOM-зависимостей
Пример:
```js
load(id) {
if (id === '\0virtual:ssr-data') {
if (this.isSSR) {
return `export default { env: "server" }`
}
return `export default { env: "client" }`
}
}
```
SSR требует строгого контроля побочных эффектов, так как модуль может выполняться на сервере при каждом запросе.
## Архитектурные паттерны использования
Виртуальные модули часто используются в следующих паттернах:
* генерация runtime-конфигурации
* abstraction layer над API
* feature flags через импорт
* динамическая регистрация плагинов
* инъекция окружения сборки
Каждый из этих паттернов строится вокруг идеи декларативного импорта как источника данных или поведения.
## Связь с экосистемой плагинов Vite
Виртуальные модули не являются изолированной концепцией, они тесно связаны с архитектурой Vite-плагинов.
Плагин становится фабрикой модулей, где:
* resolveId определяет интерфейс доступа
* load определяет реализацию
* transform может дополнительно модифицировать результат
Эта модель позволяет строить сложные расширения без изменения исходного кода приложения, заменяя его поведение на уровне графа модулей.