Chapter 07

插件系统与生态

复用 Rollup 插件 + Vite 独家钩子,让你能把任何奇怪需求塞进构建流程。

7.1 Vite 插件 = Rollup 插件 + Vite 扩展

Vite 的插件接口 超集了 Rollup 的插件接口——意味着绝大多数 Rollup 插件能直接用,同时 Vite 又加了几个自己的专属钩子(主要为 dev 服务器设计)。

一个插件就是一个对象(或返回对象的函数),含 name、若干钩子、可选的 enforce/apply

const myPlugin = {
  name: 'my-plugin',
  enforce: 'pre',         // 'pre' | 'post' 控制顺序
  apply: 'build',          // 只在 build 阶段生效,或 'serve'
  transform(code, id) { /* ... */ },
};

7.2 通用钩子(Rollup 兼容)

resolveId(source, importer)
自定义模块路径解析。返回字符串表示"把这个路径当成解析结果"。
load(id)
根据路径返回文件内容(字符串或 { code, map })。常用于虚拟模块。
transform(code, id)
拿到文件内容后的转换钩子——最常用。返回 { code, map } 或新字符串。
buildStart / buildEnd
构建开始/结束时的回调。
generateBundle / writeBundle
Rollup 生成和写出 bundle 时的钩子(仅 build 阶段有意义)。

7.3 Vite 专属钩子

config(config, { command, mode })
读到用户配置后、合并前修改配置。
configResolved(resolvedConfig)
配置完全解析后——保存配置到插件局部变量,后续 transform 用。
configureServer(server)
拿到 dev 服务器实例,能加中间件、监听 WebSocket、访问文件监听器。
configurePreviewServer(server)
类似,但针对 vite preview
transformIndexHtml(html, ctx)
专门转换 index.html——返回新 HTML 字符串或标签对象数组。
handleHotUpdate(ctx)
自定义 HMR:决定哪些模块需要热替换。

7.4 虚拟模块:加载不存在的文件

最经典的插件用法——让 import 'virtual:app-version' 能 import "根本不存在"的模块:

import type { Plugin } from 'vite';

export function appVersionPlugin(): Plugin {
  const virtualId = 'virtual:app-version';
  const resolvedId = '\0virtual:app-version';

  return {
    name: 'app-version',
    resolveId(id) {
      if (id === virtualId) return resolvedId;
    },
    load(id) {
      if (id === resolvedId) {
        return `export const version = "1.2.3"; export const buildTime = ${Date.now()};`;
      }
    },
  };
}
// 业务代码
import { version, buildTime } from 'virtual:app-version';
为什么 resolvedId 前缀 \0

Rollup 约定:虚拟模块的解析结果应该以 \0(null 字节)开头,防止其他插件/Node 误认为是真实文件路径。这是生态一致性约定。

7.5 transform 示例:把 Markdown 变成组件

import { marked } from 'marked';

export function mdPlugin(): Plugin {
  return {
    name: 'md-to-react',
    transform(code, id) {
      if (!id.endsWith('.md')) return;
      const html = marked(code);
      return {
        code: `import React from 'react';
          export default function() { return React.createElement('div', {
            dangerouslySetInnerHTML: { __html: ${JSON.stringify(html)} }
          }); }`,
        map: null,
      };
    },
  };
}

7.6 configureServer:自定义中间件

export function apiMockPlugin(): Plugin {
  return {
    name: 'api-mock',
    configureServer(server) {
      server.middlewares.use('/api/hello', (req, res) => {
        res.setHeader('content-type', 'application/json');
        res.end(JSON.stringify({ hello: 'world' }));
      });
    },
  };
}

7.7 transformIndexHtml:注入标签

export function injectEnvPlugin(): Plugin {
  return {
    name: 'inject-env',
    transformIndexHtml: {
      order: 'pre',
      handler(html) {
        return {
          html,
          tags: [{
            tag: 'script',
            children: `window.__ENV__ = ${JSON.stringify({ BUILD: Date.now() })};`,
            injectTo: 'head-prepend',
          }],
        };
      },
    },
  };
}

7.8 handleHotUpdate:自定义 HMR 决策

export function customHmrPlugin(): Plugin {
  return {
    name: 'custom-hmr',
    handleHotUpdate({ file, server, modules }) {
      if (file.endsWith('.locale.json')) {
        // 语言文件变了:发自定义事件让客户端 reload i18n
        server.ws.send({ type: 'custom', event: 'locale-update' });
        return [];      // 返回空数组 = 我已处理,Vite 不要再动
      }
    },
  };
}

7.9 常用插件精选

插件用途
@vitejs/plugin-react / react-swcReact JSX + Fast Refresh
@vitejs/plugin-vueVue 3 SFC 支持
@vitejs/plugin-legacy为旧浏览器生成 nomodule 产物
vite-plugin-svgrSVG 作为 React 组件 import
vite-tsconfig-paths自动把 tsconfig.paths 同步到 Vite alias
vite-plugin-pages文件路由(类 Next.js pages/)
unplugin-auto-import自动 import 常用 API
unplugin-icons按需导入 Iconify 图标
vite-plugin-pwaPWA / Service Worker 一键接入
@tailwindcss/viteTailwind v4 官方插件
vite-plugin-dts库模式下生成 .d.ts
vite-plugin-inspect打开 /__inspect/ 查看每个模块的 transform 链路,调试神器
unplugin 是什么?

unplugin 是一个构建工具无关的插件框架——同一份插件代码能在 Vite、Rollup、webpack、Rspack、esbuild 里跑。unplugin-auto-importunplugin-icons 等就属于它。写公共插件时优先考虑 unplugin。

7.10 小结