Plugin API

Vite 插件扩展了设计出色的 Rollup 接口,带有一些 Vite 独有的配置项。因此,你只需要编写一个 Vite 插件,就可以同时为开发环境和生产环境工作。

推荐在阅读下面的章节之前,首先阅读下 Rollup 插件文档

约定

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

  • Rollup 插件应该有一个带 rollup-plugin- 前缀、语义清晰的名称。
  • 在 package.json 中包含 rollup-pluginvite-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 插件

插件配置

用户会将插件添加到项目的 devDependencies 中并使用数组形式的 plugins 选项配置它们。

  1. // vite.config.js
  2. import vitePlugin from 'vite-plugin-feature'
  3. import rollupPlugin from 'rollup-plugin-feature'
  4. export default {
  5. plugins: [
  6. vitePlugin(),
  7. rollupPlugin()
  8. ]
  9. }

Falsy[1] 虚值的插件将被忽略,可以用来轻松地启用或停用插件。

plugins 也可以接受将多个插件作为单个元素的预设。这对于使用多个插件实现的复杂特性(如框架集成)很有用。该数组将在内部被扁平化(flatten)。

  1. // 框架插件
  2. import frameworkRefresh from 'vite-plugin-framework-refresh'
  3. import frameworkDevtools from 'vite-plugin-framework-devtools'
  4. export default function framework(config) {
  5. return [
  6. frameworkRefresh(config),
  7. frameworkDevTools(config)
  8. ]
  9. }
  1. // vite.config.js
  2. import framework from 'vite-plugin-framework'
  3. export default {
  4. plugins: [
  5. framework()
  6. ]
  7. }

简单示例

TIP

通常的惯例是创建一个 Vite/Rollup 插件作为一个返回实际插件对象的工厂函数。该函数可以接受允许用户自定义插件行为的选项。

引入一个虚拟文件

  1. export default function myPlugin() {
  2. const virtualFileId = '@my-virtual-file'
  3. return {
  4. name: 'my-plugin', // 必须的,将会显示在 warning 和 error 中
  5. resolveId(id) {
  6. if (id === virtualFileId) {
  7. return virtualFileId
  8. }
  9. },
  10. load(id) {
  11. if (id === virtualFileId) {
  12. return `export const msg = "from virtual file"`
  13. }
  14. }
  15. }
  16. }

这使得可以在 JavaScript 中引入这些文件:

  1. import { msg } from '@my-virtual-file'
  2. console.log(msg)

转换自定义文件类型

  1. const fileRegex = /\.(my-file-ext)$/
  2. export default function myPlugin() {
  3. return {
  4. name: 'transform-file',
  5. transform(src, id) {
  6. if (fileRegex.test(id)) {
  7. return {
  8. code: compileFileToJS(src),
  9. map: null // provide source map if available
  10. }
  11. }
  12. }
  13. }
  14. }

通用钩子

在开发中,Vite 开发服务器会创建一个插件容器来调用 Rollup 构建钩子,与 Rollup 如出一辙。

以下钩子在服务器启动时被调用:

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

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

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

Output Generation Hooks(除了 closeBundle) 不是 在开发中被调用的。你可以认为 Vite 的开发服务器只调用了 rollup.rollup() 而没有调用 bundle.generate().

Vite 独有钩子

Vite 插件也可以提供钩子来服务于特定的 Vite 目标。这些钩子会被 Rollup 忽略。

config

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

  • 种类: sync, sequential

    在被解析之前修改 Vite 配置。钩子接收原始用户配置(命令行选项指定的会与配置文件合并)和一个描述配置环境的变量,包含正在使用的 modecommand。它可以返回一个将被深度合并到现有配置中的部分配置对象,或者直接改变配置(如果默认的合并不能达到预期的结果)。

    示例

    1. // 返回部分配置(推荐)
    2. const partialConfigPlugin = () => ({
    3. name: 'return-partial',
    4. config: () => ({
    5. alias: {
    6. foo: 'bar'
    7. }
    8. })
    9. })
    10. // 直接改变配置(应仅在合并不起作用时使用)
    11. const mutateConfigPlugin = () => ({
    12. name: 'mutate-config',
    13. config(config, { command }) {
    14. if (command === 'build') {
    15. config.root = __dirname
    16. }
    17. }
    18. })

    注意

    用户插件在运行这个钩子之前会被解析,因此在 config 钩子中注入其他插件不会有任何效果。

configResolved

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

  • 种类: async, parallel

    在解析 Vite 配置后调用。使用这个钩子读取和存储最终解析的配置。当插件需要根据运行的命令做一些不同的事情时,它也很有用。

    示例:

    1. const exmaplePlugin = () => {
    2. let config
    3. return {
    4. name: 'read-config',
    5. configResolved(resolvedConfig) {
    6. // 存储最终解析的配置
    7. config = resolvedConfig
    8. },
    9. // 使用其他钩子存储的配置
    10. transform(code, id) {
    11. if (config.command === 'serve') {
    12. // serve: 用于启动开发服务器的插件
    13. } else {
    14. // build: 调用 Rollup 的插件
    15. }
    16. }
    17. }
    18. }

configureServer

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

  • 种类: async, sequential

  • 此外请看 ViteDevServer

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

    1. const myPlugin = () => ({
    2. name: 'configure-server',
    3. configureServer(server) {
    4. server.middlewares.use((req, res, next) => {
    5. // 自定义请求处理...
    6. })
    7. }
    8. })

    注入后置中间件

    configureServer 钩子将在内部中间件被安装前调用,所以自定义的中间件将会默认会比内部中间件早运行。如果你想注入一个在内部中间件 之后 运行的中间件,你可以从 configureServer 返回一个函数,将会在内部中间件安装后被调用:

    1. const myPlugin = () => ({
    2. name: 'configure-server',
    3. configureServer(server) {
    4. // 返回一个在内部中间件安装后被调用的后置钩子
    5. return () => {
    6. server.middlewares.use((req, res, next) => {
    7. // 自定义请求处理...
    8. })
    9. }
    10. }
    11. })

    存储服务器访问

    在某些情况下,其他插件钩子可能需要访问开发服务器实例(例如访问 websocket 服务器、文件系统监视程序或模块图)。这个钩子也可以用来存储服务器实例以供其他钩子访问:

    1. const myPlugin = () => {
    2. let server
    3. return {
    4. name: 'configure-server',
    5. configureServer(_server) {
    6. server = _server
    7. },
    8. transform(code, id) {
    9. if (server) {
    10. // 使用 server...
    11. }
    12. }
    13. }
    14. }

    注意 configureServer 在运行生产版本时不会被调用,所以其他钩子需要注意防止它的缺失。

transformIndexHtml

  • 类型: IndexHtmlTransformHook | { enforce?: 'pre' | 'post' transform: IndexHtmlTransformHook }

  • 种类: async, sequential

    转换 index.html 的专用钩子。钩子接收当前的 HTML 字符串和转换上下文。上下文在开发期间暴露ViteDevServer实例,在构建期间暴露 Rollup 输出的包。

    这个钩子可以是异步的,并且可以返回以下其中之一:

    • 经过转换的 HTML 字符串
    • 注入到现有 HTML 中的标签描述符对象数组({ tag, attrs, children })。每个标签也可以指定它应该被注入到哪里(默认是在 <head> 之前)
    • 一个包含 { html, tags } 的对象

    Basic Example

    1. const htmlPlugin = () => {
    2. return {
    3. name: 'html-transform',
    4. transformIndexHtml(html) {
    5. return html.replace(
    6. /<title>(.*?)<\/title>/,
    7. `<title>Title replaced!</title>`
    8. )
    9. }
    10. }
    11. }

    完整钩子签名:

    1. type IndexHtmlTransformHook = (
    2. html: string,
    3. ctx: {
    4. path: string
    5. filename: string
    6. server?: ViteDevServer
    7. bundle?: import('rollup').OutputBundle
    8. chunk?: import('rollup').OutputChunk
    9. }
    10. ) =>
    11. | IndexHtmlTransformResult
    12. | void
    13. | Promise<IndexHtmlTransformResult | void>
    14. type IndexHtmlTransformResult =
    15. | string
    16. | HtmlTagDescriptor[]
    17. | {
    18. html: string
    19. tags: HtmlTagDescriptor[]
    20. }
    21. interface HtmlTagDescriptor {
    22. tag: string
    23. attrs?: Record<string, string>
    24. children?: string | HtmlTagDescriptor[]
    25. /**
    26. * 默认: 'head-prepend'
    27. */
    28. injectTo?: 'head' | 'body' | 'head-prepend' | 'body-prepend'
    29. }

handleHotUpdate

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

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

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

    • read 这是一个异步读函数,它返回文件的内容。之所以这样做,是因为在某些系统上,文件更改的回调函数可能会在编辑器完成文件更新之前过快地触发,并 fs.readFile 直接会返回空内容。传入的 read 函数规范了这种行为。

    钩子可以选择:

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

    • 返回一个空数组,并通过向客户端发送自定义事件来执行完整的自定义 HMR 处理:

      1. handleHotUpdate({ server }) {
      2. server.ws.send({
      3. type: 'custom',
      4. event: 'special-update',
      5. data: {}
      6. })
      7. return []
      8. }

      客户端代码应该使用 HMR API 注册相应的处理器(这应该被被相同插件的 transform 钩子注入):

      1. if (import.meta.hot) {
      2. import.meta.hot.on('special-update', (data) => {
      3. // 执行自定义更新
      4. })
      5. }

插件顺序

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

  • Alias
  • 带有 enforce: 'pre' 的用户插件
  • Vite 内置插件
  • 没有 enforce 值的用户插件
  • Vite 构建用的插件
  • 带有 enforce: 'post' 的用户插件

情景应用

默认情况下插件在部署(serve)和构建(build)模式中都会调用。如果插件只需要在服务或构建期间有条件地应用,请使用 apply 属性指明它们仅在 'build''serve' 模式时调用:

  1. function myPlugin() {
  2. return {
  3. name: 'build-only',
  4. apply: 'build' // 或 'serve'
  5. }
  6. }

Rollup 插件兼容性

相当数量的 Rollup 插件将直接作为 Vite 插件工作(例如:@rollup/plugin-alias@rollup/plugin-json),但并不是所有的,因为有些插件钩子在非构建式的开发服务器上下文中没有意义。

一般来说,只要一个 Rollup 插件符合以下标准,那么它应该只是作为一个 Vite 插件:

  • 没有使用 moduleParsed 钩子。
  • 它在打包钩子和输出钩子之间没有很强的耦合。

如果一个 Rollup 插件只在构建阶段有意义,则在 build.rollupOptions.plugins 下指定即可。

你也可以用 Vite 独有的属性来扩展现有的 Rollup 插件:

  1. // vite.config.js
  2. import example from 'rollup-plugin-example'
  3. export default {
  4. plugins: [
  5. {
  6. ...example(),
  7. enforce: 'post',
  8. apply: 'build' // 或者 'serve'
  9. }
  10. ]
  11. }

查看 Vite Rollup 插件 获取兼容的官方 Rollup 插件列表及其使用指南。

路径规范化

Vite 会在解析路径时使用 POSIX 分隔符( / )标准化路径,同时也适用于 Windows 的分卷。而另一方面,Rollup 在默认情况下保持解析的路径不变,因此解析的路径在 Windows 中会使用 win32 分隔符( \ )。然而,Rollup 插件会从 @rollup/pluginutils 中使用一个 normalizePath 工具函数,它在执行比较之前将分隔符转换为 POSIX。所以意味着当这些插件在 Vite 中使用时,includeexclude 两个配置模式,以及与已解析路径比较相似的路径会正常工作。

所以对于 Vite 插件来说,在将路径与已解析的路径进行比较时,首先规范化路径以使用 POSIX 分隔符是很重要的。从 vite 模块中也导出了一个等效的 normalizePath 工具函数。

  1. import { normalizePath } from 'vite'
  2. normalizePath('foo\\bar') // 'foo/bar'
  3. normalizePath('foo/bar') // 'foo/bar'

译者注:
[1] Falsy 虚值 MDN 文档