Vite在开发者中越来越受欢迎,但由于社区没有Webpack那么大,您可能需要创建自己的自定义插件来解决您的问题。在这篇文章中,我们将讨论如何为Vite创建一个插件,并且我将分解我自己的插件。
要创建插件,重要的是要知道 Vite 对开发服务器(命令vite
)和捆绑包(命令vite build
)使用不同的构建系统。
对于开发服务器,它使用带有原生 ES 模块的esbuild ,这些模块受到现代浏览器的支持,我们不需要将代码捆绑到单个文件中,并且它为我们提供了快速的 HRM(热模块替换)。
对于捆绑包,它使用rollup.js,因为它很灵活并且拥有庞大的生态系统;它允许创建具有不同输出格式的高度优化的生产包。
Vite 的插件界面基于 Rollup 的,但具有用于与开发服务器配合使用的附加选项和挂钩。
创建插件时,您可以将其内联到vite.config.js
中。无需为其创建新包。一旦您发现某个插件在您的项目中有用,请考虑与社区分享并为 Vite 生态系统做出贡献。
另外,由于 rollup.js 拥有更大的社区和生态系统,您可以考虑为 rollup.js 创建一个插件,它在 Vite 中也能正常工作。因此,如果您的插件功能仅适用于捆绑包,您可以使用 rollup.js 插件而不是 Vite,并且用户可以在其 Vite 项目中使用您的 rollup 插件,不会出现任何问题。
如果您为 rollup 创建插件,您将覆盖更多仅使用 rollup.js 的用户。如果您的插件会影响开发服务器,那么您可以使用 Vite 插件。
让我们开始直接在vite.config.ts
中创建插件:
// vite.config.ts import { defineConfig, Plugin } from 'vite'; function myPlugin(): Plugin { return { name: 'my-plugin', configResolved(config) { console.log(config); }, }; } export default defineConfig({ plugins: [ myPlugin(), ], });
在此示例中,我创建了一个名为myPlugin
的插件,一旦在控制台中的两个阶段(开发服务器和捆绑包)中解析 Vite 配置,该插件就会打印它。如果我只想在开发服务器模式下打印配置,那么我应该为捆绑添加apply: 'serve'
和apply: 'build'
。
// vite.config.ts import { defineConfig, Plugin } from 'vite'; function myPlugin(): Plugin { return { name: 'my-plugin', apply: 'serve', configResolved(config) { console.log(config); }, }; } export default defineConfig({ plugins: [ myPlugin(), ], });
另外,我可以返回一组插件;它对于分离开发服务器和捆绑包的功能很有用:
// vite.config.ts import { defineConfig, Plugin } from 'vite'; function myPlugin(): Plugin[] { return [ { name: 'my-plugin:serve', apply: 'serve', configResolved(config) { console.log('dev server:', config); }, }, { name: 'my-plugin:build', apply: 'build', configResolved(config) { console.log('bundle:', config); }, }, ]; } export default defineConfig({ plugins: [ myPlugin(), ], });
差不多就是这样了;您可以轻松地将小插件添加到 Vite 配置中。如果插件太大,我更喜欢将其移动到另一个文件,甚至创建一个包。
如果您需要更复杂的东西,您可以在 Vite 文档中探索许多有用的钩子。但作为一个例子,让我们在下面分解我自己的插件。
所以,我有一个基于图标文件创建 SVG 精灵的插件 - vite-plugin-svg-spritemap 。
目标是获取src/icons
文件夹中的所有图标.svg
,并将其内容收集到单个.svg
文件(称为 SVG sprite)中。让我们从捆绑阶段开始:
import { Plugin, ResolvedConfig } from 'vite'; import path from 'path'; import fs from 'fs-extra'; function myPlugin(): Plugin { let config: ResolvedConfig; return { name: 'my-plugin:build', apply: 'build', async configResolved(_config) { config = _config; }, writeBundle() { const sprite = getSpriteContent({ pattern: 'src/icons/*.svg' }); const filePath = path.resolve(config.root, config.build.outDir, 'sprite.svg'); fs.ensureFileSync(filePath); fs.writeFileSync(filePath, sprite); }, }; }
Hook configResolved
允许我们在决定在下一个钩子中使用它时获取配置;
writeBundle
钩子在捆绑过程完成后被调用,在这里,我将创建sprite.svg
文件;
getSpriteContent
函数返回基于src/icons/*.svg
模式准备的 SVG 精灵的字符串。我不会更深入地讨论这一点;你可以看看我的另一篇文章解释了SVG精灵生成的整个过程;
然后,我创建sprite.svg
的绝对路径,以使用path.resolve()
将 SVG 精灵内容放入其中,使用fs.ensureFileSync
确保该文件存在(或创建一个),然后将 SVG 精灵内容写入其中。
现在,最有趣的部分 - 开发服务器阶段。我在这里无法使用writeBundle
,并且在开发服务器运行时无法托管文件,因此我们需要使用服务器中间件来捕获对sprite.svg
的请求。
import { Plugin, ResolvedConfig } from 'vite'; function myPlugin(): Plugin { let config: ResolvedConfig; return { name: `my-plugin:serve`, apply: 'serve', async configResolved(_config) { config = _config; }, configureServer(server) { // (1) return () => { server.middlewares.use(async (req, res, next) => { // (2) if (req.url !== '/sprite.svg') { return next(); // (3) } const sprite = getSpriteContent({ pattern, prefix, svgo, currentColor }); res.writeHead(200, { // (4) 'Content-Type': 'image/svg+xml, charset=utf-8', 'Cache-Control': 'no-cache', }); res.end(sprite); }); }; }, }; }
configureServer
是一个用于配置开发服务器的钩子。在Vite内部中间件安装之前触发;就我而言,我需要在内部中间件之后添加自定义中间件,因此我返回一个函数;
要添加自定义中间件以捕获对开发服务器的每个请求,我使用server.middlewares.use()
。我需要它来检测 URL [localhost:3000/sprite.svg](http://localhost:3000/sprite.svg)
的请求,这样我就可以模拟文件行为;
如果请求 URL 不是/sprite.svg
- 跳到下一个中间件(即,将控制权传递给链中的下一个处理程序);
为了准备文件内容,我将getSpriteContent
的结果放入变量sprite
中,并将其作为带有配置标头(内容类型和 200 HTTP 状态)的响应发送。
结果,我模拟了文件行为。
但是如果src/icons
中的文件被更改、删除或添加,我们应该重新启动服务器以通过getSpriteContent
生成新的精灵内容;为此,我将使用文件监视库 - chokidar 。让我们将 chokidar 处理程序添加到代码中:
import { Plugin, ResolvedConfig } from 'vite'; import chokidar from 'chokidar'; function myPlugin(): Plugin { let config: ResolvedConfig; let watcher: chokidar.FSWatcher; // Defined variable for chokidar instance. return { name: `my-plugin:serve`, apply: 'serve', async configResolved(_config) { config = _config; }, configureServer(server) { function reloadPage() { // Function that sends a signal to reload the server. server.ws.send({ type: 'full-reload', path: '*' }); } watcher = chokidar .watch('src/icons/*.svg', { // Watch src/icons/*.svg cwd: config.root, // Define project root path ignoreInitial: true, // Don't trigger chokidar on instantiation. }) .on('add', reloadPage) // Add listeners to add, modify, delete. .on('change', reloadPage) .on('unlink', reloadPage); return () => { server.middlewares.use(async (req, res, next) => { if (req.url !== '/sprite.svg') { return next(); } const sprite = getSpriteContent({ pattern, prefix, svgo, currentColor }); res.writeHead(200, { 'Content-Type': 'image/svg+xml, charset=utf-8', 'Cache-Control': 'no-cache', }); res.end(sprite); }); }; }, }; }
正如您所看到的,插件创建的 API 并不复杂。您只需要从 Vite 或 Rollup 中找到适合您任务的钩子即可。在我的示例中,我使用 Rollup.js 中的writeBundle
(正如我所说,它用于生成包),并使用 Vite 中的configureServer
,因为 Rollup.js 没有本机开发服务器支持。
对于writeBundle
来说,它非常简单,我们获取 SVG 精灵内容并将其放入文件中。就开发服务器而言,我很困惑为什么我不能做同样的事情;我查看了其他作者的插件,它们的作用都差不多。
因此,我使用configureServer
并通过server
参数添加中间件,通过拦截sprite.svg
请求来触发对开发服务器的每个请求。
正如我之前提到的,要创建更有用的插件,您需要探索挂钩。它们在文档中有详细解释:
https://vitejs.dev/guide/api-plugin#universal-hooks
https://vitejs.dev/guide/api-plugin#vite-specific-hooks
在命名方面,Vite 对插件有一些约定,大家最好先检查一下再完成。以下是一些要点:
vite-plugin-
前缀;
vite-plugin
关键字;
vite-plugin-vue-
、 vite-plugin-react-
、 vite-plugin-svelte-
)。如果您决定在 NPM 中发布您的插件,我建议这样做,因为共享知识和专业知识是 IT 社区的基本原则,可以促进集体成长。要了解如何发布和维护包,请查看我的指南 →创建 NPM 包的最简单方法。
我还强烈建议将您的插件提交到 vite 的社区列表 - Awesome-vite 。很多人都在寻找最合适的插件,这将是为Vite生态做出贡献的绝佳机会!在那里提交插件的过程很简单 - 只需确保满足条款并创建拉取请求即可。您可以在此处找到术语列表。
总体来说,它需要针对Vite(而不是rollup),开源,并且有良好的文档。祝你的插件好运!