Vite 插件开发

FreeGuideOnline 最新 2026-07-11

js // my-plugin.js export default function myVitePlugin(options = {}) { return { name: 'vite-plugin-my', // 必须,用于标识和调试 // 接下来可以定义各种钩子... } }


### 钩子(Hooks)简介

钩子就是插件在特定时机执行的函数。Vite 的钩子可以分为两大类:

-   **Rollup 共享钩子**:如 `buildStart`、`resolveId`、`load`、`transform`、`buildEnd` 等,在开发服务和生产构建中都会执行。
-   **Vite 独有钩子**:如 `config`、`configResolved`、`configureServer`、`transformIndexHtml`、`handleHotUpdate` 等,只在与 Vite 开发交互时有效。

## 搭建开发环境

### 创建插件项目

推荐使用 npm 或 yarn 初始化一个独立的项目:

```bash
mkdir vite-plugin-demo
cd vite-plugin-demo
npm init -y

配置 package.json

插件的包名通常以 vite-plugin- 开头,便于被社区发现。同时需要配置入口文件:

{
  "name": "vite-plugin-my-demo",
  "version": "1.0.0",
  "main": "src/index.js",
  "files": ["src"],
  "keywords": ["vite-plugin"],
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  }
}

建议在插件项目中创建一个 playground/ 目录,内嵌一个简易的 Vite 项目用于本地测试。

第一个插件:打印文件列表

让我们实现一个最简单的插件,它在构建开始时打印出所有入口文件的路径。

使用 buildStart 钩子

buildStart 是一个 Rollup 钩子,在每次构建开始前调用,可接收一个 options 参数,包含完整的构建配置。

// src/index.js
export default function fileListPlugin() {
  return {
    name: 'vite-plugin-file-list',
    buildStart(options) {
      console.log('本次构建的入口文件列表:');
      if (options.input) {
        const inputs = Array.isArray(options.input) ? options.input : [options.input];
        inputs.forEach(input => console.log(` - ${input}`));
      }
    }
  }
}

访问和过滤模块

你还可以在 moduleParsed 钩子中进一步分析已经被解析的模块信息,例如收集所有 CSS 模块的路径。

moduleParsed(moduleInfo) {
  if (moduleInfo.id.endsWith('.css')) {
    console.log('发现 CSS 模块:', moduleInfo.id);
  }
}

深入钩子机制

通用钩子与 Vite 专用钩子

  • 通用钩子:在开发模式和生产构建时均会调用,如 resolveIdloadtransform
  • Vite 专用钩子
    • 开发专用configureServerhandleHotUpdate 仅在 vite dev 时调用。
    • 构建专用renderChunkgenerateBundle 等 Rollup 输出钩子仅在 vite build 时调用。

钩子执行顺序

了解钩子的顺序有助于处理数据依赖。大致流程如下:

  1. 用户配置阶段:configconfigResolved
  2. 开发服务器启动:configureServer
  3. 模块解析加载(循环):resolveIdloadtransform
  4. 构建阶段:buildStart → 上述解析 → renderChunkgenerateBundlewriteBundlebuildEnd
  5. 关闭阶段:closeBundlecloseWatcher

虚拟模块与 ID 解析

虚拟模块是插件动态生成的“文件”,它在磁盘上并不存在,但对构建系统可见。常用于注入运行时配置、虚拟数据等。

resolveId 钩子

resolveId 用来拦截并自定义模块 ID 的解析。如果返回一个特殊字符串(如 \0virtual:my-module),就可以让后续的 load 钩子来处理该虚拟模块。

resolveId(id) {
  if (id === 'virtual:config') {
    return '\0virtual:config'; // \0 前缀约定,避免与其他插件冲突
  }
  return null; // 继续默认解析
}

load 钩子

load 钩子负责返回模块的源代码。当 id 匹配到我们解析出来的虚拟模块 ID 时,就可以返回自定义内容。

load(id) {
  if (id === '\0virtual:config') {
    return `export default ${JSON.stringify({ base: '/', env: 'dev' })};`;
  }
  return null;
}

示例:创建虚拟模块

用户代码中可以直接引入:

import config from 'virtual:config';
console.log(config.base); // '/'

转换与代码处理

transform 是使用最频繁的钩子之一,它在每个模块被加载后执行,可用于修改源码、进行预编译等操作。

transform 钩子

transform(code, id) {
  if (id.endsWith('.myext')) {
    // 将自定义文件转为 JS 模块
    const transformed = `export default ${JSON.stringify(code.trim())}`;
    return {
      code: transformed,
      map: null // 可选 source map
    };
  }
}

使用 @babel/core 或其他工具

你可以在 transform 中调用任何代码处理器,例如 Babel 转译、PostCSS 处理等。但注意:如果是对 .js/.ts 进行大量转换,建议优先使用 Vite 内置的 esbuild 或让用户配置相应的原生插件,避免过度影响性能。

import { transformAsync } from '@babel/core';

transform(code, id) {
  if (id.endsWith('.js')) {
    const result = await transformAsync(code, { /* babel config */ });
    return { code: result.code };
  }
}

示例:自动引入 CSS 模块

假设我们想让所有 .global.css 文件自动增加 index.css 的引用:

transform(code, id) {
  if (id.endsWith('.global.css')) {
    const injectedCode = `@import './index.css';\n${code}`;
    return { code: injectedCode };
  }
}

处理 HTML 与静态资源

transformIndexHtml 钩子

这个钩子可以修改 Vite 服务的 index.html 内容(字符串形式),常用于注入脚本、meta 标签或替换占位符。

transformIndexHtml(html, ctx) {
  // 在 <head> 前注入自定义数据
  return html.replace(
    '</head>',
    `<script>window.__MY_PLUGIN__ = { version: '1.0' }</script></head>`
  );
}

也可以返回一个包含 { html, tags } 的对象,使用 Vite 的 tags 约定来注入资源。

configureServer 与中间件

通过 configureServer 可以访问 Vite 的开发服务器实例,从而添加自定义中间件,实现 API 代理、模拟数据等功能。

configureServer(server) {
  server.middlewares.use((req, res, next) => {
    if (req.url.startsWith('/api/custom')) {
      res.statusCode = 200;
      res.end(JSON.stringify({ message: 'Hello from plugin' }));
      return;
    }
    next();
  });
}

插件配置与选项

接受用户选项

在插件工厂函数中接收 options 参数,并在返回的配置中使用 config 钩子读取最终的用户配置。

export default function myPlugin(options = { enableLog: true }) {
  return {
    name: 'vite-plugin-demo',
    config(config, env) {
      // 访问最终合并的 vite.config.js
      if (options.enableLog) {
        console.log('当前命令:', env.command);
      }
    }
  }
}

使用 config 钩子修改配置

config 钩子可以直接返回一个配置对象,来合并或覆盖用户原有的 Vite 配置。

config() {
  return {
    define: {
      __PLUGIN_VERSION__: JSON.stringify('1.0.0')
    },
    resolve: {
      alias: {
        '@my-lib': '/path/to/lib'
      }
    }
  }
}

注意:在 config 钩子中返回的值会被深合并,谨慎处理数组类配置。

发布与调试

本地测试

在插件项目根目录创建一个 example/playground/ 目录:

vite-plugin-demo/
├── src/
│   └── index.js
├── playground/
│   ├── index.html
│   ├── main.js
│   └── vite.config.js (引用本地插件)
└── package.json

playground/vite.config.js 中通过相对路径引用插件:

import myPlugin from '../src/index.js';

export default {
  plugins: [myPlugin({ /* options */ })]
};