Vite 插件简要

我点燃了火,却控制不了它。 ---刘慈欣-

简介

Viite 的插件机制是基于 Rollup 的插件机制实现的,但是又进行了一些扩展。Vite 的插件机制是通过钩子函数实现的,当 Vite 运行时,会通过钩子函数调用插件中的方法,插件可以在这些方法中干预 Vite 的构建过程。

如果插件不使用 Vite 特有的钩子,可以作为 兼容 Rollup 的插件 来实现,推荐使用 Rollup 插件名称约定

  • Rollup 插件应该有一个带 rollup-plugin- 前缀、语义清晰的名称。
  • 在 package.json 中包含 rollup-plugin 和 vite-plugin 关键字。

这样,插件也可以用于纯 Rollup 或基于 WMR 的项目。

对于 Vite 专属的插件:

  • Vite 插件应该有一个带 vite-plugin- 前缀、语义清晰的名称。
  • 在 package.json 中包含 vite-plugin 关键字。
  • 在插件文档增加一部分关于为什么本插件是一个 Vite 专属插件的详细说明(如,本插件使用了 Vite 特有的插件钩子)。

如果你的插件只适用于特定的框架,它的名字应该遵循以下前缀格式:

  • vite-plugin-vue- 前缀作为 Vue 插件
  • vite-plugin-react- 前缀作为 React 插件
  • vite-plugin-svelte- 前缀作为 Svelte 插件

更多详情参见 虚拟模块的相关内容.

Rollup 插件机制

Rollup 的插件机制实现主要基于两点:

  • Rollup 维护了各个插件接口的 Hook 列表,插件可以向这些列表中添加回调函数。
  • 在执行对应过程时,Rollup 会依次触发这些 Hook 列表中的回调函数。
js 复制代码
const hookLists = {
  load: [] // load hook 列表
}

function addHook(hookName, hook) {
  hookLists[hookName].push(hook)  // 向 hook 列表中添加回调函数
}

function load(id) {
  for (const hook of hookLists.load) { // 触发所有 load 钩子函数
    const result = hook(id)  // 调用钩子函数
    if (result) return result  // 使用第一个结果并返回
  }
}

插件可以通过 Rollup 提供的 addHook 方法相对应的 Hook 列表中添加回调函数:

js 复制代码
export function myPlugin() {
  addHook('load', id => {  // 向 load 列表添加回调函数
    // ...
  })
}

Vite

Vite 主要将用户插件排序,然后和内置的插件配置合并,传递给了 Rollup 打包。

关键的部分源码如下:

js 复制代码
// vite/node/config.ts
export async function resolveConfig() {

  // ...

  // resolve plugins
  const rawUserPlugins = (
    (await asyncFlatten(config.plugins || [])) as Plugin[]
  ).filter(filterPlugin)

  const [prePlugins, normalPlugins, postPlugins] =
    sortUserPlugins(rawUserPlugins)

  // run config hooks
  const userPlugins = [...prePlugins, ...normalPlugins, ...postPlugins]

  // ...
}
js 复制代码
// vite/node/build.ts 
export async function build() {

  const config = await resolveConfig(
    inlineConfig,
    'build',
    'production',
    'production',
  )

  //...

  const plugins = (
    ssr ? config.plugins.map((p) => injectSsrFlagToHooks(p)) : config.plugins
  ) as Plugin[]

  const rollupOptions: RollupOptions = {
    context: 'globalThis',
    preserveEntrySignatures: ssr
      ? 'allow-extension'
      : libOptions
      ? 'strict'
      : false,
    cache: config.build.watch ? undefined : false,
    ...options.rollupOptions,
    input,
    plugins,
    external,
    onwarn(warning, warn) {
      onRollupWarning(warning, warn, config)
    },
  }

  // ...

  // write or generate files with rollup
  const { rollup } = await import('rollup')
  bundle = await rollup(rollupOptions)

  // ...
}

使用

Vite 使用插件时,需要将插件放入 plugins 的数组中如下:

js 复制代码
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import autoSwitchPortPlugin from "./src/utils/autoSwitchPortPlugin.js";

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [vue(), autoSwitchPortPlugin()],
})

实践

接下来我们自定义几个插件,感受下 Vite 的插件机制。

写这几个插件是为了理解插件机制,官方已经提供了相关的配置或者现成的插件

🚗 自动切换端口,默认8080

编写一个 Vite 插件,将项目的默认端口设置为 8080,当8080 被占用的时候并自动+1 寻找下一个可用端口。

js 复制代码
import net from 'net'

/**
 * 获取下一个可用端口
 * 在这段代码中,server的作用是查找并确定下一个可用的TCP端口。通过创建一个未绑定到特定端口的TCP服务器实例,并尝试在指定的起始端口上监听连接请求:
 * 使用net.createServer()创建TCP服务器。
 * 调用.unref()方法确保当没有活动连接时,Node.js进程可以正常退出,不影响其他任务执行或进程结束。
 * 在server.listen(port, ...)中,尝试在给定的端口上启动服务器监听。如果端口已被占用,则会触发错误事件,递归调用getNextPort函数以获取下一个端口继续尝试。
 * 若成功监听到指定端口,则关闭服务器(server.close(...)),并使用resolve(port)返回当前找到的可用端口。
 * 整个流程的核心目的是实现一个自动切换端口的插件功能,该功能能够帮助找到下一个可用的TCP端口供服务器应用使用。
 * @param {number} port - 起始端口默认 8080,当被占用,自动递增 port
 * @returns {Promise<number>} - Promise对象,resolve时返回下一个可用端口
 */
function getNextPort(port) {
    return new Promise((resolve) => {
        // net.createServer()是一个函数调用,它创建了一个新的TCP服务器实例。这个服务器等待并处理来自客户端的连接请求。
        const server = net.createServer()
        /**
         * 是对服务器实例调用.unref()方法。此方法的作用是将服务器从事件循环(Event Loop)中的引用计数中移除。
         * 当所有活动的引用都被取消引用(unref)后,且没有其他任务在执行时,Node.js进程可以正常退出,即使服务器仍在监听连接。
         * 换句话说,如果仅有一个对服务器的引用,并对其调用了unref,那么当服务器上没有活动连接时,
         * 主线程在完成其他任务后将能够优雅地关闭,而不会因为服务器还在监听而继续保持运行状态。
         */
        server.unref()

        // 错误处理
        server.on('error', () => {
            resolve(getNextPort(port + 1))
        })

        // 监听指定端口
        server.listen(port, () => {
            server.close(() => {
                resolve(port)
            })
        })
    })
}

/**
 * 自动切换端口插件
 * @returns {object} - 包含name和configResolved方法的对象
 */
function autoSwitchPortPlugin() {
    let port = 8080

    return {
        name: 'auto-switch-port',
        /**
         * 在配置resolved时动态切换服务器端口
         * @param {object} config - 服务器配置对象
         */
        async configResolved(config) {
            port = await getNextPort(port)
            config.server.port = port
        },
    }
}

export default autoSwitchPortPlugin

🚗 为文件加上版本号

由于这个操作是转换 index.html文件,所以需要使用专用钩子transformIndexHtml

js 复制代码
import { createHash } from "crypto"

/**
 * 自动版本插件
 *
 * @returns {Object} - 包含 transformIndexHtml 方法的对象
 */
export default function autoVersionPlugin() {
    return {
        /**
         * 组件名称
         *
         * @type {string}
         */
        name: 'auto-version',
        /**
         * 转换首页 HTML
         *
         * @param {string} html - 需要转换的 HTML 字符串
         * @returns {string} - 转换后的 HTML 字符串
         */
        async transformIndexHtml(html) {
            /**
             * 创建哈希对象
             *
             * @type {Object}
             */
            // 创建一个md5的哈希对象
            const hash = createHash('md5').update(html).digest('hex')
            // 将html中的所有(src|href)="*"替换为(src|href)="*?v=hash",其中hash为上面计算得到的哈希值
            return html.replace(/(src|href)="(.*?)"/g, `$1="$2?v=${hash}"`)

        },
    }
}

在编译后的 dist 目录发现所有外部资源的尾部都添加了 hash 值

引用

通用的钩子Vite

Vite 独有的钩子Vite

rollupRollup

相关推荐
ywf12152 小时前
前端的dist包放到后端springboot项目下一起打包
前端·spring boot·后端
恋猫de小郭2 小时前
2026,Android Compose 终于支持 Hot Reload 了,但是收费
android·前端·flutter
hpoenixf8 小时前
2026 年前端面试问什么
前端·面试
还是大剑师兰特8 小时前
Vue3 中的 defineExpose 完全指南
前端·javascript·vue.js
泯泷8 小时前
阶段一:从 0 看懂 JSVMP 架构,先在脑子里搭出一台最小 JSVM
前端·javascript·架构
mengchanmian9 小时前
前端node常用配置
前端
华洛9 小时前
利好打工人,openclaw不是企业提效工具,而是个人助理
前端·javascript·产品经理
xkxnq9 小时前
第六阶段:Vue生态高级整合与优化(第93天)Element Plus进阶:自定义主题(变量覆盖)+ 全局配置与组件按需加载优化
前端·javascript·vue.js
A黄俊辉A10 小时前
vue css中 :global的使用
前端·javascript·vue.js
小码哥_常10 小时前
被EdgeToEdge适配折磨疯了,谁懂!
前端