DevToolBox免费
博客

Vite插件开发:从零创建自定义插件 2026

13 分钟作者 DevToolBox

Vite 插件开发:自定义转换与开发工具构建

Vite 的插件 API 是对 Rollup API 的轻量封装,并扩展了开发服务器钩子。掌握它可以让你构建自定义转换器、虚拟模块、开发服务器中间件和 HMR 逻辑,无需脱离 Vite 生态。

插件解剖:钩子与生命周期

Vite 插件是一个普通对象(或返回该对象的工厂函数),包含 name 属性和钩子方法。钩子在构建/开发流水线的特定节点触发。

// Minimal Vite plugin structure
// A plugin is just an object with a name and hooks
export default function myPlugin(options = {}) {
  return {
    name: 'vite-plugin-my-plugin',   // required, shown in warnings/errors
    enforce: 'pre',                   // 'pre' | 'post' | undefined

    // Called once when Vite starts
    configResolved(config) {
      console.log('Vite mode:', config.mode);
    },

    // Modify Vite config before it resolves
    config(config, { command }) {
      if (command === 'build') {
        return { build: { sourcemap: true } };
      }
    },

    // Transform file contents
    transform(code, id) {
      if (!id.endsWith('.vue')) return null; // skip non-Vue files
      return { code: transformedCode, map: sourceMap };
    },

    // Resolve module imports to file paths
    resolveId(source, importer) {
      if (source === 'virtual:my-module') {
        return 'virtual:my-module';  // '' prefix = virtual module
      }
    },

    // Load virtual module content
    load(id) {
      if (id === 'virtual:my-module') {
        return 'export const message = "Hello from virtual module!"';
      }
    }
  };
}

transform 钩子:修改源文件

transform 钩子是最常用的钩子。它接收原始源码和模块 ID(文件路径),返回新代码和可选的 source map。

// Real-world transform: inject build metadata into source files
// Similar to how vite-plugin-inspect or vite-plugin-svgr work
import { readFileSync } from 'fs';
import { createHash } from 'crypto';

export default function buildMetaPlugin() {
  let config;
  const buildTime = new Date().toISOString();

  return {
    name: 'vite-plugin-build-meta',
    enforce: 'post',

    configResolved(resolvedConfig) {
      config = resolvedConfig;
    },

    transform(code, id) {
      // Only process files that import the meta module
      if (!code.includes('__BUILD_META__')) return null;

      const hash = createHash('sha256')
        .update(code)
        .digest('hex')
        .slice(0, 8);

      const meta = JSON.stringify({
        buildTime,
        mode: config.mode,
        hash,
        file: id.replace(config.root, ''),
      });

      const transformed = code.replace(
        /__BUILD_META__/g,
        meta
      );

      return {
        code: transformed,
        // Return null map to inherit original sourcemap
        map: null,
      };
    },
  };
}

// Usage in your app:
// import meta from 'virtual:build-meta';
// console.log(meta.buildTime);

resolveId 钩子:自定义模块解析

resolveId 钩子可以拦截导入路径,并将其重定向到不同的文件或虚拟模块。返回 null 则交由其他解析器处理。

// Advanced resolveId: path aliases + conditional resolution
// Replaces tsconfig paths in runtime (Vite handles build, but not always HMR)
import path from 'path';
import fs from 'fs';

export default function aliasPlugin(aliases = {}) {
  // aliases = { '@': './src', '~utils': './src/utils' }
  const resolvedAliases = Object.fromEntries(
    Object.entries(aliases).map(([key, value]) => [
      key,
      path.resolve(process.cwd(), value),
    ])
  );

  return {
    name: 'vite-plugin-advanced-alias',

    resolveId(source, importer, options) {
      for (const [alias, target] of Object.entries(resolvedAliases)) {
        if (source === alias || source.startsWith(alias + '/')) {
          const resolved = source.replace(alias, target);

          // Try multiple extensions
          for (const ext of ['', '.ts', '.tsx', '.js', '/index.ts']) {
            const candidate = resolved + ext;
            if (fs.existsSync(candidate)) {
              return candidate;
            }
          }
        }
      }

      // Return null to let other resolvers handle it
      return null;
    },
  };
}

// vite.config.ts usage:
// plugins: [aliasPlugin({ '@': './src', '~hooks': './src/hooks' })]

configureServer:开发服务器中间件

configureServer 钩子提供对 Vite 开发服务器的完整访问权限——Connect 中间件、WebSocket、文件监听器以及模块图。

// Custom dev server middleware: mock API + WebSocket HMR notifications
export default function devApiPlugin(routes = {}) {
  return {
    name: 'vite-plugin-dev-api',
    apply: 'serve',   // Only active in dev server, not build

    configureServer(server) {
      // Add middleware BEFORE Vite's internal middlewares
      server.middlewares.use('/api', (req, res, next) => {
        const handler = routes[req.url];
        if (!handler) return next();

        res.setHeader('Content-Type', 'application/json');
        res.setHeader('Access-Control-Allow-Origin', '*');

        // Support async handlers
        Promise.resolve(handler(req))
          .then(data => res.end(JSON.stringify(data)))
          .catch(err => {
            res.statusCode = 500;
            res.end(JSON.stringify({ error: err.message }));
          });
      });

      // Hook into Vite's WebSocket for custom HMR events
      server.ws.on('custom:ping', (data, client) => {
        client.send('custom:pong', { timestamp: Date.now() });
      });

      // Send custom event when a file changes
      server.watcher.on('change', (file) => {
        if (file.endsWith('.mock.ts')) {
          server.ws.send({ type: 'custom', event: 'mock-updated', data: { file } });
        }
      });
    },
  };
}

// Usage:
// plugins: [devApiPlugin({
//   '/users': () => [{ id: 1, name: 'Alice' }],
//   '/auth/me': () => ({ id: 1, role: 'admin' }),
// })]

HMR:热模块替换集成

插件可以注入 HMR 边界代码并处理自定义更新事件,实现热重载时的模块级状态保留。

// Plugin with HMR (Hot Module Replacement) support
// Allows modules to opt-in to custom update logic
export default function statePlugin() {
  return {
    name: 'vite-plugin-state-preserve',

    // Inject HMR handling code into every JS/TS file
    transform(code, id) {
      if (!/.(js|ts|jsx|tsx)$/.test(id)) return null;
      if (id.includes('node_modules')) return null;

      // Append HMR boundary acceptance
      const hmrCode = `
if (import.meta.hot) {
  import.meta.hot.accept((newModule) => {
    // Module updated - newModule has the fresh exports
    console.log('[HMR] Module updated:', import.meta.url);
  });

  import.meta.hot.dispose((data) => {
    // Clean up side effects before module is replaced
    // data is passed to the next module version
    data.savedState = window.__APP_STATE__;
  });

  // Receive data from old module instance
  if (import.meta.hot.data.savedState) {
    window.__APP_STATE__ = import.meta.hot.data.savedState;
  }
}
`;

      return { code: code + hmrCode, map: null };
    },

    handleHotUpdate({ file, server, modules }) {
      // Custom logic: invalidate dependent modules when a .schema file changes
      if (file.endsWith('.schema.json')) {
        const affectedModules = server.moduleGraph.getModulesByFile(file);
        return [...(affectedModules || []), ...modules];
      }
    },
  };
}

完整插件配置示例

一个生产级 vite.config.ts,整合了多个插件、手动代码分割和服务器代理配置。

// Complete vite.config.ts with multiple plugins
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';
import myPlugin from './plugins/my-plugin';

export default defineConfig(({ command, mode }) => ({
  plugins: [
    react(),
    myPlugin({ debug: mode === 'development' }),

    // Bundle analyzer - only in build mode
    command === 'build' && visualizer({
      filename: 'dist/stats.html',
      open: true,
      gzipSize: true,
    }),
  ].filter(Boolean),

  resolve: {
    alias: { '@': '/src' },
    extensions: ['.tsx', '.ts', '.jsx', '.js'],
  },

  build: {
    rollupOptions: {
      output: {
        // Manual chunk splitting for better caching
        manualChunks: {
          vendor: ['react', 'react-dom'],
          router: ['react-router-dom'],
          query: ['@tanstack/react-query'],
        },
      },
    },
  },

  server: {
    port: 5173,
    proxy: {
      '/api': { target: 'http://localhost:3000', changeOrigin: true },
    },
    hmr: {
      overlay: true,    // Show errors as overlay
      port: 24678,
    },
  },
}));

Vite 钩子速查

HookWhen CalledTypical UseApplies
configBefore config resolveModify Vite configBoth
configResolvedAfter config resolveRead final configBoth
configureServerBefore server startsAdd middlewareDev only
transformOn each file requestModify source codeBoth
resolveIdOn import resolutionRedirect importsBoth
loadAfter resolveIdSupply file contentBoth
handleHotUpdateOn file change (HMR)Custom HMR logicDev only
buildStartBuild beginsInitialize stateBuild only
generateBundleBundle generatedModify output assetsBuild only

常见问题

何时使用 enforce: "pre" 或 "post"?

当插件必须在其他转换器(如编译器)之前运行时,使用 "pre";当插件需要完整转换后的输出时,使用 "post"。省略 enforce 则插件位于 pre 与 post 插件之间。

如何在 Vite 中创建虚拟模块?

在 resolveId 中返回以 \0(空字节)为前缀的模块 ID,然后在 load 钩子中返回其内容。\0 前缀告知 Rollup 将其视为虚拟模块,不在磁盘上查找。

Vite 插件可以同时在开发和构建模式下工作吗?

可以。使用 apply 属性:"serve" 仅用于开发,"build" 仅用于构建,省略则在两种模式下均运行。也可以使用函数形式:apply(config, { command }) { return command === "build"; }。

如何调试 Vite 插件?

在环境变量中添加 DEBUG=vite:* 可查看 Vite 内部日志。在钩子中使用 this.warn() 和 this.error() 输出插件特定信息。vite-plugin-inspect 插件可在浏览器中可视化插件转换过程。

相关工具

𝕏 Twitterin LinkedIn
这篇文章有帮助吗?

保持更新

获取每周开发技巧和新工具通知。

无垃圾邮件,随时退订。

试试这些相关工具

{ }JSON FormatterB→Base64 Encode Online

相关文章

Bun 包管理器:2026年最快的JavaScript运行时与包管理器

2026年Bun完整指南:安装、工作区管理、脚本,以及为何比npm/yarn/pnpm更快。基准测试、迁移指南与实际使用。

Monorepo工具2026:Turborepo vs Nx vs Lerna vs pnpm Workspaces对比

2026年monorepo工具完整对比:Turborepo、Nx、Lerna与pnpm workspaces,选择适合团队的工具。

Webpack vs Vite 2026:该选哪个构建工具?

2026年 Webpack 与 Vite 全面对比:性能基准、生态支持、迁移策略。