Виртуальные модули в плагинах

## Природа виртуальных модулей в экосистеме 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 может дополнительно модифицировать результат Эта модель позволяет строить сложные расширения без изменения исходного кода приложения, заменяя его поведение на уровне графа модулей.