Environment API для плагинов
Релиз-кандидат
Environment API находится на стадии релиз-кандидата. Мы будем поддерживать стабильность API между основными релизами, чтобы экосистема могла экспериментировать и развиваться на их основе. Однако обратите внимание, что некоторые конкретные API всё ещё считаются экспериментальными.
Мы планируем стабилизировать эти новые API (с возможными критическими изменениями) в будущем крупном релизе, как только проекты, зависящие от них, получат достаточно времени для экспериментов и проверки новых функций.
Ресурсы:
- Обсуждение отзывов, где мы собираем отзывы о новых API.
- Пулреквест, в котором новый API был реализован и рассмотрен.
Пожалуйста, поделитесь с нами своим мнением.
Доступ к текущему окружению в хуках
Учитывая, что до Vite 6 существовало только два окружения (client и ssr), булевый параметр ssr был достаточен для идентификации текущего окружения в API Vite. Хуки плагинов получали булевый параметр ssr в последнем параметре опций, и несколько API ожидали необязательный последний параметр ssr, чтобы правильно ассоциировать модули с соответствующим окружением (например, server.moduleGraph.getModuleByUrl(url, { ssr })).
С появлением настраиваемых окружений у нас теперь есть единый способ доступа к их параметрам и экземплярам в плагинах. Хуки плагинов теперь предоставляют this.environment в своем контексте, а API, которые ранее ожидали булевый параметр ssr, теперь ограничены соответствующим окружением (например, environment.moduleGraph.getModuleByUrl(url)).
Сервер Vite имеет общий конвейер плагинов, но когда модуль обрабатывается, это всегда происходит в контексте данного окружения. Экземпляр environment доступен в контексте плагина.
Плагин может использовать экземпляр environment, чтобы изменить способ обработки модуля в зависимости от конфигурации для окружения (которую можно получить с помощью environment.config).
transform(code, id) {
console.log(this.environment.config.resolve.conditions)
}Регистрация новых окружений с помощью хуков
Плагины могут добавлять новые окружения в хуке config. Например, поддержка RSC использует дополнительное окружение для создания отдельного графа модулей с условием react-server:
config(config: UserConfig) {
return {
environments: {
rsc: {
resolve: {
conditions: ['react-server', ...defaultServerConditions],
},
},
},
}
}Пустого объекта достаточно для регистрации окружения, значения по умолчанию берутся из конфигурации окружения на корневом уровне.
Настройка окружения с помощью хуков
Во время выполнения хука config полный список окружений ещё не известен, и на окружения могут влиять как значения по умолчанию из конфигурации окружения на корневом уровне, так и явно через запись config.environments. Плагины должны устанавливать значения по умолчанию с помощью хука config. Чтобы настроить каждое окружение, они могут использовать новый хук configEnvironment. Этот хук вызывается для каждого окружения с его частично разрешённой конфигурацией, включая разрешение окончательных значений по умолчанию.
configEnvironment(name: string, options: EnvironmentOptions) {
// добавляем условие `workerd` к окружению RSC
if (name === 'rsc') {
return {
resolve: {
conditions: ['workerd'],
},
}
}
}Хук hotUpdate
- Тип:
(this: { environment: DevEnvironment }, options: HotUpdateOptions) => Array<EnvironmentModuleNode> | void | Promise<Array<EnvironmentModuleNode> | void> - Режим работы:
async,sequential - См. также: HMR API
Хук hotUpdate позволяет плагинам выполнять пользовательскую обработку обновлений HMR для данного окружения. Когда файл изменяется, алгоритм HMR выполняется для каждого окружения последовательно в соответствии с порядком в server.environments, поэтому хук hotUpdate будет вызываться несколько раз. Хук получает объект контекста со следующей сигнатурой:
interface HotUpdateOptions {
type: 'create' | 'update' | 'delete'
file: string
timestamp: number
modules: Array<EnvironmentModuleNode>
read: () => string | Promise<string>
server: ViteDevServer
}this.environment— это среда выполнения модуля, в которой в настоящее время обрабатывается обновление файла.modules— это массив модулей в этом окружении, которые затронуты изменённым файлом. Это массив, потому что один файл может соответствовать нескольким обслуживаемым модулям (например, Vue SFC).read— это асинхронная функция чтения, которая возвращает содержимое файла. Это предоставляется потому, что на некоторых системах обратный вызов изменения файла может сработать слишком быстро, прежде чем редактор завершит обновление файла, и прямой вызовfs.readFileвернет пустое содержимое. Функция чтения, переданная в хук, нормализует это поведение.
Хук может выбрать:
Отфильтровать и уточнить список затронутых модулей, чтобы HMR был более точным.
Вернуть пустой массив и выполнить полную перезагрузку:
jshotUpdate({ modules, timestamp }) { if (this.environment.name !== 'client') return // Ручная инвалидация модулей const invalidatedModules = new Set() for (const mod of modules) { this.environment.moduleGraph.invalidateModule( mod, invalidatedModules, timestamp, true ) } this.environment.hot.send({ type: 'full-reload' }) return [] }Вернуть пустой массив и выполнить полную пользовательскую обработку HMR, отправляя пользовательские события клиенту:
jshotUpdate() { if (this.environment.name !== 'client') return this.environment.hot.send({ type: 'custom', event: 'special-update', data: {} }) return [] }Код клиента должен зарегистрировать соответствующий обработчик, используя HMR API (это может быть внедрено через хук
transformтого же плагина):jsif (import.meta.hot) { import.meta.hot.on('special-update', (data) => { // пользовательское обновление }) }
Состояние плагинов для разных окружений
Поскольку один и тот же экземпляр плагина используется для разных окружений, состояние плагина должно быть привязано к this.environment. Это соответствует подходу, уже используемому в экосистеме для хранения состояния модулей с использованием булева значения ssr в качестве ключа, чтобы избежать смешивания состояния клиентских и SSR-модулей. Для разделения состояния по окружениям можно использовать Map<Environment, State>. Обратите внимание, что для обратной совместимости хуки buildStart и buildEnd вызываются только для клиентского окружения, если не установлен флаг perEnvironmentStartEndDuringDev: true. То же самое для watchChange и флага perEnvironmentWatchChangeDuringDev: true.
function PerEnvironmentCountTransformedModulesPlugin() {
const state = new Map<Environment, { count: number }>()
return {
name: 'count-transformed-modules',
perEnvironmentStartEndDuringDev: true,
buildStart() {
state.set(this.environment, { count: 0 })
},
transform(id) {
state.get(this.environment).count++
},
buildEnd() {
console.log(this.environment.name, state.get(this.environment).count)
}
}
}Плагины для каждого окружения
Плагин может определить, к каким окружениям он должен применяться, с помощью функции applyToEnvironment.
const UnoCssPlugin = () => {
// общее глобальное состояние
return {
buildStart() {
// инициализация состояния для каждого окружения с WeakMap<Environment,Data>
// с использованием this.environment
},
configureServer() {
// используйте глобальные хуки как обычно
},
applyToEnvironment(environment) {
// верните true, если этот плагин должен быть активен в этом окружении,
// или верните новый плагин, чтобы заменить его.
// если хук не используется, плагин активен во всех окружениях
},
resolveId(id, importer) {
// вызывается только для окружений, к которым этот плагин применяется
},
}
}Если плагин не учитывает окружение и имеет состояние, которое не связано с текущим окружением, хук applyToEnvironment позволяет легко сделать его специфичным для каждого окружения.
import { nonShareablePlugin } from 'non-shareable-plugin'
export default defineConfig({
plugins: [
{
name: 'per-environment-plugin',
applyToEnvironment(environment) {
return nonShareablePlugin({ outputName: environment.name })
},
},
],
})Vite экспортирует вспомогательную функцию perEnvironmentPlugin, чтобы упростить такие случаи, когда не требуются другие хуки:
import { nonShareablePlugin } from 'non-shareable-plugin'
export default defineConfig({
plugins: [
perEnvironmentPlugin('per-environment-plugin', (environment) =>
nonShareablePlugin({ outputName: environment.name }),
),
],
})Хук applyToEnvironment вызывается во время конфигурации, в настоящее время после configResolved, поскольку проекты в экосистеме модифицируют плагины в нём. Разрешение плагинов окружения может быть перенесено на этап до configResolved в будущем.
Связь между приложением и плагином
environment.hot позволяет плагинам общаться с кодом на стороне приложения для данного окружения. Это эквивалент связи «клиент-сервер», но поддерживает окружения, отличные от клиентского.
ПРЕДУПРЕЖДЕНИЕ
Эта функция доступна только для окружений, поддерживающих HMR.
Управление экземплярами приложения
Имейте в виду, что в одном и том же окружении может работать несколько экземпляров приложения. Например, если у вас открыто несколько вкладок в браузере, каждая вкладка — это отдельный экземпляр приложения с отдельным соединением к серверу.
Когда устанавливается новое соединение, на экземпляре hot окружения эмитируется событие vite:client:connect. Когда соединение закрывается, эмитируется событие vite:client:disconnect.
Каждый обработчик события получает NormalizedHotChannelClient в качестве второго аргумента. Клиент — это объект с методом send, который можно использовать для отправки сообщений этому конкретному экземпляру приложения. Ссылка на клиент всегда одинакова для одного и того же соединения, поэтому вы можете сохранять её для отслеживания соединения.
Пример использования
Сторона плагина:
configureServer(server) {
server.environments.ssr.hot.on('my:greetings', (data, client) => {
// делаем что-нибудь с данными,
// и, если нужно, отправляем ответ этому экземпляру приложения
client.send('my:foo:reply', `Привет от сервера! Вы сказали: ${data}`)
})
// рассылаем сообщение всем экземплярам приложения
server.environments.ssr.hot.send('my:foo', 'Привет от сервера!')
}Сторона приложения аналогична связи «клиент-сервер». Вы можете использовать объект import.meta.hot для отправки сообщений плагину.
Окружение в хуках сборки
Так же, как и во время разработки, хуки плагинов также получают экземпляр окружения во время сборки, заменяя булевый параметр ssr. Это также работает для renderChunk, generateBundle и других хуков, которые используются только во время сборки.
Общие плагины во время сборки
До Vite 6 конвейеры плагинов работали по-разному во время разработки и сборки:
- Во время разработки: плагины общие
- Во время сборки: плагины изолированы для каждого окружения (в разных процессах:
vite build, затемvite build --ssr).
Это заставляло фреймворки делиться состоянием между сборкой client и сборкой ssr через манифесты, записанные в файловую систему. В Vite 6 мы теперь собираем все окружения в одном процессе, поэтому способ работы конвейера плагинов и коммуникации между окружениями может быть согласован с разработкой.
В будущем крупном релизе мы сможем достичь полного соответствия:
- Во время разработки и сборки: плагины общие, с фильтрацией по окружению
Также будет единственный экземпляр ResolvedConfig, который будет общим во время сборки, что позволит кэшировать на уровне всего процесса сборки приложения так же, как мы делали это с WeakMap<ResolvedConfig, CachedData> во время разработки.
Для Vite 6 нам нужно сделать небольшой шаг, чтобы сохранить обратную совместимость. Плагины экосистемы в настоящее время используют config.build вместо environment.config.build для доступа к конфигурации, поэтому нам нужно создать новый ResolvedConfig по умолчанию для каждого окружения. Проект может выбрать возможность совместного использования полной конфигурации и конвейера плагинов, установив builder.sharedConfigBuild в true.
Эта опция будет работать только для небольшой части проектов в начале, поэтому авторы плагинов могут выбрать, чтобы конкретный плагин был общим, установив флаг sharedDuringBuild в true. Это позволяет легко делиться состоянием как для обычных плагинов:
function myPlugin() {
// Делимся состоянием между всеми окружениями как во время разработки, так и во время сборки
const sharedState = ...
return {
name: 'shared-plugin',
transform(code, id) { ... },
// Выбор единственного экземпляра для всех окружений
sharedDuringBuild: true,
}
}