Плагин в Vite представляет собой объект с набором хуков, которые вызываются на различных этапах работы dev-сервера, обработки модулей и сборки проекта. Архитектура плагинов Vite основана на модели плагинов Rollup, однако дополняется собственными механизмами, связанными с HMR, dev-сервером и трансформацией модулей в режиме разработки.
Каждый хук отвечает за строго определённый этап:
Порядок вызова хуков критически важен. Ошибка в понимании жизненного цикла приводит к конфликтам между плагинами, дублирующимся трансформациям, некорректному HMR и нестабильной сборке.
Во время работы Vite плагины проходят несколько фаз:
Часть хуков работает только в dev-режиме, часть — только при production build, а некоторые поддерживаются в обоих режимах.
configХук config вызывается на этапе создания конфигурации
Vite до её финальной нормализации.
Используется для:
config(config, env) {
}
Параметры:
| Параметр | Описание |
|---|---|
config |
Исходная конфигурация |
env |
Информация о режиме запуска |
export default function myPlugin() {
return {
name: 'my-plugin',
config(config, env) {
return {
resolve: {
alias: {
'@shared': '/src/shared'
}
}
}
}
}
}
Возвращаемый объект не заменяет конфигурацию полностью. Vite выполняет merge.
config() {
return {
define: {
__DEVTOOLS__: true
}
}
}
config(config, { mode }) {
if (mode === 'production') {
return {
build: {
sourcemap: false
}
}
}
}
configResolvedВызывается после полной обработки конфигурации Vite.
В отличие от config, здесь доступна окончательная
конфигурация со всеми merge-операциями.
let resolvedConfig
export default function plugin() {
return {
name: 'config-plugin',
configResolved(config) {
resolvedConfig = config
}
}
}
configResolved(config) {
console.log(config.command)
}
Возможные значения:
| Значение | Описание |
|---|---|
serve |
dev-сервер |
build |
production build |
configureServerПозволяет получить доступ к dev-серверу Vite.
Работает только в режиме разработки.
Через configureServer можно:
configureServer(server) {
server.middlewares.use((req, res, next) => {
console.log(req.url)
next()
})
}
configureServer(server) {
server.middlewares.use('/api/ping', (req, res) => {
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({
success: true
}))
})
}
configureServer(server) {
server.ws.on('custom:event', (data) => {
console.log(data)
})
}
configureServer(server) {
setInterval(() => {
server.ws.send({
type: 'custom',
event: 'timer',
data: Date.now()
})
}, 1000)
}
configurePreviewServerАналог configureServer, но работает с preview
server.
Используется редко, в основном для кастомизации production preview.
configurePreviewServer(server) {
server.middlewares.use((req, res, next) => {
console.log('preview request')
next()
})
}
resolveIdОтвечает за разрешение импортов.
Это один из ключевых хуков всей системы плагинов.
При каждом:
import ...
или
require(...)
Vite запускает цепочку resolveId.
resolveId(source) {
if (source === 'virtual:config') {
return source
}
}
Чаще всего resolveId используется для создания
виртуальных модулей.
const virtualModuleId = 'virtual:env'
const resolvedVirtualModuleId = '\0virtual:env'
export default function plugin() {
return {
name: 'virtual-plugin',
resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId
}
}
}
}
\0Префикс \0:
loadПозволяет предоставить содержимое модуля вручную.
Обычно используется совместно с resolveId.
load(id) {
if (id === '\0virtual:env') {
return `
export const API_URL = 'https://example.com'
`
}
}
load часто используется для:
transformГлавный хук обработки кода.
Через transform проходят:
transform(code, id) {
}
transform(code, id) {
if (id.endsWith('.js')) {
return code.replace('__DEV__', 'true')
}
}
transform(code) {
return {
code: transformedCode,
map: sourceMap
}
}
Корректная генерация source maps крайне важна:
return {
code,
map: null
}
Ошибкой считается выполнение тяжёлой обработки для всех модулей.
Правильный подход:
transform(code, id) {
if (!id.endsWith('.md')) {
return
}
return compileMarkdown(code)
}
transform работает:
Из-за этого трансформации должны быть:
handleHotUpdateПозволяет перехватывать механизм HMR.
handleHotUpdate(ctx) {
console.log(ctx.file)
}
| Поле | Описание |
|---|---|
file |
Изменённый файл |
modules |
Затронутые модули |
server |
Экземпляр dev-сервера |
timestamp |
Время изменения |
handleHotUpdate(ctx) {
ctx.server.ws.send({
type: 'full-reload'
})
}
handleHotUpdate(ctx) {
return ctx.modules
}
handleHotUpdate(ctx) {
if (ctx.file.endsWith('.txt')) {
ctx.server.ws.send({
type: 'custom',
event: 'text-update'
})
return []
}
}
buildStartВызывается в начале Rollup build.
buildStart() {
console.log('build started')
}
buildEndВызывается после завершения build pipeline.
buildEnd(error) {
if (error) {
console.error(error)
}
}
generateBundleПозволяет модифицировать финальный bundle перед записью файлов.
generateBundle(options, bundle) {
}
{
'index.js': {
type: 'chunk'
},
'style.css': {
type: 'asset'
}
}
generateBundle() {
this.emitFile({
type: 'asset',
fileName: 'meta.json',
source: JSON.stringify({
build: Date.now()
})
})
}
generateBundle(options, bundle) {
for (const file in bundle) {
const chunk = bundle[file]
if (chunk.type === 'chunk') {
chunk.code += '\nconsole.log("loaded")'
}
}
}
writeBundleВызывается после записи файлов на диск.
generateBundle| Хук | Когда вызывается |
|---|---|
generateBundle |
До записи |
writeBundle |
После записи |
writeBundle() {
console.log('files written')
}
closeBundleФинальный этап жизненного цикла.
Используется для:
closeBundle() {
console.log('bundle closed')
}
transformIndexHtmlСпециализированный хук обработки HTML.
transformIndexHtml(html) {
return html.replace(
'</head>',
'<script src="/analytics.js"></script></head>'
)
}
transformIndexHtml() {
return [
{
tag: 'meta',
attrs: {
name: 'theme-color',
content: '#000'
},
injectTo: 'head'
}
]
}
| Значение | Описание |
|---|---|
head |
В конец <head> |
head-prepend |
В начало <head> |
body |
В конец <body> |
body-prepend |
В начало <body> |
При импорте файла Vite выполняет:
resolveIdloadtransformВо время build:
configconfigResolvedbuildStartresolveIdloadtransformgenerateBundlewriteBundlecloseBundleenforce| Значение | Описание |
|---|---|
pre |
Выполняется раньше |
post |
Выполняется позже |
export default function plugin() {
return {
name: 'pre-plugin',
enforce: 'pre'
}
}
Порядок:
prepostЭто особенно важно при:
Практически все хуки могут быть async.
async load(id) {
const result = await fs.promises.readFile(id, 'utf-8')
return result
}
Многие хуки получают доступ к plugin context через
this.
transform(code) {
this.warn('deprecated api')
return code
}
| Метод | Назначение |
|---|---|
this.emitFile() |
Добавление файлов |
this.warn() |
Предупреждение |
this.error() |
Ошибка |
this.resolve() |
Ручной resolve |
this.addWatchFile() |
Подписка на изменения |
this.resolveПозволяет вручную запускать механизм resolve.
async resolveId(source, importer) {
const resolved = await this.resolve(source, importer)
return resolved
}
this.addWatchFileДобавляет файл в watcher.
load(id) {
this.addWatchFile('config/theme.json')
}
При изменении файла Vite инициирует обновление.
Некоторые плагины работают только в dev или build.
apply: 'serve'
apply: 'build'
export default function plugin() {
return {
name: 'dev-plugin',
apply: 'serve'
}
}
export default function virtualConfigPlugin() {
const virtualId = 'virtual:app-config'
const resolvedVirtualId = '\0virtual:app-config'
let config
return {
name: 'virtual-config-plugin',
configResolved(resolvedConfig) {
config = resolvedConfig
},
resolveId(id) {
if (id === virtualId) {
return resolvedVirtualId
}
},
load(id) {
if (id === resolvedVirtualId) {
return `
export const MODE = '${config.mode}'
export const BASE = '${config.base}'
`
}
},
transform(code, id) {
if (!id.endsWith('.js')) {
return
}
return code.replace('__BUILD_TIME__', Date.now())
},
handleHotUpdate(ctx) {
if (ctx.file.endsWith('.json')) {
ctx.server.ws.send({
type: 'full-reload'
})
return []
}
}
}
}
В dev-среде Vite:
Из-за этого:
generateBundle не вызывается;writeBundle отсутствует;closeBundle может не срабатывать так, как
ожидается;Во время production build:
В этом режиме жизненный цикл ближе к классическому Rollup.
Большинство Rollup-хуков поддерживается напрямую:
| Rollup | Vite |
|---|---|
resolveId |
поддерживается |
load |
поддерживается |
transform |
поддерживается |
generateBundle |
поддерживается |
renderChunk |
поддерживается |
writeBundle |
поддерживается |
Однако Vite добавляет собственные хуки:
configconfigResolvedconfigureServerconfigurePreviewServerhandleHotUpdatetransformIndexHtmlПлохой вариант:
transform(code) {
return heavyTransform(code)
}
Правильный:
transform(code, id) {
if (!id.endsWith('.js')) {
return
}
return heavyTransform(code)
}
Опасный вариант:
const cache = []
transform(code) {
cache.push(code)
}
Во время HMR и параллельной обработки это может приводить к трудноуловимым ошибкам.
Неверная отправка websocket-событий способна инициировать постоянную перезагрузку страницы.
Некачественные source maps ломают:
Плохой пример:
transform(code) {
const huge = fs.readFileSync(...)
}
Такие операции блокируют event loop dev-сервера.
Крупные плагины обычно разделяют:
Наиболее сложные плагины Vite:
Именно жизненный цикл хуков определяет архитектуру таких систем и позволяет плагинам Vite глубоко вмешиваться практически во все этапы работы сборщика и dev-сервера.