Skip to content

Vite 插件开发指南

从基础概念到实战应用的完整 Vite 插件开发教程。

1. 基础概念

1.1 什么是 Vite 插件

Vite 插件是基于 Rollup 插件架构的扩展系统,用于在构建过程中执行自定义逻辑。插件可以:

  • 修改配置:在构建开始前调整 Vite 配置
  • 处理文件:转换、加载、解析各种类型的文件
  • 注入代码:在构建过程中动态添加代码
  • 优化构建:压缩、分析、缓存等构建优化
  • 开发增强:热更新、代理、中间件等开发体验优化

1.2 插件基本结构

typescript
import type { Plugin } from "vite";

export function myPlugin(options = {}): Plugin {
  return {
    // 插件名称(必需)
    name: "my-plugin",

    // 执行时机控制
    enforce: "pre", // 'pre' | 'post' | undefined

    // 应用条件
    apply: "build", // 'build' | 'serve' | function

    // 钩子函数
    configResolved(config) {
      // 配置解析完成后执行
    },

    transform(code, id) {
      // 代码转换时执行
      return code;
    },
  };
}

1.3 插件配置选项

配置项类型说明示例
namestring插件名称,用于调试和错误提示'vite-plugin-demo'
enforce'pre' | 'post'执行顺序,pre 最先,post 最后'pre'
apply'build' | 'serve' | function应用场景限制'build'
externalstring[]外部依赖,不打包进 bundle['lodash']

2. 钩子函数详解

2.1 配置阶段钩子

config(config, { command })

  • 触发时机:配置文件解析之前
  • 用途:修改用户配置
  • 参数
    • config: 用户配置对象
    • command: 'build' | 'serve'
  • 返回值UserConfig | void
typescript
config(config, { command }) {
  if (command === 'serve') {
    // 开发模式配置
    config.server = config.server || {}
    config.server.port = 3000
  }
}

configResolved(resolvedConfig)

  • 触发时机:配置解析完成后
  • 用途:获取最终配置,不能修改
  • 参数resolvedConfig - 最终配置对象
  • 返回值void
typescript
configResolved(resolvedConfig) {
  this.isProduction = resolvedConfig.command === 'build'
  this.root = resolvedConfig.root
}

configureServer(server)

  • 触发时机:开发服务器创建时
  • 用途:配置开发服务器,添加中间件
  • 参数server - Vite 开发服务器实例
  • 返回值void
typescript
configureServer(server) {
  // 添加 API 路由
  server.middlewares.use('/api/hello', (req, res) => {
    res.end('Hello from plugin!')
  })
}

2.2 构建阶段钩子

buildStart(options)

  • 触发时机:构建开始时
  • 用途:初始化构建状态
  • 参数options - 构建选项
  • 返回值void

resolveId(id, importer)

  • 触发时机:解析模块 ID 时
  • 用途:自定义模块解析逻辑
  • 参数
    • id: 模块标识符
    • importer: 导入该模块的文件路径
  • 返回值string | void
typescript
resolveId(id) {
  if (id === 'virtual:my-module') {
    return id // 返回虚拟模块 ID
  }
}

load(id)

  • 触发时机:加载模块内容时
  • 用途:提供模块内容
  • 参数id - 模块 ID
  • 返回值string | void
typescript
load(id) {
  if (id === 'virtual:my-module') {
    return 'export const msg = "Hello from virtual module!"'
  }
}

transform(code, id)

  • 触发时机:转换模块代码时
  • 用途:修改源代码
  • 参数
    • code: 源代码字符串
    • id: 文件路径
  • 返回值string | TransformResult | void
typescript
transform(code, id) {
  if (id.endsWith('.vue')) {
    // 转换 Vue 文件
    return transformVueCode(code)
  }
}

2.3 生成阶段钩子

generateBundle(options, bundle)

  • 触发时机:生成 bundle 时
  • 用途:修改最终输出
  • 参数
    • options: 输出选项
    • bundle: 打包结果对象

writeBundle(options, bundle)

  • 触发时机:写入文件后
  • 用途:后处理操作

2.4 开发专用钩子

handleHotUpdate(ctx)

  • 触发时机:文件变更时(仅开发模式)
  • 用途:自定义热更新逻辑
  • 参数ctx - 热更新上下文
  • 返回值ModuleNode[] | void
typescript
handleHotUpdate({ file, modules }) {
  if (file.endsWith('.data.json')) {
    // 自定义文件类型的热更新
    console.log('Data file updated:', file)
    return modules
  }
}

transformIndexHtml(html, context)

  • 触发时机:处理 HTML 文件时
  • 用途:修改 HTML 内容
  • 返回值string | HtmlTagDescriptor[]
typescript
transformIndexHtml(html) {
  return html.replace(
    '<head>',
    '<head>\n  <meta name="generator" content="my-plugin">'
  )
}

3. 钩子执行流程

3.1 开发模式流程图

mermaid
graph TD
    A[启动 vite serve] --> B[config]
    B --> C[configResolved]
    C --> D[configureServer]
    D --> E[buildStart]
    E --> F[服务器启动]
    F --> G[文件请求]
    G --> H[resolveId]
    H --> I[load]
    I --> J[transform]
    J --> K[返回处理结果]
    K --> L{文件变更?}
    L -->|是| M[handleHotUpdate]
    L -->|否| N[继续服务]
    M --> O[HMR 更新]
    O --> N
    N --> G

3.2 构建模式流程图

mermaid
graph TD
    A[启动 vite build] --> B[config]
    B --> C[configResolved]
    C --> D[buildStart]
    D --> E[resolveId]
    E --> F[load]
    F --> G[transform]
    G --> H[generateBundle]
    H --> I[writeBundle]
    I --> J[构建完成]

    K[处理 HTML] --> L[transformIndexHtml]
    L --> M[HTML 输出]

    G --> K

3.3 钩子执行顺序

阶段钩子执行顺序说明
配置阶段configconfigResolvedconfigureServer配置解析和服务器设置
构建初始化buildStart构建开始
模块处理resolveIdloadtransform按需执行,每个模块都会经历
代码生成generateBundlewriteBundle仅构建模式
开发特殊handleHotUpdate仅开发模式,文件变更时
HTML 处理transformIndexHtmlHTML 文件处理时

4. 简单示例入门

4.1 最简单的插件

让我们从一个最基础的插件开始,理解插件的工作原理:

typescript
// 最简单的插件 - 只打印信息
import type { Plugin } from "vite";

export function helloPlugin(): Plugin {
  return {
    name: "hello-plugin",

    configResolved() {
      console.log("👋 Hello! Plugin is loaded!");
    },
  };
}

4.2 带配置的插件

typescript
// 带选项配置的插件
export interface HelloPluginOptions {
  message?: string;
  showTime?: boolean;
}

export function helloPlugin(options: HelloPluginOptions = {}): Plugin {
  const { message = "Hello", showTime = false } = options;

  return {
    name: "hello-plugin",

    configResolved() {
      const time = showTime ? ` at ${new Date().toLocaleTimeString()}` : "";
      console.log(`👋 ${message}! Plugin is loaded${time}`);
    },
  };
}

4.3 代码转换插件

typescript
// 简单的代码转换插件
export function commentPlugin(): Plugin {
  return {
    name: "comment-plugin",

    transform(code, id) {
      // 只处理 .js 和 .ts 文件
      if (id.endsWith(".js") || id.endsWith(".ts")) {
        // 在文件开头添加注释
        return `/* 由 comment-plugin 处理 */\n${code}`;
      }
      // 不处理其他文件
      return null;
    },
  };
}

4.4 虚拟模块插件

typescript
// 创建虚拟模块的插件
export function virtualPlugin(): Plugin {
  return {
    name: "virtual-plugin",

    resolveId(id) {
      if (id === "virtual:config") {
        return id; // 告诉 Vite 这是一个虚拟模块
      }
    },

    load(id) {
      if (id === "virtual:config") {
        // 返回虚拟模块的内容
        return `
          export const appName = 'My App'
          export const version = '1.0.0'
          export const buildTime = '${new Date().toISOString()}'
        `;
      }
    },
  };
}

4.5 使用插件

typescript
// vite.config.ts
import { defineConfig } from "vite";
import { helloPlugin, commentPlugin, virtualPlugin } from "./plugins";

export default defineConfig({
  plugins: [
    helloPlugin({
      message: "Welcome to Vite",
      showTime: true,
    }),
    commentPlugin(),
    virtualPlugin(),
  ],
});
typescript
// 在代码中使用虚拟模块
import { appName, version, buildTime } from "virtual:config";

console.log(`${appName} v${version} built at ${buildTime}`);

5. 实战应用场景

| transform | 代码转换时 | 转换源码 | TransformResult \| void | | handleHotUpdate | 文件变更时 | 自定义 HMR | HmrContext[] \| void | | transformIndexHtml | HTML 转换时 | 修改 HTML | string \| HtmlTagDescriptor[] |

生命周期流程图

mermaid
graph TD
    A[启动 Vite] --> B[config]
    B --> C[configResolved]
    C --> D{开发模式?}
    D -->|是| E[configureServer]
    D -->|否| F[构建流程]
    E --> G[resolveId]
    F --> G
    G --> H[load]
    H --> I[transform]
    I --> J{开发模式?}
    J -->|是| K[handleHotUpdate]
    J -->|否| L[transformIndexHtml]
    K --> M[HMR 更新]
    L --> N[构建完成]

核心 Hook 示例

typescript
// 完整的 Hook 示例
export function fullDemoPlugin(): Plugin {
  return {
    name: "full-demo-plugin",

    // 1. 修改配置
    config(config, { command }) {
      if (command === "serve") {
        config.server = config.server || {};
        config.server.port = 3000;
      }
    },

    // 2. 配置确定后执行
    configResolved(resolvedConfig) {
      this.isProduction = resolvedConfig.command === "build";
    },

    // 3. 配置开发服务器
    configureServer(server) {
      server.middlewares.use("/api/hello", (req, res) => {
        res.end("Hello from plugin middleware!");
      });
    },

    // 4. 解析模块 ID
    resolveId(id) {
      if (id === "virtual:my-plugin") {
        return id; // 返回虚拟模块 ID
      }
    },

    // 5. 加载模块内容
    load(id) {
      if (id === "virtual:my-plugin") {
        return 'export const msg = "Hello from virtual module!"';
      }
    },

    // 6. 转换代码
    transform(code, id) {
      if (id.includes("main.ts")) {
        return `// Transformed by plugin\n${code}`;
      }
    },

    // 7. 处理 HMR 更新
    handleHotUpdate({ file, modules }) {
      if (file.endsWith(".special")) {
        // 返回需要更新的模块
        return modules;
      }
    },

    // 8. 转换 HTML
    transformIndexHtml(html) {
      return html.replace(
        "<head>",
        '<head>\n  <meta name="plugin" content="demo-plugin">'
      );
    },
  };
}

注意事项:Hook 执行顺序固定,enforce: 'pre' 可提前执行。

3. 开发态能力(Dev/HMR)

中间件与 WebSocket

typescript
export function devPlugin(): Plugin {
  return {
    name: "dev-plugin",

    configureServer(server) {
      // 添加 API 中间件
      server.middlewares.use("/api/status", (req, res) => {
        res.setHeader("Content-Type", "application/json");
        res.end(JSON.stringify({ status: "ok", timestamp: Date.now() }));
      });

      // 监听文件变化并通知客户端
      const { ws } = server;
      server.watcher.on("change", (file) => {
        if (file.endsWith(".config.json")) {
          ws.send({
            type: "full-reload",
            path: "*",
          });
        }
      });
    },
  };
}

HMR 自定义更新

typescript
export function hmrPlugin(): Plugin {
  return {
    name: "hmr-plugin",

    handleHotUpdate({ file, modules, server }) {
      // 自定义文件类型的 HMR 处理
      if (file.endsWith(".data.json")) {
        console.log("Data file changed:", file);

        // 通知客户端自定义更新
        server.ws.send({
          type: "custom",
          event: "data-update",
          data: { file },
        });

        // 返回空数组阻止默认 HMR
        return [];
      }

      // 返回受影响的模块
      return modules;
    },
  };
}

HMR 时序图

mermaid
sequenceDiagram
    participant F as 文件系统
    participant W as Watcher
    participant P as Plugin
    participant S as Server
    participant C as Client

    F->>W: 文件变更
    W->>P: handleHotUpdate
    P->>P: 分析影响范围
    P->>S: 返回受影响模块
    S->>C: WebSocket 推送更新
    C->>C: 执行 HMR 更新
    C->>S: 请求新模块
    S->>C: 返回更新内容

预期现象:文件变更时控制台显示自定义日志,浏览器接收到自定义事件。

4. 构建态能力(Rollup 流水线)

完整构建流程示例

typescript
export function buildPlugin(): Plugin {
  return {
    name: "build-plugin",

    // 解析虚拟模块
    resolveId(id) {
      if (id === "virtual:build-info") {
        return "\0virtual:build-info"; // \0 前缀标记虚拟模块
      }
    },

    // 加载构建信息
    load(id) {
      if (id === "\0virtual:build-info") {
        return `export const buildTime = "${new Date().toISOString()}";
export const version = "${process.env.npm_package_version || "1.0.0"}";`;
      }
    },

    // 转换代码并生成 SourceMap
    transform(code, id) {
      if (id.endsWith(".banner.js")) {
        const banner = "/* Build by Custom Plugin */\n";
        return {
          code: banner + code,
          map: null, // 简单示例,实际应生成正确的 SourceMap
        };
      }
    },

    // 生成额外资源
    generateBundle(options, bundle) {
      // 生成构建报告
      const report = {
        timestamp: new Date().toISOString(),
        chunks: Object.keys(bundle).length,
        assets: Object.values(bundle).filter((item) => item.type === "asset")
          .length,
      };

      // 输出 JSON 文件
      this.emitFile({
        type: "asset",
        fileName: "build-report.json",
        source: JSON.stringify(report, null, 2),
      });

      // 为每个 chunk 添加 banner
      for (const chunk of Object.values(bundle)) {
        if (chunk.type === "chunk") {
          chunk.code = `/* Built at ${new Date().toISOString()} */\n${
            chunk.code
          }`;
        }
      }
    },
  };
}

使用示例

typescript
// playground/src/main.ts
import { buildTime, version } from "virtual:build-info";

console.log(`App version: ${version}, built at: ${buildTime}`);

预期现象:构建产物包含时间戳 banner,生成 build-report.json 文件。

5. 常见场景范式

5.1 虚拟模块

typescript
export function virtualModulePlugin(): Plugin {
  const virtualModules = new Map<string, string>();

  return {
    name: "virtual-module-plugin",

    resolveId(id) {
      if (id.startsWith("virtual:")) {
        return "\0" + id; // \0 前缀避免与真实文件冲突
      }
    },

    load(id) {
      if (id.startsWith("\0virtual:")) {
        const moduleId = id.slice(1); // 移除 \0 前缀
        return virtualModules.get(moduleId) || `export default {}`;
      }
    },

    configureServer(server) {
      // 动态更新虚拟模块
      server.middlewares.use("/api/update-virtual", (req, res) => {
        virtualModules.set(
          "virtual:config",
          `export const config = ${JSON.stringify({ updated: Date.now() })}`
        );

        // 触发 HMR 更新
        const module = server.moduleGraph.getModuleById("\0virtual:config");
        if (module) {
          server.reloadModule(module);
        }

        res.end("Updated");
      });
    },
  };
}

5.2 源码注入与 SourceMap

typescript
import { createFilter } from "@rollup/pluginutils";
import MagicString from "magic-string";

export function injectPlugin(
  options: { include?: string[]; exclude?: string[] } = {}
): Plugin {
  const filter = createFilter(
    options.include || ["**/*.js", "**/*.ts"],
    options.exclude
  );

  return {
    name: "inject-plugin",

    transform(code, id) {
      if (!filter(id)) return;

      const s = new MagicString(code);

      // 在文件开头注入代码
      s.prepend('console.log("File loaded:", import.meta.url);\n');

      // 在函数调用前注入代码
      const functionCallRegex = /console\.log\(/g;
      let match;
      while ((match = functionCallRegex.exec(code)) !== null) {
        s.appendLeft(match.index, "/* Enhanced */ ");
      }

      return {
        code: s.toString(),
        map: s.generateMap({ hires: true }),
      };
    },
  };
}

5.3 HTML 标签注入

typescript
export function htmlInjectPlugin(): Plugin {
  return {
    name: "html-inject-plugin",

    transformIndexHtml: {
      enforce: "pre",
      transform(html, context) {
        // 方式1:字符串替换
        if (context.server) {
          html = html.replace(
            "<head>",
            "<head>\n  <script>window.__DEV__ = true</script>"
          );
        }

        // 方式2:返回标签描述符数组
        return [
          {
            tag: "meta",
            attrs: { name: "generator", content: "Vite Plugin" },
            injectTo: "head",
          },
          {
            tag: "script",
            attrs: { type: "module" },
            children: 'console.log("Injected by plugin")',
            injectTo: "body",
          },
        ];
      },
    },
  };
}

5.4 文件过滤器

typescript
import { createFilter } from "@rollup/pluginutils";

export function filterPlugin(
  options: {
    include?: string | string[];
    exclude?: string | string[];
    extensions?: string[];
  } = {}
): Plugin {
  const {
    include = ["**/*.js", "**/*.ts"],
    exclude = ["node_modules/**"],
    extensions = [".js", ".ts", ".vue"],
  } = options;

  const filter = createFilter(include, exclude);

  return {
    name: "filter-plugin",

    transform(code, id) {
      // 基础过滤
      if (!filter(id)) return;

      // 扩展名过滤
      if (!extensions.some((ext) => id.endsWith(ext))) return;

      // 自定义过滤逻辑
      if (id.includes("test") || id.includes("spec")) return;

      return `// Processed by filter plugin\n${code}`;
    },
  };
}

运行命令

bash
cd playground && npx vite build --mode production

6. 测试与发布

6.1 单元测试

typescript
// tests/plugin.test.ts
import { describe, it, expect } from "vitest";
import { demoPlugin } from "../src/index";

describe("demoPlugin", () => {
  it("should transform code correctly", async () => {
    const plugin = demoPlugin({ message: "Test Message" });
    const result = plugin.transform?.call(
      {} as any,
      'console.log("hello")',
      "test.js"
    );

    expect(result).toContain("/* Test Message */");
    expect(result).toContain('console.log("hello")');
  });

  it("should resolve virtual modules", () => {
    const plugin = demoPlugin();
    const resolved = plugin.resolveId?.call({} as any, "virtual:test");

    expect(resolved).toBe("virtual:test");
  });
});

6.2 集成测试

typescript
// tests/integration.test.ts
import { createServer } from "vite";
import { demoPlugin } from "../src/index";

describe("Integration Tests", () => {
  it("should work with Vite dev server", async () => {
    const server = await createServer({
      plugins: [demoPlugin()],
      logLevel: "silent",
    });

    await server.listen();

    // 测试插件是否正常加载
    expect(
      server.config.plugins.some((p) => p.name === "vite-plugin-demo")
    ).toBe(true);

    await server.close();
  });
});

6.3 打包配置

typescript
// tsup.config.ts
import { defineConfig } from "tsup";

export default defineConfig({
  entry: ["src/index.ts"],
  format: ["cjs", "esm"],
  dts: true,
  clean: true,
  external: ["vite"],
});

6.4 Package.json 配置

json
{
  "name": "vite-plugin-demo",
  "version": "1.0.0",
  "type": "module",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "require": "./dist/index.cjs",
      "types": "./dist/index.d.ts"
    }
  },
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "files": ["dist"],
  "scripts": {
    "build": "tsup",
    "test": "vitest",
    "dev": "cd playground && vite",
    "build:playground": "cd playground && vite build"
  },
  "peerDependencies": {
    "vite": "^4.0.0 || ^5.0.0"
  },
  "keywords": ["vite", "plugin", "vite-plugin"]
}

6.5 发布流程

bash
# 构建插件
npm run build

# 运行测试
npm test

# 发布到 npm(需要 2FA)
npm login
npm publish --access public

# 或使用 changeset 管理版本
npx changeset
npx changeset version
npx changeset publish

7. 常见坑速查

症状原因解法
插件不执行enforce 顺序问题设置 enforce: 'pre' 或调整插件顺序
SourceMap 丢失transform 未返回 map使用 MagicString 生成正确的 SourceMap
HMR 不生效handleHotUpdate 返回值错误返回正确的 modules 数组或空数组
虚拟模块冲突未使用 \0 前缀resolveId 返回时添加 \0 前缀
只在 dev 生效Hook 仅在开发模式触发检查 Hook 的适用阶段,使用 apply 配置
只在 build 生效使用了构建专用 Hook区分开发和构建 Hook,或使用通用 Hook
SSR 报错 window 未定义服务端执行了客户端代码添加 typeof window !== 'undefined' 判断
缓存未失效Vite 缓存了转换结果删除 node_modules/.vite 目录
过滤规则不生效createFilter 配置错误检查 include/exclude 模式匹配
与其他插件冲突执行顺序问题调整 enforce 或插件数组位置
emitFile 路径错误fileName 包含非法字符使用相对路径,避免 ../
TypeScript 类型错误缺少类型定义安装 @types/node 和正确的 Vite 类型
插件选项不生效选项合并逻辑错误使用解构赋值设置默认值
开发服务器中间件 404路径匹配问题检查中间件路径是否正确

排错技巧

  • 使用 console.log 确认 Hook 执行
  • 检查 this.isProduction 区分环境
  • 查看 vite --debug 详细日志

完成清单: ✅ 最小插件模板
✅ Playground 演示环境
✅ 单元测试与集成测试
✅ 构建与发布脚本
✅ 常见问题解决方案