12k
All articles

如何在 Vite 中构建和使用插件

通过生命周期钩子、虚拟模块和 Rollup 集成构建自定义 Vite 插件,实现文件转换、逻辑注入及构建流程扩展。

OpenReplay Team
OpenReplay Team
如何在 Vite 中构建和使用插件

创建自定义 Vite 插件可以让你扩展构建过程,超越默认配置的限制——无论你需要转换自定义文件类型、注入构建时逻辑,还是添加开发服务器中间件。如果你已经触及标准 Vite 配置的极限,构建自己的插件就是自然而然的下一步。

本指南涵盖了核心概念:Vite 的 Plugin API 如何工作、你将使用的关键生命周期钩子,以及帮助你入门的实用示例。你将学会创建、测试和发布插件,以解决开发工作流中的实际问题。

核心要点

  • Vite 插件通过生命周期钩子扩展开发和生产构建过程
  • 大多数 Rollup 插件可以在 Vite 中使用,但开发服务器功能需要 Vite 特定的钩子
  • 在转向虚拟模块等高级模式之前,先从简单的转换开始
  • 发布插件时遵循命名约定和文档标准

理解 Vite 插件及其与 Rollup 的关系

Vite 插件是 JavaScript 对象,它们钩入开发和构建过程的不同阶段。它们基于 Rollup 的插件系统构建,并添加了 Vite 开发服务器特定的额外钩子。

Vite 在底层使用两种不同的构建工具:esbuild 为开发服务器提供原生 ES 模块的极速支持,而 Rollup 处理生产环境打包以获得最优输出。这种双重架构意味着你的插件可以使用 apply 选项针对特定环境:

function myPlugin(): Plugin {
  return {
    name: 'my-plugin',
    apply: 'serve', // 仅在开发环境运行
    // ... 钩子
  }
}

大多数 Rollup 插件无需修改即可在 Vite 中使用。但是,如果你需要开发服务器功能——比如添加中间件或在开发期间修改 HTML——你将需要 Vite 特定的钩子。

基本插件结构和配置

每个 Vite 插件必须有一个 name 属性和至少一个钩子函数:

import type { Plugin } from 'vite'

function myPlugin(options?: MyPluginOptions): Plugin {
  return {
    name: 'vite-plugin-example',
    enforce: 'pre', // 可选:控制插件执行顺序
    apply: 'build', // 可选:'serve' | 'build' | undefined
    
    // 钩子函数放在这里
    transform(code, id) {
      if (id.endsWith('.custom')) {
        return transformCustomFile(code)
      }
    }
  }
}

Plugin API 为所有钩子和选项提供了 TypeScript 类型定义。从 ‘vite’ 导入 Plugin 类型以获得完整的类型安全和自动补全。

Plugin API 中的核心生命周期钩子

不同的钩子在构建过程的不同阶段触发。以下是最常用的钩子:

配置钩子

  • config:在配置解析之前修改 Vite 配置
  • configResolved:访问最终解析的配置

转换钩子

  • transform:修改单个模块的源代码
  • load:为特定文件类型提供自定义加载逻辑
  • resolveId:自定义模块解析
{
  name: 'transform-plugin',
  transform(code, id) {
    if (!id.includes('node_modules') && id.endsWith('.js')) {
      return {
        code: addCustomHeader(code),
        map: null // Source map 支持
      }
    }
  }
}

服务器和构建钩子

  • configureServer:向开发服务器添加自定义中间件
  • transformIndexHtml:修改 HTML 文件
  • writeBundle:在打包文件写入磁盘后运行

每个钩子都有特定的返回类型和执行时机。transform 钩子会为每个模块运行,因此要保持快速。对特定文件的昂贵操作使用 load

构建你的第一个 Vite 插件:实用示例

让我们构建三个实用插件来演示常见模式:

示例 1:HTML 修改

function htmlPlugin(): Plugin {
  return {
    name: 'html-modifier',
    transformIndexHtml(html) {
      return html.replace(
        '</head>',
        '<script>console.log("Injected!")</script></head>'
      )
    }
  }
}

示例 2:开发服务器中间件

function analyticsPlugin(): Plugin {
  return {
    name: 'request-analytics',
    configureServer(server) {
      server.middlewares.use((req, res, next) => {
        console.log(`${req.method} ${req.url}`)
        next()
      })
    }
  }
}

示例 3:构建时文件生成

import path from 'path'
import fs from 'fs'
import type { Plugin, ResolvedConfig } from 'vite'

function generateManifest(): Plugin {
  let config: ResolvedConfig
  
  return {
    name: 'generate-manifest',
    configResolved(resolvedConfig) {
      config = resolvedConfig
    },
    writeBundle() {
      const manifestPath = path.join(config.build.outDir, 'manifest.json')
      fs.writeFileSync(manifestPath, JSON.stringify({
        timestamp: Date.now(),
        version: process.env.npm_package_version
      }))
    }
  }
}

对于调试,在钩子中使用 console.log 来跟踪执行。this.warn()this.error() 方法比抛出异常提供更好的错误报告。

高级插件模式和最佳实践

虚拟模块

创建磁盘上不存在的模块:

const virtualModuleId = 'virtual:my-module'
const resolvedVirtualModuleId = '\0' + virtualModuleId

export function virtualPlugin(): Plugin {
  return {
    name: 'virtual-plugin',
    resolveId(id) {
      if (id === virtualModuleId) {
        return resolvedVirtualModuleId
      }
    },
    load(id) {
      if (id === resolvedVirtualModuleId) {
        return `export const msg = "from virtual module"`
      }
    }
  }
}

性能优化

缓存昂贵的转换操作:

const cache = new Map<string, string>()

function cachedTransform(): Plugin {
  return {
    name: 'cached-transform',
    transform(code, id) {
      if (cache.has(id)) return cache.get(id)
      
      const result = expensiveOperation(code)
      cache.set(id, result)
      return result
    }
  }
}

使用 VitestcreateServer API 测试你的 Vite 插件,以独立验证钩子行为。

发布和分享你的插件

发布时遵循以下约定:

  1. 将包命名为 vite-plugin-[name]
  2. 在 package.json 的 keywords 中包含 "vite-plugin"
  3. 说明为什么它是 Vite 特定的(如果使用了 Vite 专有钩子)
  4. 如果是框架特定的,添加框架前缀:vite-plugin-vue-[name]

创建清晰的 README,包含安装说明、配置示例和 API 文档。将你的插件提交到 awesome-vite 以触达社区。

总结

构建自定义 Vite 插件让你能够完全控制构建过程。从使用 transformtransformIndexHtml 等钩子的简单转换开始,然后根据需要扩展到更复杂的模式。Plugin API 强大而易于上手——大多数插件只需要几个钩子就能解决特定问题。

要深入探索,请查看 Vite 官方插件文档 并研究生态系统中的流行插件。你的下一个构建优化或工作流改进可能只需一个插件就能实现。

常见问题

我可以在 Vite 中使用现有的 Rollup 插件吗?

可以,大多数 Rollup 插件无需修改即可在 Vite 中使用。但是,依赖 moduleParsed 钩子或需要开发服务器功能的插件需要进行 Vite 特定的适配。请查看插件文档中的 Vite 兼容性说明。

如何在开发过程中调试 Vite 插件?

在钩子中使用 console.log 语句来跟踪执行流程。this.warn() 和 this.error() 方法提供更好的错误报告。你还可以在 Vite 中使用 DEBUG 环境变量来查看详细日志。

插件中的 enforce pre 和 post 有什么区别?

enforce 选项控制插件的执行顺序。pre 插件在 Vite 核心插件之前运行,post 插件在之后运行。使用 pre 进行输入转换,使用 post 进行输出修改。如果不设置 enforce,插件按照它们出现的顺序运行。

如果我的插件只适用于特定用例,我应该发布它吗?

如果你的插件解决了其他人可能面临的问题,即使是专业化的,也可以考虑发布。清楚地记录其特定用例。对于真正项目特定的插件,将它们保留在本地或私有仓库中。

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.