FORMA

插件开发

Vite 插件兼容 Rollup 插件,并扩展 configconfigureServertransformIndexHtml 等钩子。原理见 工作原理

一、Vite 特有钩子

Vite 在 Rollup 插件生命周期之上新增了以下钩子(仅开发环境或同时用于构建):

1. config

作用:修改 Vite 配置(在解析配置前调用)。可以返回一个对象(深度合并)或直接修改 config 参数。

js
// 修改 alias 或 base
function myPlugin() {
  return {
    name: "my-plugin",
    config(config, env) {
      // env: { mode, command }
      config.resolve.alias = { "@": "/src" };
      return config; // 或返回部分配置
    },
  };
}

2. configResolved

作用:在 Vite 配置最终确定后调用,可以获取完整的配置信息。

js
configResolved(resolvedConfig) {
  console.log('最终配置:', resolvedConfig);
  // 可以保存配置供其他钩子使用
  this.config = resolvedConfig;
}

3. configureServer

作用:配置开发服务器的自定义中间件。通常用于添加 Mock API、代理、静态文件等。

js
configureServer(server) {
  // server 是 ViteDevServer 实例
  server.middlewares.use('/api/mock', (req, res) => {
    res.end(JSON.stringify({ data: 'mock' }));
  });
}

高级用法:返回一个函数,可以在内部中间件之后执行:

js
configureServer(server) {
  return () => {
    server.middlewares.use((req, res, next) => {
      // 在所有 Vite 内置中间件之后执行
      next();
    });
  };
}

4. transformIndexHtml

作用:转换入口 HTML 文件(index.html)。可添加标签、修改内容等。

js
transformIndexHtml(html, ctx) {
  // ctx: { path, filename, originalUrl }
  return html.replace('<!-- inject -->', '<script>console.log("injected")</script>');
}

也可以返回对象数组,用于插入特定标签:

js
transformIndexHtml() {
  return [
    { tag: 'meta', attrs: { name: 'theme-color', content: '#fff' } },
  ];
}

5. handleHotUpdate

作用:自定义 HMR 更新逻辑,可以过滤或扩展热更新的模块。

js
handleHotUpdate({ file, modules, server }) {
  // 如果文件是某个特定组件,可以只重新加载该组件
  if (file.endsWith('Component.vue')) {
    return modules.filter(m => m.id === file);
  }
  // 默认返回所有受影响的模块
  return modules;
}

二、开发环境与生产环境钩子差异

Vite 插件钩子在不同环境下有选择性执行:

钩子开发环境生产环境说明
config都执行
configResolved都执行
configureServer仅开发服务器
transformIndexHtml构建时也会转换 HTML
handleHotUpdate仅开发 HMR
resolveIdRollup 钩子,都执行
load都执行
transform都执行
buildStart构建开始时
renderStart构建渲染开始时
generateBundle生成 bundle 时
writeBundle写入文件后

开发环境实际上是无打包的,因此不会触发 Rollup 的 generateBundle 等钩子。生产环境使用 Rollup 打包,这些钩子都会执行。

三、中间件模式

Vite 开发服务器基于 connect 中间件栈。通过 configureServer 可以注入自定义中间件,常用于:

  • Mock API:拦截特定请求返回 JSON。
  • 代理重写:修改代理规则或响应。
  • 静态文件托管:提供额外的静态资源目录。

示例:Mock API 中间件

js
export default function mockPlugin() {
  return {
    name: "mock-api",
    configureServer(server) {
      server.middlewares.use("/api/users", (req, res) => {
        res.setHeader("Content-Type", "application/json");
        res.end(JSON.stringify([{ id: 1, name: "Alice" }]));
      });
    },
  };
}

注意中间件顺序:默认情况下,自定义中间件会添加到内部中间件之前。如果需要在 Vite 内部中间件(如静态文件、HMR)之后执行,可以返回一个函数:

js
configureServer(server) {
  return () => {
    server.middlewares.use('/after', (req, res) => {
      // 在 Vite 内部中间件之后执行
    });
  };
}

四、虚拟模块

虚拟模块是指不实际存在于磁盘上的模块,通过插件动态生成内容。使用 resolveIdload 钩子实现。

基本模式

js
export default function virtualModulePlugin() {
  const virtualModuleId = "virtual:my-module";
  const resolvedVirtualModuleId = "\0" + virtualModuleId; // 前缀避免被其他插件处理

  return {
    name: "virtual-module",
    resolveId(id) {
      if (id === virtualModuleId) return resolvedVirtualModuleId;
    },
    load(id) {
      if (id === resolvedVirtualModuleId) {
        return `export const msg = "Hello from virtual module";`;
      }
    },
  };
}

然后在代码中导入:

js
import { msg } from "virtual:my-module";
console.log(msg); // "Hello from virtual module"

典型应用

  • 注入环境变量:将 .env 文件变量生成虚拟模块。
  • 生成路由表:根据文件系统自动生成路由配置。
  • 注入组件库注册信息:自动导入组件。

五、插件示例

1. 全局样式自动导入

自动在入口文件或每个模块中注入全局 CSS(如重置样式、主题变量)。

js
function globalStylePlugin() {
  return {
    name: "global-style",
    transformIndexHtml(html) {
      // 在 HTML 中直接插入 link 标签
      return html.replace(
        "</head>",
        '<link rel="stylesheet" href="/src/styles/global.css"></head>',
      );
    },
    // 或者使用 transform 钩子在 JS 模块中注入 import
    transform(code, id) {
      if (id.endsWith("main.js")) {
        return `import '/src/styles/global.css';\n${code}`;
      }
    },
  };
}

2. 组件库按需加载(类似 unplugin-vue-components)

自动解析模板中使用的组件并注册,无需手动导入。

js
import fs from "fs";
import path from "path";

function autoComponentsPlugin() {
  const componentsDir = path.resolve(__dirname, "src/components");
  const componentList = fs.readdirSync(componentsDir).map((file) => path.basename(file, ".vue"));

  return {
    name: "auto-components",
    transform(code, id) {
      if (!id.endsWith(".vue")) return;
      // 查找模板中使用了哪些组件名(简单正则匹配)
      const used = [];
      const regex = /<(?:[\w-]+)?\s+([^>]*?)(?:>|$)/g;
      let match;
      while ((match = regex.exec(code))) {
        const attrs = match[1];
        const tagMatch = attrs.match(/^(\w+)/);
        if (tagMatch && componentList.includes(tagMatch[1])) {
          used.push(tagMatch[1]);
        }
      }
      if (used.length) {
        const imports = used
          .map((name) => `import ${name} from '@/components/${name}.vue'`)
          .join("\n");
        return `${imports}\n<script>\nexport default { components: { ${used.join(",")} } }\n</script>${code}`;
      }
      return code;
    },
  };
}

3. 自动生成 API 类型文件

根据后端接口定义(如 OpenAPI 或自定义 JSON)生成 TypeScript 类型定义并作为虚拟模块提供。

js
import { generateTypes } from "./api-generator";

function apiTypesPlugin() {
  const virtualId = "virtual:api-types";
  const resolvedId = "\0" + virtualId;
  let typesContent = "";

  return {
    name: "api-types",
    buildStart() {
      typesContent = generateTypes(); // 生成类型字符串
    },
    resolveId(id) {
      if (id === virtualId) return resolvedId;
    },
    load(id) {
      if (id === resolvedId) return typesContent;
    },
  };
}

然后在代码中:

ts
import type { User, Post } from "virtual:api-types";

总结

钩子作用典型应用
config修改配置动态添加 alias、proxy
configureServer添加自定义中间件Mock API、代理
transformIndexHtml转换 HTML注入 script、meta
handleHotUpdate自定义 HMR特定模块只更新自身
虚拟模块动态生成模块内容类型、路由、常量

Vite 插件开发充分利用这些钩子,可以极大扩展 Vite 的能力,满足各种工程化需求。开发时注意区分开发/生产环境,合理使用 Rollup 兼容钩子。

参考文献

以下链接在编写时均可正常访问:

资料说明
插件 API官方
Rollup 插件兼容基础

Series

vite

5 / 6