В экосистеме Vite плагины практически всегда подключаются через вызов функции. Вместо передачи готового объекта плагина используется фабричная функция, которая принимает пользовательские параметры и возвращает объект с хуками, настройками и логикой работы.
Базовая структура выглядит следующим образом:
function myPlugin(options = {}) {
return {
name: 'my-plugin'
}
}
Подключение:
import { defineConfig } from 'vite'
import myPlugin from './plugins/my-plugin.js'
export default defineConfig({
plugins: [
myPlugin({
enabled: true
})
]
})
Такой подход позволяет:
Плагин Vite представляет собой обычный JavaScript-объект. Однако объект сам по себе не умеет принимать параметры. Если экспортировать только объект:
export default {
name: 'my-plugin'
}
то конфигурировать его извне будет невозможно.
Фабричная функция решает эту проблему:
export default function myPlugin(options) {
return {
name: 'my-plugin'
}
}
Теперь параметры можно передавать при подключении.
export default function loggerPlugin(options = {}) {
const {
prefix = '[LOG]'
} = options
return {
name: 'logger-plugin',
configureServer(server) {
server.middlewares.use((req, res, next) => {
console.log(prefix, req.url)
next()
})
}
}
}
import loggerPlugin from './plugins/logger-plugin.js'
export default {
plugins: [
loggerPlugin({
prefix: '[DEV SERVER]'
})
]
}
Результат:
[DEV SERVER] /src/main.js
Один из важнейших аспектов фабричной функции — наличие безопасных значений по умолчанию.
Неправильный вариант:
function plugin(options) {
console.log(options.enabled)
}
Если пользователь не передаст объект:
plugin()
возникнет ошибка:
Cannot read properties of undefined
Правильный вариант:
function plugin(options = {}) {
const {
enabled = true
} = options
}
Теперь плагин устойчив к отсутствию конфигурации.
Чаще всего параметры извлекаются через деструктуризацию.
export default function bannerPlugin(options = {}) {
const {
banner = 'Default Banner',
include = /\.js$/,
enabled = true
} = options
return {
name: 'banner-plugin'
}
}
Преимущества:
Опции фабрики доступны во всех внутренних хуках плагина благодаря замыканию.
export default function htmlPlugin(options = {}) {
const {
title = 'Vite App'
} = options
return {
name: 'html-plugin',
transformIndexHtml(html) {
return html.replace(
/<title>.*<\/title>/,
`<title>${title}</title>`
)
}
}
}
Подключение:
htmlPlugin({
title: 'Admin Panel'
})
Результат:
<title>Admin Panel</title>
Очень часто фабричная функция принимает пути.
import path from 'node:path'
export default function assetsPlugin(options = {}) {
const {
outputDir = 'dist/assets'
} = options
return {
name: 'assets-plugin',
config(config) {
config.build ??= {}
config.build.assetsDir = outputDir
}
}
}
Подключение:
assetsPlugin({
outputDir: 'static'
})
Булевы опции обычно управляют включением и отключением возможностей.
export default function debugPlugin(options = {}) {
const {
debug = false
} = options
return {
name: 'debug-plugin',
transform(code, id) {
if (debug) {
console.log(id)
}
return code
}
}
}
Плагин может принимать фильтры файлов.
export default function filterPlugin(options = {}) {
const {
include = /\.js$/,
exclude = /node_modules/
} = options
return {
name: 'filter-plugin',
transform(code, id) {
if (!include.test(id)) {
return
}
if (exclude.test(id)) {
return
}
return code
}
}
}
Подключение:
filterPlugin({
include: /\.(js|ts)$/
})
Массивы часто используются для списков расширений, директорий или разрешённых модулей.
export default function extensionsPlugin(options = {}) {
const {
extensions = ['.js']
} = options
return {
name: 'extensions-plugin',
resolveId(source) {
for (const ext of extensions) {
console.log(ext)
}
return null
}
}
}
Фабричная функция может принимать пользовательские обработчики.
export default function hookPlugin(options = {}) {
const {
onTransform
} = options
return {
name: 'hook-plugin',
transform(code, id) {
if (onTransform) {
onTransform(code, id)
}
return code
}
}
}
Подключение:
hookPlugin({
onTransform(code, id) {
console.log('Transform:', id)
}
})
В сложных плагинах используется вложенная структура параметров.
export default function apiPlugin(options = {}) {
const {
server = {},
auth = {}
} = options
const {
host = 'localhost',
port = 3000
} = server
const {
enabled = false,
token = ''
} = auth
return {
name: 'api-plugin'
}
}
Подключение:
apiPlugin({
server: {
host: '127.0.0.1',
port: 8080
},
auth: {
enabled: true,
token: 'secret'
}
})
Хороший плагин проверяет входные параметры.
export default function portPlugin(options = {}) {
const {
port = 3000
} = options
if (typeof port !== 'number') {
throw new Error(
'Option "port" must be a number'
)
}
return {
name: 'port-plugin'
}
}
Иногда часть конфигурации обязательна.
export default function tokenPlugin(options = {}) {
const {
token
} = options
if (!token) {
throw new Error(
'Token is required'
)
}
return {
name: 'token-plugin'
}
}
Один из самых популярных подходов — merge-конфигурация.
const defaultOptions = {
enabled: true,
include: /\.js$/,
debug: false
}
export default function plugin(userOptions = {}) {
const options = {
...defaultOptions,
...userOptions
}
return {
name: 'merge-plugin'
}
}
Оператор spread работает только на первом уровне.
const defaults = {
server: {
host: 'localhost',
port: 3000
}
}
const user = {
server: {
port: 8080
}
}
Результат:
{
server: {
port: 8080
}
}
Поле host исчезнет.
Для сложной конфигурации нужен deep merge.
function deepMerge(target, source) {
const result = { ...target }
for (const key in source) {
const sourceValue = source[key]
const targetValue = target[key]
if (
typeof sourceValue === 'object' &&
sourceValue !== null &&
!Array.isArray(sourceValue)
) {
result[key] = deepMerge(
targetValue || {},
sourceValue
)
} else {
result[key] = sourceValue
}
}
return result
}
Использование:
const options = deepMerge(
defaults,
userOptions
)
Фабрика может изменять поведение в зависимости от режима сборки.
export default function modePlugin(options = {}) {
return {
name: 'mode-plugin',
config(config, env) {
if (env.mode === 'production') {
console.log('Production mode')
}
if (env.mode === 'development') {
console.log('Development mode')
}
}
}
}
Опции удобно использовать для активации отдельных возможностей.
export default function featurePlugin(options = {}) {
const {
minify = false,
removeComments = false
} = options
return {
name: 'feature-plugin',
transform(code) {
let result = code
if (removeComments) {
result = result.replace(
/\/\*[\s\S]*?\*\//g,
''
)
}
if (minify) {
result = result.replace(/\s+/g, ' ')
}
return result
}
}
}
Фабричная функция может возвращать массив.
export default function fullPlugin(options = {}) {
return [
{
name: 'plugin-a'
},
{
name: 'plugin-b'
}
]
}
Подключение:
plugins: [
...fullPlugin()
]
Фабрика позволяет создавать готовые наборы конфигурации.
function createPreset(type) {
switch (type) {
case 'development':
return {
debug: true,
minify: false
}
case 'production':
return {
debug: false,
minify: true
}
}
}
Использование:
plugin(createPreset('production'))
Vite поддерживает асинхронные плагины.
export default async function plugin(options = {}) {
const data = await loadConfig()
return {
name: 'async-plugin'
}
}
В TypeScript фабричная функция особенно удобна.
interface PluginOptions {
debug?: boolean
include?: RegExp
port?: number
}
export default function plugin(
options: PluginOptions = {}
) {
return {
name: 'typed-plugin'
}
}
Для сложных решений применяются generic-типы.
interface BaseOptions<T> {
data: T
}
function plugin<T>(
options: BaseOptions<T>
) {
return {
name: 'generic-plugin'
}
}
Типизированная фабрика обеспечивает:
Даже при наличии TypeScript параметры рекомендуется документировать.
interface PluginOptions {
/**
* Включает режим отладки
*/
debug?: boolean
/**
* Регулярное выражение фильтрации
*/
include?: RegExp
}
Во многих зрелых плагинах используется отдельная нормализация параметров.
function normalizeOptions(options = {}) {
return {
debug: options.debug ?? false,
include: options.include ?? /\.js$/,
exclude: options.exclude ?? /node_modules/
}
}
export default function plugin(userOptions) {
const options = normalizeOptions(userOptions)
return {
name: 'normalized-plugin'
}
}
Преимущества:
Иногда конфигурацию делают неизменяемой.
const options = Object.freeze({
...defaults,
...userOptions
})
Это предотвращает случайное изменение параметров внутри хуков.
Фабрика может вычислять параметры только при необходимости.
export default function plugin(options = {}) {
let cache
function getCache() {
if (!cache) {
cache = createCache(options)
}
return cache
}
return {
name: 'lazy-plugin',
buildStart() {
getCache()
}
}
}
Плохо:
options.debug = true
Пользовательский объект нельзя изменять напрямую.
Правильно:
const normalized = {
...options,
debug: true
}
Плохо:
function plugin(options) {
}
Правильно:
function plugin(options = {}) {
}
Плохо:
plugin({
a: true,
b: true,
c: true,
d: true,
e: true
})
Лучше группировать параметры:
plugin({
build: {
minify: true
},
debug: {
enabled: true
}
})
Крупные плагины обычно состоят из:
Пример структуры:
plugins/
└── my-plugin/
├── index.js
├── options.js
├── validator.js
├── hooks/
└── utils/
function normalizeOptions(options = {}) {
return {
enabled: options.enabled ?? true,
debug: options.debug ?? false,
include: options.include ?? /\.js$/
}
}
export default function customPlugin(userOptions = {}) {
const options = normalizeOptions(userOptions)
if (!(options.include instanceof RegExp)) {
throw new Error(
'"include" must be RegExp'
)
}
return {
name: 'custom-plugin',
transform(code, id) {
if (!options.enabled) {
return code
}
if (!options.include.test(id)) {
return code
}
if (options.debug) {
console.log('Transform:', id)
}
return code.replace(
'__VERSION__',
'1.0.0'
)
}
}
}
Подключение:
customPlugin({
debug: true,
include: /\.(js|ts)$/
})
Такой подход обеспечивает: