Сборщик 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
