跳到内容

插件 API

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

建议在阅读以下章节之前,先浏览 Rollup 的插件文档

编写插件

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

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

提示

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

约定

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

  • Rollup 插件应具有清晰的名称,并带有 rollup-plugin- 前缀。
  • 在 package.json 中包含 rollup-pluginvite-plugin 关键字。

这将使该插件也可以在纯 Rollup 或基于 WMR 的项目中使用

对于仅 Vite 插件

  • Vite 插件应具有清晰的名称,并带有 vite-plugin- 前缀。
  • 在 package.json 中包含 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/Rollup 插件编写为返回实际插件对象的工厂函数。 该函数可以接受选项,允许用户自定义插件的行为。

转换自定义文件类型

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

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

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

导入虚拟文件

请参阅下一节中的示例。

虚拟模块约定

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

js
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(id) {
      if (id === virtualModuleId) {
        return resolvedVirtualModuleId
      }
    },
    load(id) {
      if (id === resolvedVirtualModuleId) {
        return `export const msg = "from virtual module"`
      }
    },
  }
}

这允许在 JavaScript 中导入模块

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

console.log(msg)

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

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

通用钩子

在开发期间,Vite 开发服务器创建一个插件容器,该容器以 Rollup 相同的方式调用 Rollup 构建钩子

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

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

这些钩子还具有带有其他 Vite 特定属性的扩展 options 参数。 您可以在SSR 文档中阅读更多内容。

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

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

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

输出生成钩子(除了 closeBundle)在开发期间不会被调用。 您可以将 Vite 的开发服务器视为仅调用 rollup.rollup() 而不调用 bundle.generate()

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...
          })
        }
      },
    })

    存储服务器访问权限

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

    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' 在应用所有 order 未定义的钩子之后应用该钩子。

    基本示例

    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
      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>

  • 另请参阅: 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 插件还可以指定一个 enforce 属性(类似于 webpack 加载器)来调整其应用顺序。 enforce 的值可以是 "pre""post"。 解析的插件将按以下顺序排列

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

请注意,这与钩子排序是分开的,这些排序仍然分别受到它们的 order 属性的约束,与 Rollup 钩子一样

条件应用

默认情况下,插件会为 serve 和 build 调用。 如果插件需要在 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
}

Rollup 插件兼容性

相当数量的 Rollup 插件可以直接作为 Vite 插件工作(例如,@rollup/plugin-alias@rollup/plugin-json),但并非所有插件都可以,因为某些插件钩子在未捆绑的开发服务器上下文中没有意义。

一般来说,只要 Rollup 插件符合以下标准,它就应该可以作为 Vite 插件工作

  • 它不使用 moduleParsed 钩子。
  • 它在 bundle 阶段的钩子和输出阶段的钩子之间没有很强的耦合。

如果 Rollup 插件仅对构建阶段有意义,则可以在 build.rollupOptions.plugins 下指定它。 它的工作方式与带有 enforce: 'post'apply: 'build' 的 Vite 插件相同。

您还可以使用仅 Vite 属性来增强现有的 Rollup 插件

vite.config.js
js
import example from 'rollup-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 分隔符非常重要。 等效的 normalizePath 实用程序函数从 vite 模块导出。

js
import { normalizePath } from 'vite'

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

过滤,包含/排除模式

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

客户端-服务器通信

自 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 接口推断有效负载的类型,可以通过扩展接口键入自定义事件

注意

指定 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 的有效负载类型。 有关如何使用此接口的更多信息,请参阅 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 })

在 MIT 许可下发布。 (083ff36d)