Сборщик Vite предоставляет не только хороший функционал, но и удобный API для создания плагинов, позволяющих кастомизировать его практически под любую задачу. То есть, плагины можно писать не только для публикации их в npmjs.com
репозитории, но и для автоматизации исключительно своих задач.
Сложность написания плагина сравнима со сложностью написания сценария для Gulp или GitHub Actions. Для примера напишем плагин, который будет вставлять фрагменты кода в файл index.html. В зависимости от проекта в данный файл необходимо помещать код Google Analytics, метатэги Open Graph и Twitter, подключение Service worker-a, виджета чата поддержки, сплэш скрин и многое другое. В результате index.html становится очень большим и ориентироваться в нем и блоках кода довольно сложно.
Наш плагин позволить держать фрагменты кода в отдельных файлах, а при сборке все будет помещаться в index.html
. Причем это будет происходить не только при непосредственно сборке ( npm build ), но и при запуске Vite dev сервера с поддержкой HRM (Hot Module Replacement).
Репозиторий плагина - https://github.com/altrusl/vite-plugin-html-injection
NPM - https://www.npmjs.com/package/vite-plugin-html-injection
Система плагинов Vite
Плагины Vite являются расширением плагинов сборщика Rollup, который используется для сборки Vite проекта под капотом. За небольшим исключением плагины Rollup работают в Vite, в то же время последний добавляет несколько хуков в API для плагинов, которые мы и будет использовать.
Написание плагина по большому счету является написанием кода для данных хуков. Мы задействуем два из них.
vite-plugin-html-injection
Первый используемый хук - configResolved.
Он нужен для того, чтобы получить конфиг Vite, из которого мы позже получим абсолютный путь к директории проекта - config.root
Второй хук - transformIndexHtml
. В нем непосредственно нужно произвести изменение содержимого index.html
. Аргументом мы получим строку с оригинальным содержимым index.html
, вернуть надо модифицированный контент.
import { Plugin, ResolvedConfig } from "vite";
import path from "path";
import fs from "fs";
import { IHtmlInjectionConfig, IHtmlInjectionConfigInjection } from "./types";
export function htmlInjectionPlugin(
htmlInjectionConfig: IHtmlInjectionConfig
): Plugin {
let config: undefined | ResolvedConfig;
return {
name: "html-injection",
configResolved(resolvedConfig) {
config = resolvedConfig;
},
transformIndexHtml(html: string) {
let out = html;
for (let i = 0; i < htmlInjectionConfig.injections.length; i++) {
const injection: IHtmlInjectionConfigInjection =
htmlInjectionConfig.injections[i];
let root = (config as ResolvedConfig).root;
const filePath = path.resolve(root, injection.path);
let data = fs.readFileSync(filePath, "utf8");
if (injection.type === "js") {
data = `<script>\n${data}\n</script>\n`;
} else if (injection.type === "css") {
data = `<style>\n${data}\n</style>\n`;
}
switch (injection.injectTo) {
case "head":
out = out.replace("</head>", `${data}\n</head>`);
break;
case "head-prepend":
out = out.replace(/<head(.*)>/i, `$&\n${data}`);
break;
case "body":
out = out.replace("</body>", `${data}\n</body>`);
break;
case "body-prepend":
out = out.replace(/<body(.*)>/i, `$&\n${data}`);
break;
default:
break;
}
}
return out;
},
};
}
В массиве htmlInjectionConfig.injections
содержатся описания вставляемых фрагментов кода - конфигурация плагина, передаваемая ему как аргумент в vite.config.js
проекта, использующего наш плагин.
// vite.config.js
// example for Vue project
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { htmlInjectionPlugin } from "vite-plugin-html-injection";
export default defineConfig({
plugins: [
vue(),
htmlInjectionPlugin({
// Example configuration. Can be stored in a separate json file.
injections: [
{
name: "Open Graph",
path: "./src/injections/open-graph.html",
type: "raw",
injectTo: "head",
},
{
name: "Splash screen",
path: "./src/injections/splash-screen.html",
type: "raw",
injectTo: "body-prepend",
},
{
name: "Service worker",
path: "./src/injections/sw.js",
type: "js",
injectTo: "head",
},
],
}),
]
});
В случае написания "локального" плагина для себя, вместо добавления его в devDependencies
и импорта из node_modules
:
import { htmlInjectionPlugin } from "vite-plugin-html-injection";
можно импортить его локально:
import { htmlInjectionPlugin } from "./src/plugins/vite-plugin-html-injection";
Типы вставляемых фрагментов кода
Существует три типа фрагментов кода, с которыми работает плагин — raw
, js
и css
.
Raw
фрагменты вставляются как есть, js
и css
оборачиваются в теги <script>
и <style>
соответственно.
Также есть четыре места, куда можно вставить фрагмент кода: начало и конец тега head
и начало и конец body
. Соответствующие значения свойства injectTo
: head-prepend
, head
, body-prepend
и body
.
Файл ./src/injections/ga.html
может выглядеть как-то так:
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-8W4X32XXXX" />
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag("js", new Date());
gtag("config", "G-8W4X32XXXX");
</script>
Сборка плагина как пакета
В коде плагина мы используем библиотеки fs
и path
, которые помещать с сборку плагина не нужно, потому что они будут предоставлены самым Vite во время выполнения плагина. В vite.config.js
проекта плагина это нужно указать явно.
// vite.config.js
import { defineConfig } from "vite";
import { resolve } from "path";
export default defineConfig({
plugins: [],
build: {
lib: {
entry: resolve(__dirname, "./index.ts"),
name: "HtmlInjection",
fileName: "index",
formats: ["es", "cjs"],
},
rollupOptions: {
external: ["fs", "path"],
},
},
});
В итоге размер плагина получается меньше 1КБ.
Ну и package.json
плагина:
{
"name": "vite-plugin-html-injection",
"version": "1.1.9",
"description": "Vite plugin for injecting html, js, css code snippets into index.html",
"license": "MIT",
"homepage": "https://github.com/altrusl/vite-plugin-html-injection",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist"
],
"scripts": {
"build": "vite build && copy types.d.ts dist\\index.d.ts"
},
"devDependencies": {
"@antfu/eslint-config": "^0.39.8",
"@types/node": "^20.4.5",
"eslint": "^8.46.0",
"typescript": "^5.1.6",
"vite": "^4.4.7"
},
"peerDependencies": {
"vite": ">= 2.0.0"
}
}
После сборки плагина публикуем его на npmjs.com:
npm publish --access public
Комментарии (5)
dom1n1k
10.08.2023 14:05Я правильно понимаю, что хук получает содержимое
index.html
в виде сплошной строки, которую предлагается вручную расковыривать регулярками? То есть какого-то осмысленного разбора HTML или, скажем, модификации узлов по селекторам не предусмотрено. Как-то эта апишка ни о чём.gmtd Автор
10.08.2023 14:05Хороший вопрос
На вход хуку поступает действительно всего лишь строка и разбора её средствами Vite не предусмотрено и не может быть, потому как данный разбор, скажем так, не может быть универсальным/"стандартным" (нет стандартной библиотека парсинга HTML, непонятно как парсить невалидный html и т.п.). Поэтому "эта апишка" не про парсинг HTML. Подключить свою библиотеку и отпарсить самому при желании - 5 строк кода
Однако возвращать хук может намного больше, чем строку. Вот сигнатура хука:
IndexHtmlTransformHook
type IndexHtmlTransformHook = ( html: string, ctx: { path: string filename: string server?: ViteDevServer bundle?: import('rollup').OutputBundle chunk?: import('rollup').OutputChunk }, ) => | IndexHtmlTransformResult | void | Promise<IndexHtmlTransformResult | void> type IndexHtmlTransformResult = | string | HtmlTagDescriptor[] | { html: string tags: HtmlTagDescriptor[] } interface HtmlTagDescriptor { tag: string attrs?: Record<string, string | boolean> children?: string | HtmlTagDescriptor[] /** * default: 'head-prepend' */ injectTo?: 'head' | 'body' | 'head-prepend' | 'body-prepend' }
Другими словам, можно вернуть массив объектов
HtmlTagDescriptor
, каждый из которых говорит какой тэг, с каким содержимым и куда надо вставить, что может быть иногда удобней. В моем случае это не подходит, так как фрагмент кода может быть чем угодно, в частности, массивом тэгов - как в случае с GA фрагментом, и пришлось бы его парсить на тэги, чтобы добавить.dom1n1k
10.08.2023 14:05Я просто подумал поначалу, что продукт такой важности и известности, вероятно, поступает по аналогии с PostCSS. Там система делает парсинг/токенизацию стилей — плагин получает всё разобранное по полочкам, ему остается сосредоточиться на смысловой части. Ну нет, значит нет.
RegIon
10.08.2023 14:05Очень странно, что хук не получает AST, как например это в бабеле или вебпаке, или в ТС
Spaceoddity
Слишком сложно для индустрии, где через пару лет появится новый сборщик (ну или Хромиум научится сам собирать) ;)