Плагинная система Vite построена поверх архитектуры Rollup, однако сама среда разработки Vite добавляет собственные механизмы обработки HTTP-запросов, HMR, HTML-документов и dev server. Для взаимодействия с этими механизмами существуют специальные хуки, которых нет в стандартном Rollup API.
Наиболее важными среди них являются:
configureServertransformIndexHtmlhandleHotUpdateЭти хуки используются преимущественно при разработке dev-инструментов, SSR-инфраструктуры, кастомных middleware, систем виртуальных модулей, HMR-интеграций и HTML-трансформаций.
Хук configureServer предоставляет доступ к внутреннему
dev server Vite. Через него можно:
Этот хук работает только во время vite dev.
export default function myPlugin() {
return {
name: 'my-plugin',
configureServer(server) {
console.log('Vite server started')
}
}
}
Параметр server содержит объект
ViteDevServer.
Connect-приложение, через которое проходят все HTTP-запросы.
configureServer(server) {
server.middlewares.use((req, res, next) => {
console.log(req.url)
next()
})
}
WebSocket-сервер HMR.
Позволяет отправлять кастомные события клиенту.
configureServer(server) {
server.ws.send({
type: 'custom',
event: 'my:event',
data: {
message: 'Hello'
}
})
}
На клиенте:
if (import.meta.hot) {
import.meta.hot.on('my:event', (data) => {
console.log(data)
})
}
Граф модулей Vite.
Позволяет:
configureServer(server) {
const module = server.moduleGraph.getModuleById('/src/main.js')
console.log(module)
}
Экземпляр chokidar.
Используется для отслеживания файловой системы.
configureServer(server) {
server.watcher.on('change', (file) => {
console.log('changed:', file)
})
}
Финальная конфигурация Vite.
configureServer(server) {
console.log(server.config.root)
}
export default function apiPlugin() {
return {
name: 'api-plugin',
configureServer(server) {
server.middlewares.use('/api/hello', (req, res) => {
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({
message: 'Hello API'
}))
})
}
}
}
Теперь запрос:
/api/hello
будет обрабатываться внутри Vite.
Vite использует библиотеку Connect, совместимую с Express middleware.
Сигнатура:
(req, res, next)
Переход к следующему middleware:
next()
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (req.url === '/health') {
res.end('OK')
return
}
next()
})
}
configureServer(server) {
server.middlewares.use('/api/time', (req, res) => {
res.setHeader('Content-Type', 'application/json')
res.end(JSON.stringify({
now: Date.now()
}))
})
}
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
if (!req.url.startsWith('/app')) {
return next()
}
const html = `
<html>
<body>
<h1>SSR Response</h1>
</body>
</html>
`
res.setHeader('Content-Type', 'text/html')
res.end(html)
})
}
configureServer может вернуть функцию.
Она будет вызвана после установки встроенных middleware Vite.
configureServer(server) {
return () => {
server.middlewares.use((req, res, next) => {
next()
})
}
}
Это важно, если middleware должно работать после HMR, transform pipeline или static serving.
configureServer(server) {
setInterval(() => {
server.ws.send({
type: 'custom',
event: 'clock:upd ate',
data: {
time: new Date().toISOString()
}
})
}, 1000)
}
Клиент:
if (import.meta.hot) {
import.meta.hot.on('clock:update', (data) => {
console.log(data.time)
})
}
configureServer(server) {
server.watcher.on('change', (file) => {
if (file.endsWith('.txt')) {
server.ws.send({
type: 'full-reload'
})
}
})
}
configureServer(server) {
server.watcher.on('change', async (file) => {
const module = server.moduleGraph.getModuleById(file)
if (module) {
server.moduleGraph.invalidateModule(module)
}
})
}
transformIndexHtml предназначен для трансформации
HTML-документа перед отправкой браузеру.
Через него можно:
Хук работает как в dev, так и в build.
export default function htmlPlugin() {
return {
name: 'html-plugin',
transformIndexHtml(html) {
return html.replace(
'</body>',
'<script src="/custom.js"></script></body>'
)
}
}
}
Исходный HTML.
transformIndexHtml(html) {
console.log(html)
return html
}
Контекст трансформации.
transformIndexHtml(html, ctx) {
console.log(ctx.path)
return html
}
Текущий URL.
transformIndexHtml(html, ctx) {
if (ctx.path === '/admin') {
return html.replace(
'</head>',
'<meta name="admin" content="true"></head>'
)
}
return html
}
Dev server.
Доступен только во время vite dev.
transformIndexHtml(html, ctx) {
if (ctx.server) {
console.log('dev mode')
}
return html
}
Bundle Rollup.
Доступен только при build.
Текущий HTML chunk.
Вместо строковой трансформации можно возвращать объект.
transformIndexHtml() {
return {
html: '',
tags: [
{
tag: 'script',
attrs: {
src: '/analytics.js'
},
injectTo: 'body'
}
]
}
}
Определяет место вставки:
headbodyhead-prependbody-prependtransformIndexHtml() {
return {
html: '',
tags: [
{
tag: 'meta',
attrs: {
name: 'theme-color',
content: '#000000'
},
injectTo: 'head'
}
]
}
}
transformIndexHtml() {
return {
html: '',
tags: [
{
tag: 'script',
children: `
window.__APP_VERSION__ = '1.0.0'
`,
injectTo: 'head'
}
]
}
}
transformIndexHtml() {
return {
html: '',
tags: [
{
tag: 'link',
attrs: {
rel: 'preload',
href: '/fonts/main.woff2',
as: 'font',
crossorigin: true
},
injectTo: 'head'
}
]
}
}
transformIndexHtml(html, ctx) {
if (ctx.path.startsWith('/admin')) {
return html.replace(
'</body>',
'<script src="/admin.js"></script></body>'
)
}
return html
}
По умолчанию:
pre -> normal -> post
Указание порядка:
{
name: 'my-plugin',
transformIndexHtml: {
order: 'pre',
handler(html) {
return html
}
}
}
Выполняется до остальных трансформаций.
После всех.
export default function injectPlugin() {
return {
name: 'inject-plugin',
transformIndexHtml(html) {
return {
html,
tags: [
{
tag: 'script',
attrs: {
src: '/runtime.js'
},
injectTo: 'head'
},
{
tag: 'meta',
attrs: {
name: 'build-time',
content: new Date().toISOString()
},
injectTo: 'head'
}
]
}
}
}
}
handleHotUpdate позволяет полностью контролировать
механизм HMR Vite.
Через него можно:
export default function hmrPlugin() {
return {
name: 'hmr-plugin',
handleHotUpdate(ctx) {
console.log(ctx.file)
}
}
}
Изменённый файл.
handleHotUpdate(ctx) {
console.log(ctx.file)
}
Dev server.
handleHotUpdate(ctx) {
ctx.server.ws.send({
type: 'full-reload'
})
}
Модули, связанные с файлом.
handleHotUpdate(ctx) {
console.log(ctx.modules)
}
Функция чтения нового содержимого файла.
handleHotUpdate(async ctx) {
const content = await ctx.read()
console.log(content)
}
Время обновления.
handleHotUpdate(ctx) {
if (ctx.file.endsWith('.md')) {
ctx.server.ws.send({
type: 'full-reload'
})
return []
}
}
Возврат пустого массива отключает стандартный HMR.
handleHotUpdate(ctx) {
return ctx.modules.filter((module) => {
return module.url.includes('client')
})
}
Обновляться будут только выбранные модули.
handleHotUpdate(ctx) {
if (ctx.file.endsWith('.data')) {
ctx.server.ws.send({
type: 'custom',
event: 'dat a:update',
data: {
file: ctx.file
}
})
return []
}
}
Клиент:
if (import.meta.hot) {
import.meta.hot.on('dat a:update', (payload) => {
console.log(payload)
})
}
handleHotUpdate(async ctx) {
const content = await ctx.read()
if (content.includes('reload')) {
ctx.server.ws.send({
type: 'full-reload'
})
return []
}
}
handleHotUpdate(ctx) {
const invalidatedModules = new Se t()
for (const mod of ctx.modules) {
ctx.server.moduleGraph.invalidateModule(mod)
invalidatedModules.add(mod)
}
return [...invalidatedModules]
}
Очень часто эти хуки используются вместе.
Пример:
export default function liveDataPlugin() {
let server
return {
name: 'live-data-plugin',
configureServer(_server) {
server = _server
},
handleHotUpdate(ctx) {
if (ctx.file.endsWith('.json')) {
server.ws.send({
type: 'custom',
event: 'json:update',
data: {
file: ctx.file
}
})
return []
}
}
}
}
export default function runtimePlugin() {
let currentTime = Date.now()
return {
name: 'runtime-plugin',
configureServer(server) {
setInterval(() => {
currentTime = Date.now()
server.ws.send({
type: 'custom',
event: 'runtime:update',
data: {
currentTime
}
})
}, 1000)
},
transformIndexHtml(html) {
return html.replace(
'</head>',
`
<script>
window.__START_TIME__ = ${currentTime}
</script>
</head>
`
)
}
}
}
| Хук | Dev | Build |
|---|---|---|
| configureServer | Да | Нет |
| transformIndexHtml | Да | Да |
| handleHotUpdate | Да | Нет |
import type { Plugin } from 'vite'
import type { Plugin } from 'vite'
export default function myPlugin(): Plugin {
return {
name: 'my-plugin'
}
}
import type { ViteDevServer } from 'vite'
configureServer(server: ViteDevServer) {
}
import type { HmrContext } from 'vite'
handleHotUpdate(ctx: HmrContext) {
}
import type { IndexHtmlTransformContext } from 'vite'
transformIndexHtml(
html: string,
ctx: IndexHtmlTransformContext
) {
}
Работает поверх HTTP-сервера Vite.
Подходит для:
Работает с HTML до отправки браузеру.
Подходит для:
Работает во время изменения файлов.
Подходит для:
Если требуется отключить стандартный HMR:
return []
Без этого Vite продолжит обычное обновление.
Этот хук никогда не вызывается во время production build.
Строковые replace:
html.replace(...)
могут ломать сложные HTML-трансформации.
Безопаснее использовать:
tags: []
server.ws.send({
type: 'full-reload'
})
может вызывать циклические обновления при неправильной настройке watcher.
Специфические хуки Vite обычно работают совместно:
configureServer создаёт инфраструктуру;transformIndexHtml подготавливает клиент;handleHotUpdate синхронизирует runtime при
изменениях.Именно эта комбинация делает возможными: