跳转到内容

插件 API

Vite 插件扩展了 Rolldown 的插件接口,并提供了一些额外的 Vite 特有选项。因此,您可以编写一次 Vite 插件,并使其同时适用于开发环境和生产构建。

建议在阅读以下部分之前,先通读 Rolldown 的插件文档

编写插件

Vite 致力于开箱即用地提供成熟的模式,因此在创建新插件之前,请务必查看 功能指南,看看您的需求是否已涵盖。同时,请查阅现有的社区插件,包括 兼容的 Rollup 插件Vite 专用插件

在创建插件时,您可以将其内联在 vite.config.js 中,无需为其创建新包。一旦您发现该插件在项目中非常有用,可以考虑分享它以帮助 生态系统中的其他人

提示

在学习、调试或编写插件时,我们建议在项目中包含 vite-plugin-inspect。它允许您检查 Vite 插件的中间状态。安装后,您可以访问 localhost:5173/__inspect/ 来查看项目的模块和转换堆栈。请查看 vite-plugin-inspect 文档 中的安装说明。 vite-plugin-inspect

约定

如果插件不使用 Vite 特有的钩子,并且可以实现为 兼容的 Rolldown 插件,那么建议使用 Rolldown 插件命名约定

  • Rolldown 插件应具有清晰的名称,并以 rolldown-plugin- 为前缀。
  • 在 package.json 的 keywords 字段中包含 rolldown-pluginvite-plugin 关键字。

这样可以使该插件也能在纯 Rolldown 或基于 Rollup 的项目中使用。

对于仅限 Vite 使用的插件

  • Vite 插件应具有清晰的名称,并以 vite-plugin- 为前缀。
  • 在 package.json 的 keywords 字段中包含 vite-plugin 关键字。
  • 在插件文档中包含一个章节,详细说明为什么它是一个仅限 Vite 的插件(例如,它使用了 Vite 特有的插件钩子)。

如果您的插件仅适用于特定框架,则应将框架名称作为前缀的一部分。

  • Vue 插件使用 vite-plugin-vue- 前缀
  • React 插件使用 vite-plugin-react- 前缀
  • Svelte 插件使用 vite-plugin-svelte- 前缀

另请参阅 虚拟模块约定

插件配置

用户会将插件添加到项目的 devDependencies 中,并使用 plugins 数组选项进行配置。

vite.config.js
js
import vitePlugin from 'vite-plugin-feature'
import rollupPlugin from 'rollup-plugin-feature'

export default defineConfig({
  plugins: [vitePlugin(), rollupPlugin()],
})

假值(falsy)的插件将被忽略,这可用于轻松激活或停用插件。

plugins 也接受包含多个插件作为单个元素的预设。这对于实现复杂功能(如框架集成)非常有用,这些功能通常由多个插件组成。该数组在内部会被扁平化处理。

js
// framework-plugin
import frameworkRefresh from 'vite-plugin-framework-refresh'
import frameworkDevtools from 'vite-plugin-framework-devtools'

export default function framework(config) {
  return [frameworkRefresh(config), frameworkDevTools(config)]
}
vite.config.js
js
import { defineConfig } from 'vite'
import framework from 'vite-plugin-framework'

export default defineConfig({
  plugins: [framework()],
})

简单示例

提示

通常,将 Vite/Rolldown/Rollup 插件编写为一个返回实际插件对象的工厂函数是一种惯例。该函数可以接受选项,从而允许用户自定义插件的行为。

转换自定义文件类型

js
const fileRegex = /\.(my-file-ext)$/

export default function myPlugin() {
  return {
    name: 'transform-file',

    transform: {
      filter: {
        id: fileRegex,
      },
      handler(src, id) {
        return {
          code: compileFileToJS(src),
          map: null, // provide source map if available
        }
      },
    },
  }
}

导入虚拟文件

请参阅 下一节 中的示例。

虚拟模块约定

虚拟模块是一种有用的方案,允许您使用正常的 ESM 导入语法将构建时信息传递给源文件。

js
import { exactRegex } from '@rolldown/pluginutils'

export default function myPlugin() {
  const virtualModuleId = 'virtual:my-module'
  const resolvedVirtualModuleId = '\0' + virtualModuleId

  return {
    name: 'my-plugin', // required, will show up in warnings and errors
    resolveId: {
      filter: { id: exactRegex(virtualModuleId) },
      handler() {
        return resolvedVirtualModuleId
      },
    },
    load: {
      filter: { id: exactRegex(resolvedVirtualModuleId) },
      handler() {
        return `export const msg = "from virtual module"`
      },
    },
  }
}

这允许在 JavaScript 中导入该模块

js
import { msg } from 'virtual:my-module'

console.log(msg)

按照约定,Vite(以及 Rolldown / Rollup)中的虚拟模块在面向用户的路径前带有 virtual: 前缀。如果可能,应将插件名称用作命名空间,以避免与生态系统中的其他插件冲突。例如,一个 vite-plugin-posts 插件可以要求用户导入 virtual:postsvirtual:posts/helpers 虚拟模块以获取构建时信息。在内部,使用虚拟模块的插件应在解析 ID 时为模块 ID 添加 \0 前缀,这是 Rollup 生态系统中的一个约定。这可以防止其他插件尝试处理该 ID(如 node 解析),核心功能(如 sourcemaps)可以使用此信息来区分虚拟模块和常规文件。\0 在导入 URL 中是不允许的字符,因此我们必须在导入分析期间替换它们。在开发环境的浏览器中,一个 \0{id} 虚拟 ID 最终会被编码为 /@id/__x00__{id}。该 ID 在进入插件流水线之前会被解码回原样,因此插件钩子代码不会看到这种编码。

请注意,直接衍生自真实文件的模块(例如单文件组件中的脚本模块,如 .vue 或 .svelte SFC)不需要遵循此约定。SFC 在处理时通常会生成一组子模块,但这些子模块中的代码可以映射回文件系统。对这些子模块使用 \0 会导致 sourcemaps 无法正常工作。

通用钩子

在开发过程中,Vite 开发服务器会创建一个插件容器,该容器以与 Rolldown 相同的方式调用 Rolldown 构建钩子

以下钩子在服务器启动时调用一次

以下钩子在每个传入的模块请求时调用

这些钩子还有一个扩展的 options 参数,包含额外的 Vite 特有属性。您可以在 SSR 文档 中了解更多信息。

某些 resolveId 调用中的 importer 值可能是根目录下通用 index.html 的绝对路径,因为由于 Vite 的非打包开发服务器模式,并不总是能推导出实际的 importer。对于 Vite 解析流水线内处理的导入,可以在导入分析阶段跟踪 importer,从而提供正确的 importer 值。

以下钩子在服务器关闭时调用

请注意,moduleParsed 钩子在开发期间**不会**被调用,因为 Vite 为了更好的性能避免了完整的 AST 解析。

输出生成钩子closeBundle 除外)在开发期间**不会**被调用。

Vite 特有钩子

Vite 插件还可以提供具有特定于 Vite 用途的钩子。这些钩子会被 Rollup 忽略。

config

  • 类型: (config: UserConfig, env: { mode: string, command: string }) => UserConfig | null | void

  • 类型: async, sequential

    在解析 Vite 配置之前修改它。该钩子接收原始用户配置(CLI 选项与配置文件合并后的结果)以及当前配置环境(暴露了正在使用的 modecommand)。它可以返回一个部分配置对象,该对象将与现有配置进行深度合并,或者直接修改配置(如果默认合并无法达到预期结果)。

    示例

    js
    // return partial config (recommended)
    const partialConfigPlugin = () => ({
      name: 'return-partial',
      config: () => ({
        resolve: {
          alias: {
            foo: 'bar',
          },
        },
      }),
    })
    
    // mutate the config directly (use only when merging doesn't work)
    const mutateConfigPlugin = () => ({
      name: 'mutate-config',
      config(config, { command }) {
        if (command === 'build') {
          config.root = 'foo'
        }
      },
    })

    注意

    用户插件在运行此钩子之前已解析,因此在 config 钩子中注入其他插件将不会生效。

configResolved

  • 类型: (config: ResolvedConfig) => void | Promise<void>

  • 类型: async, parallel

    在 Vite 配置解析后调用。使用此钩子读取并存储最终解析的配置。当插件需要根据当前运行的命令执行不同操作时,此钩子也非常有用。

    示例

    js
    const examplePlugin = () => {
      let config
    
      return {
        name: 'read-config',
    
        configResolved(resolvedConfig) {
          // store the resolved config
          config = resolvedConfig
        },
    
        // use stored config in other hooks
        transform(code, id) {
          if (config.command === 'serve') {
            // dev: plugin invoked by dev server
          } else {
            // build: plugin invoked by Rollup
          }
        },
      }
    }

    注意:在开发环境中,command 值为 serve(在 CLI 中 vitevite devvite serve 是别名)。

configureServer

  • 类型: (server: ViteDevServer) => (() => void) | void | Promise<(() => void) | void>

  • 类型: async, sequential

  • 另请参阅: ViteDevServer

    用于配置开发服务器的钩子。最常见的用例是向内部 connect 应用添加自定义中间件。

    js
    const myPlugin = () => ({
      name: 'configure-server',
      configureServer(server) {
        server.middlewares.use((req, res, next) => {
          // custom handle request...
        })
      },
    })

    注入后续中间件

    configureServer 钩子在安装内部中间件之前调用,因此自定义中间件默认会在内部中间件之前运行。如果您希望在内部中间件**之后**注入中间件,可以从 configureServer 返回一个函数,该函数将在安装内部中间件后被调用。

    js
    const myPlugin = () => ({
      name: 'configure-server',
      configureServer(server) {
        // return a post hook that is called after internal middlewares are
        // installed
        return () => {
          server.middlewares.use((req, res, next) => {
            // custom handle request...
          })
        }
      },
    })

    存储服务器访问权限

    在某些情况下,其他插件钩子可能需要访问开发服务器实例(例如访问 WebSocket 服务器、文件系统监视器或模块图)。此钩子也可用于存储服务器实例,以便在其他钩子中访问。

    js
    const myPlugin = () => {
      let server
      return {
        name: 'configure-server',
        configureServer(_server) {
          server = _server
        },
        transform(code, id) {
          if (server) {
            // use server...
          }
        },
      }
    }

    注意:在运行生产构建时不会调用 configureServer,因此您的其他钩子需要防止其缺失的情况。

configurePreviewServer

  • 类型: (server: PreviewServer) => (() => void) | void | Promise<(() => void) | void>

  • 类型: async, sequential

  • 另请参阅: PreviewServer

    configureServer 相同,但用于预览服务器。与 configureServer 类似,configurePreviewServer 钩子在安装其他中间件之前调用。如果您希望在其他中间件**之后**注入中间件,可以从 configurePreviewServer 返回一个函数,该函数将在安装内部中间件后被调用。

    js
    const myPlugin = () => ({
      name: 'configure-preview-server',
      configurePreviewServer(server) {
        // return a post hook that is called after other middlewares are
        // installed
        return () => {
          server.middlewares.use((req, res, next) => {
            // custom handle request...
          })
        }
      },
    })

transformIndexHtml

  • 类型: IndexHtmlTransformHook | { order?: 'pre' | 'post', handler: IndexHtmlTransformHook }

  • 类型: async, sequential

    用于转换 HTML 入口点文件(如 index.html)的专用钩子。该钩子接收当前的 HTML 字符串和一个转换上下文。在开发期间,该上下文暴露了 ViteDevServer 实例,在构建期间暴露了 Rollup 输出 bundle。

    该钩子可以是异步的,并可以返回以下内容之一:

    • 转换后的 HTML 字符串
    • 要注入到现有 HTML 中的标签描述符对象数组({ tag, attrs, children })。每个标签还可以指定应注入到何处(默认为前置到 <head> 中)
    • 包含上述两者的对象,形式为 { html, tags }

    默认情况下,orderundefined,此钩子在 HTML 转换后应用。为了注入一个需要通过 Vite 插件流水线的脚本,order: 'pre' 将在处理 HTML 之前应用该钩子。order: 'post' 将在所有 orderundefined 的钩子应用后应用该钩子。

    基础示例

    js
    const htmlPlugin = () => {
      return {
        name: 'html-transform',
        transformIndexHtml(html) {
          return html.replace(
            /<title>(.*?)<\/title>/,
            `<title>Title replaced!</title>`,
          )
        },
      }
    }

    完整钩子签名

    ts
    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
      /**
       * attribute values will be escaped automatically if needed
       */
      attrs?: Record<string, string | boolean>
      children?: string | HtmlTagDescriptor[]
      /**
       * default: 'head-prepend'
       */
      injectTo?: 'head' | 'body' | 'head-prepend' | 'body-prepend'
    }

    注意

    如果您使用的是对入口文件有自定义处理的框架(例如 SvelteKit),则不会调用此钩子。

handleHotUpdate

  • 类型: (ctx: HmrContext) => Array<ModuleNode> | void | Promise<Array<ModuleNode> | void>

  • 类型: async, sequential

  • 另请参阅: HMR API

    执行自定义 HMR 更新处理。该钩子接收一个上下文对象,具有以下签名:

    ts
    interface HmrContext {
      file: string
      timestamp: number
      modules: Array<ModuleNode>
      read: () => string | Promise<string>
      server: ViteDevServer
    }
    • modules 是一个受更改文件影响的模块数组。它是一个数组,因为单个文件可能映射到多个服务模块(例如 Vue SFC)。

    • read 是一个异步读取函数,返回文件内容。提供此功能是因为在某些系统上,文件更改回调触发得太快,编辑器还没完成文件更新,直接 fs.readFile 会返回空内容。传入的读取函数规范化了此行为。

    钩子可以选择:

    • 过滤并缩小受影响的模块列表,使 HMR 更加准确。

    • 返回一个空数组并执行完全重新加载

      js
      handleHotUpdate({ server, modules, timestamp }) {
        // Invalidate modules manually
        const invalidatedModules = new Set()
        for (const mod of modules) {
          server.moduleGraph.invalidateModule(
            mod,
            invalidatedModules,
            timestamp,
            true
          )
        }
        server.ws.send({ type: 'full-reload' })
        return []
      }
    • 返回一个空数组并通过向客户端发送自定义事件执行完全自定义的 HMR 处理

      js
      handleHotUpdate({ server }) {
        server.ws.send({
          type: 'custom',
          event: 'special-update',
          data: {}
        })
        return []
      }

      客户端代码应使用 HMR API 注册相应的处理程序(这可以通过同一插件的 transform 钩子注入)

      js
      if (import.meta.hot) {
        import.meta.hot.on('special-update', (data) => {
          // perform custom update
        })
      }

插件上下文元数据

对于可以访问插件上下文的钩子,Vite 在 this.meta 上暴露了额外的属性:

  • this.meta.viteVersion:当前的 Vite 版本字符串(例如 "8.0.0")。

检测由 Rolldown 驱动的 Vite

this.meta.rolldownVersion 仅适用于由 Rolldown 驱动的 Vite(即 Vite 8+)。您可以使用它来检测当前的 Vite 实例是否由 Rolldown 驱动。

ts
function versionCheckPlugin(): Plugin {
  return {
    name: 'version-check',
    buildStart() {
      if (this.meta.rolldownVersion) {
        // only do something if running on a Rolldown powered Vite
      } else {
        // do something else if running on a Rollup powered Vite
      }
    },
  }
}

输出 Bundle 元数据

在构建过程中,Vite 会使用 Vite 特有的 viteMetadata 字段增强 Rolldown 的构建输出对象。

这可以通过以下方式获取:

  • RenderedChunk(例如在 renderChunkaugmentChunkHash 中)
  • OutputChunkOutputAsset(例如在 generateBundlewriteBundle 中)

viteMetadata 提供:

  • viteMetadata.importedCss: Set<string>
  • viteMetadata.importedAssets: Set<string>

这在编写需要检查发出的 CSS 和静态资源而不依赖 build.manifest 的插件时非常有用。

示例

vite.config.ts
ts
function outputMetadataPlugin(): Plugin {
  return {
    name: 'output-metadata-plugin',
    generateBundle(_, bundle) {
      for (const output of Object.values(bundle)) {
        const css = output.viteMetadata?.importedCss
        const assets = output.viteMetadata?.importedAssets
        if (!css?.size && !assets?.size) continue

        console.log(output.fileName, {
          css: css ? [...css] : [],
          assets: assets ? [...assets] : [],
        })
      }
    },
  }
}

插件排序

Vite 插件可以额外指定一个 enforce 属性(类似于 webpack 加载器)来调整其应用顺序。enforce 的值可以是 "pre""post"。解析后的插件将按以下顺序排列:

  • 别名(Alias)
  • 带有 enforce: 'pre' 的用户插件
  • Vite 核心插件
  • 没有 enforce 值的用户插件
  • Vite 构建插件
  • 带有 enforce: 'post' 的用户插件
  • Vite 构建后插件(压缩、清单、报告)

请注意,这与钩子排序不同,钩子仍然按照 Rolldown 钩子通常的 order 属性 单独受制于其顺序。

条件应用

默认情况下,插件在服务(serve)和构建(build)时都会被调用。如果插件仅需在服务或构建期间有条件地应用,请使用 apply 属性,仅在 'build''serve' 时调用它们。

js
function myPlugin() {
  return {
    name: 'build-only',
    apply: 'build', // or 'serve'
  }
}

也可以使用函数进行更精确的控制。

js
apply(config, { command }) {
  // apply only on build but not for SSR
  return command === 'build' && !config.build.ssr
}

Rolldown 插件兼容性

相当一部分 Rolldown / Rollup 插件可以直接作为 Vite 插件工作(例如 @rollup/plugin-alias@rollup/plugin-json),但并非全部,因为某些插件钩子在非打包的开发服务器上下文中没有意义。

通常情况下,只要 Rolldown / Rollup 插件符合以下标准,它就可以直接作为 Vite 插件工作:

  • 它不使用 moduleParsed 钩子。
  • 它不依赖于 Rolldown 特有的选项,如 transform.inject
  • 它在 bundle 阶段钩子和 output 阶段钩子之间没有强耦合。

如果 Rolldown / Rollup 插件仅对构建阶段有意义,则可以将其指定在 build.rolldownOptions.plugins 下。它将作为带有 enforce: 'post'apply: 'build' 的 Vite 插件运行。

您还可以使用 Vite 特有的属性增强现有的 Rolldown / Rollup 插件。

vite.config.js
js
import example from 'rolldown-plugin-example'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [
    {
      ...example(),
      enforce: 'post',
      apply: 'build',
    },
  ],
})

路径规范化

Vite 在解析 ID 时会将路径规范化为使用 POSIX 分隔符 ( / ),同时在 Windows 上保留卷。另一方面,Rollup 默认保持已解析路径不变,因此已解析 ID 在 Windows 上带有 win32 分隔符 ( \ )。不过,Rollup 插件在内部使用来自 @rollup/pluginutilsnormalizePath 工具函数,它在执行比较之前将分隔符转换为 POSIX。这意味着当这些插件在 Vite 中使用时,includeexclude 配置模式以及其他针对已解析 ID 比较的类似路径都能正常工作。

因此,对于 Vite 插件,在比较路径与已解析 ID 时,首先将路径规范化为使用 POSIX 分隔符非常重要。vite 模块导出了一个等效的 normalizePath 工具函数。

js
import { normalizePath } from 'vite'

normalizePath('foo\\bar') // 'foo/bar'
normalizePath('foo/bar') // 'foo/bar'

过滤、包含/排除模式

Vite 暴露了 @rollup/pluginutilscreateFilter 函数,以鼓励 Vite 特有插件和集成使用标准的包含/排除过滤模式,这在 Vite 核心中也使用了该模式。

钩子过滤器

Rolldown 引入了 钩子过滤器功能,以减少 Rust 和 JavaScript 运行时之间的通信开销。此功能允许插件指定模式来确定何时调用钩子,通过避免不必要的钩子调用来提高性能。

Rollup 4.38.0+ 和 Vite 6.3.0+ 也支持此功能。为了使您的插件与旧版本向后兼容,请确保同时在钩子处理程序内部运行过滤器。

js
export default function myPlugin() {
  const jsFileRegex = /\.js$/

  return {
    name: 'my-plugin',
    // Example: only call transform for .js files
    transform: {
      filter: {
        id: jsFileRegex,
      },
      handler(code, id) {
        // Additional check for backward compatibility
        if (!jsFileRegex.test(id)) return null

        return {
          code: transformCode(code),
          map: null,
        }
      },
    },
  }
}

提示

@rolldown/pluginutils 导出了一些用于钩子过滤器的工具,如 exactRegexprefixRegex。为了方便起见,这些也从 rolldown/filter 中重新导出。

客户端-服务器通信

从 Vite 2.9 开始,我们为插件提供了一些工具来帮助处理与客户端的通信。

服务器到客户端

在插件端,我们可以使用 server.ws.send 向客户端广播事件。

vite.config.js
js
export default defineConfig({
  plugins: [
    {
      // ...
      configureServer(server) {
        server.ws.on('connection', () => {
          server.ws.send('my:greetings', { msg: 'hello' })
        })
      },
    },
  ],
})

注意

我们建议**始终为您的事件名称添加前缀**,以避免与其他插件发生冲突。

在客户端,使用 hot.on 监听这些事件。

ts
// client side
if (import.meta.
hot
) {
import.meta.
hot
.
on
('my:greetings', (
data
) => {
console
.
log
(
data
.msg) // hello
}) }

客户端到服务器

要从客户端向服务器发送事件,我们可以使用 hot.send

ts
// client side
if (import.meta.hot) {
  import.meta.hot.send('my:from-client', { msg: 'Hey!' })
}

然后使用 server.ws.on 在服务器端监听这些事件。

vite.config.js
js
export default defineConfig({
  plugins: [
    {
      // ...
      configureServer(server) {
        server.ws.on('my:from-client', (data, client) => {
          console.log('Message from client:', data.msg) // Hey!
          // reply only to the client (if needed)
          client.send('my:ack', { msg: 'Hi! I got your message!' })
        })
      },
    },
  ],
})

自定义事件的 TypeScript

在内部,Vite 从 CustomEventMap 接口推断 payload 的类型,可以通过扩展该接口来定义自定义事件类型。

注意

指定 TypeScript 声明文件时,请确保包含 .d.ts 扩展名。否则,TypeScript 可能不知道该模块试图扩展哪个文件。

events.d.ts
ts
import 'vite/types/customEvent.d.ts'

declare module 'vite/types/customEvent.d.ts' {
  interface CustomEventMap {
    'custom:foo': { msg: string }
    // 'event-key': payload
  }
}

此接口扩展被 InferCustomEventPayload<T> 用来推断事件 T 的 payload 类型。有关如何使用此接口的更多信息,请参阅 HMR API 文档

ts
type 
CustomFooPayload
=
InferCustomEventPayload
<'custom:foo'>
import.meta.
hot
?.
on
('custom:foo', (
payload
) => {
// The type of payload will be { msg: string } }) import.meta.
hot
?.
on
('unknown:event', (
payload
) => {
// The type of payload will be any })