esbuild 插件系统 #

插件概述 #

esbuild 插件系统允许你自定义构建过程,处理特殊文件类型,添加自定义转换逻辑等。

text
┌─────────────────────────────────────────────────────────────┐
│                    esbuild 插件系统                          │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ┌──────────────────────────────────────────────────────┐   │
│  │                    构建流程                           │   │
│  │  ┌─────────┐  ┌─────────┐  ┌─────────┐  ┌─────────┐ │   │
│  │  │  解析   │->│  加载   │->│  转换   │->│  输出   │ │   │
│  │  └─────────┘  └─────────┘  └─────────┘  └─────────┘ │   │
│  │       ↑            ↑            ↑                     │   │
│  │       │            │            │                     │   │
│  │  ┌────┴────┐  ┌────┴────┐  ┌────┴────┐              │   │
│  │  │ onResolve│  │  onLoad │  │ onEnd   │              │   │
│  │  └─────────┘  └─────────┘  └─────────┘              │   │
│  └──────────────────────────────────────────────────────┘   │
│                                                              │
└─────────────────────────────────────────────────────────────┘

插件基础结构 #

基本格式 #

javascript
const myPlugin = {
  name: 'my-plugin',
  setup(build) {
    // 插件逻辑
  },
};

使用插件 #

javascript
const esbuild = require('esbuild');

esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/bundle.js',
  plugins: [myPlugin],
});

完整示例 #

javascript
const esbuild = require('esbuild');

const myPlugin = {
  name: 'my-plugin',
  setup(build) {
    console.log('Plugin setup started');

    build.onEnd((result) => {
      console.log('Build completed with', result.errors.length, 'errors');
    });
  },
};

esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/bundle.js',
  plugins: [myPlugin],
});

插件钩子 #

onResolve - 模块解析 #

自定义模块路径解析:

javascript
const pathPlugin = {
  name: 'path-alias',
  setup(build) {
    build.onResolve({ filter: /^@\// }, (args) => {
      return {
        path: args.path.replace('@', './src'),
        namespace: 'file',
      };
    });
  },
};

onResolve 参数 #

javascript
build.onResolve(
  {
    filter: /\.txt$/,      // 正则匹配
    namespace: 'file',     // 命名空间过滤
  },
  (args) => {
    // args 包含:
    // - path: 导入路径
    // - importer: 导入者路径
    // - namespace: 命名空间
    // - resolveDir: 解析目录
    // - kind: 导入类型 (import-statement, require-call, etc.)

    return {
      path: 'resolved-path',   // 解析后的路径
      namespace: 'custom',     // 命名空间
      external: false,         // 是否外部模块
      sideEffects: true,       // 是否有副作用
      pluginData: {},          // 传递给 onLoad 的数据
    };
  }
);

示例:URL 导入 #

javascript
const urlImportPlugin = {
  name: 'url-import',
  setup(build) {
    build.onResolve({ filter: /^https?:\/\// }, (args) => ({
      path: args.path,
      namespace: 'http-url',
    }));

    build.onLoad({ filter: /.*/, namespace: 'http-url' }, async (args) => {
      const response = await fetch(args.path);
      const contents = await response.text();
      return { contents, loader: 'js' };
    });
  },
};

onLoad - 文件加载 #

自定义文件加载和转换:

javascript
const txtPlugin = {
  name: 'txt-loader',
  setup(build) {
    build.onLoad({ filter: /\.txt$/ }, async (args) => {
      const fs = require('fs').promises;
      const contents = await fs.readFile(args.path, 'utf8');
      return {
        contents: `export default ${JSON.stringify(contents)}`,
        loader: 'js',
      };
    });
  },
};

onLoad 参数 #

javascript
build.onLoad(
  {
    filter: /\.custom$/,    // 正则匹配
    namespace: 'file',      // 命名空间过滤
  },
  async (args) => {
    // args 包含:
    // - path: 文件路径
    // - namespace: 命名空间
    // - suffix: 查询字符串
    // - pluginData: onResolve 传递的数据

    return {
      contents: 'export default "hello"',  // 文件内容
      loader: 'js',                        // 加载器类型
      resolveDir: '/path/to/dir',          // 解析目录
      pluginData: {},                      // 传递给后续钩子的数据
    };
  }
);

示例:YAML 加载器 #

javascript
const yaml = require('yaml');

const yamlPlugin = {
  name: 'yaml-loader',
  setup(build) {
    build.onLoad({ filter: /\.(yaml|yml)$/ }, async (args) => {
      const fs = require('fs').promises;
      const source = await fs.readFile(args.path, 'utf8');
      const data = yaml.parse(source);
      return {
        contents: `export default ${JSON.stringify(data)}`,
        loader: 'js',
      };
    });
  },
};

onEnd - 构建结束 #

构建完成后执行:

javascript
const notifyPlugin = {
  name: 'notify',
  setup(build) {
    build.onEnd((result) => {
      if (result.errors.length > 0) {
        console.error('❌ Build failed with errors');
      } else {
        console.log('✅ Build completed successfully');
        console.log(`   Output files: ${result.metafile?.outputs ? Object.keys(result.metafile.outputs).length : 0}`);
      }
    });
  },
};

onStart - 构建开始 #

构建开始前执行:

javascript
const timerPlugin = {
  name: 'timer',
  setup(build) {
    let startTime;

    build.onStart(() => {
      startTime = Date.now();
      console.log('🚀 Build started...');
    });

    build.onEnd(() => {
      const duration = Date.now() - startTime;
      console.log(`⏱️  Build completed in ${duration}ms`);
    });
  },
};

onDispose - 清理资源 #

插件清理时执行:

javascript
const cleanupPlugin = {
  name: 'cleanup',
  setup(build) {
    const watcher = startWatcher();

    build.onDispose(() => {
      watcher.close();
      console.log('Resources cleaned up');
    });
  },
};

实用插件示例 #

1. 环境变量插件 #

javascript
const envPlugin = {
  name: 'env',
  setup(build) {
    const env = process.env;

    build.onResolve({ filter: /^env$/ }, (args) => ({
      path: args.path,
      namespace: 'env-ns',
    }));

    build.onLoad({ filter: /.*/, namespace: 'env-ns' }, () => ({
      contents: JSON.stringify(env),
      loader: 'json',
    }));
  },
};

// 使用
// import env from 'env';
// console.log(env.NODE_ENV);

2. HTML 模板插件 #

javascript
const htmlPlugin = {
  name: 'html-template',
  setup(build) {
    build.onLoad({ filter: /\.html$/ }, async (args) => {
      const fs = require('fs').promises;
      const html = await fs.readFile(args.path, 'utf8');

      const code = `
        const template = ${JSON.stringify(html)};
        export default template;
        export function render(data) {
          return template.replace(/\{\{(\w+)\}\}/g, (_, key) => data[key] || '');
        }
      `;

      return { contents: code, loader: 'js' };
    });
  },
};

3. Markdown 插件 #

javascript
const markdownPlugin = {
  name: 'markdown',
  setup(build) {
    build.onLoad({ filter: /\.md$/ }, async (args) => {
      const fs = require('fs').promises;
      const markdown = await fs.readFile(args.path, 'utf8');

      const html = markdownToHtml(markdown);

      return {
        contents: `
          export const html = ${JSON.stringify(html)};
          export const raw = ${JSON.stringify(markdown)};
          export default html;
        `,
        loader: 'js',
      };
    });
  },
};

function markdownToHtml(md) {
  return md
    .replace(/^### (.*$)/gm, '<h3>$1</h3>')
    .replace(/^## (.*$)/gm, '<h2>$1</h2>')
    .replace(/^# (.*$)/gm, '<h1>$1</h1>')
    .replace(/\*\*(.*)\*\*/g, '<strong>$1</strong>')
    .replace(/\*(.*)\*/g, '<em>$1</em>')
    .replace(/\n/g, '<br>');
}

4. GraphQL 插件 #

javascript
const graphqlPlugin = {
  name: 'graphql',
  setup(build) {
    build.onLoad({ filter: /\.(graphql|gql)$/ }, async (args) => {
      const fs = require('fs').promises;
      const query = await fs.readFile(args.path, 'utf8');

      return {
        contents: `
          export const query = ${JSON.stringify(query)};
          export default query;
        `,
        loader: 'js',
      };
    });
  },
};

5. 虚拟模块插件 #

javascript
const virtualModulePlugin = {
  name: 'virtual',
  setup(build) {
    const virtualModules = {
      'virtual:config': `
        export const apiUrl = 'https://api.example.com';
        export const version = '1.0.0';
      `,
      'virtual:features': `
        export const features = ['auth', 'dashboard', 'settings'];
        export default features;
      `,
    };

    build.onResolve({ filter: /^virtual:/ }, (args) => ({
      path: args.path,
      namespace: 'virtual',
    }));

    build.onLoad({ filter: /.*/, namespace: 'virtual' }, (args) => ({
      contents: virtualModules[args.path],
      loader: 'js',
    }));
  },
};

// 使用
// import { apiUrl } from 'virtual:config';
// import features from 'virtual:features';

6. 复制文件插件 #

javascript
const copyPlugin = (options = {}) => {
  const { from, to } = options;

  return {
    name: 'copy',
    setup(build) {
      const fs = require('fs').promises;
      const path = require('path');

      build.onEnd(async () => {
        await fs.mkdir(to, { recursive: true });
        const files = await fs.readdir(from);

        for (const file of files) {
          const src = path.join(from, file);
          const dest = path.join(to, file);
          await fs.copyFile(src, dest);
        }

        console.log(`Copied ${files.length} files to ${to}`);
      });
    },
  };
};

// 使用
esbuild.build({
  plugins: [
    copyPlugin({ from: 'public', to: 'dist/public' }),
  ],
});

7. 代码替换插件 #

javascript
const replacePlugin = (replacements = {}) => {
  return {
    name: 'replace',
    setup(build) {
      build.onLoad({ filter: /\.(js|ts|jsx|tsx)$/ }, async (args) => {
        const fs = require('fs').promises;
        let contents = await fs.readFile(args.path, 'utf8');

        for (const [from, to] of Object.entries(replacements)) {
          contents = contents.replace(new RegExp(from, 'g'), to);
        }

        return { contents, loader: args.path.endsWith('.tsx') ? 'tsx' : args.path.endsWith('.ts') ? 'ts' : 'js' };
      });
    },
  };
};

// 使用
esbuild.build({
  plugins: [
    replacePlugin({
      '__VERSION__': '1.0.0',
      '__BUILD_TIME__': new Date().toISOString(),
    }),
  ],
});

8. 压缩报告插件 #

javascript
const sizeReportPlugin = {
  name: 'size-report',
  setup(build) {
    build.onEnd(async (result) => {
      if (!result.metafile) return;

      const outputs = result.metafile.outputs;
      console.log('\n📦 Build Size Report:');
      console.log('─'.repeat(50));

      for (const [file, info] of Object.entries(outputs)) {
        const size = info.bytes;
        const sizeKB = (size / 1024).toFixed(2);
        console.log(`  ${file}: ${sizeKB} KB`);
      }

      console.log('─'.repeat(50));
      const total = Object.values(outputs).reduce((sum, o) => sum + o.bytes, 0);
      console.log(`  Total: ${(total / 1024).toFixed(2)} KB\n`);
    });
  },
};

社区插件 #

esbuild-plugin-copy #

复制文件和目录:

javascript
const { copy } = require('esbuild-plugin-copy');

esbuild.build({
  plugins: [
    copy({
      assets: [
        { from: './public/images', to: './dist/images' },
        { from: './public/fonts', to: './dist/fonts' },
      ],
    }),
  ],
});

esbuild-plugin-pnpm #

处理 pnpm 符号链接:

javascript
const { pnpmPlugin } = require('esbuild-plugin-pnpm');

esbuild.build({
  plugins: [pnpmPlugin()],
});

esbuild-plugin-svgr #

将 SVG 转换为 React 组件:

javascript
const svgrPlugin = require('esbuild-plugin-svgr');

esbuild.build({
  plugins: [svgrPlugin()],
});

// 使用
// import Logo from './logo.svg';
// <Logo />

esbuild-plugin-less #

处理 Less 文件:

javascript
const { lessLoader } = require('esbuild-plugin-less');

esbuild.build({
  plugins: [lessLoader()],
});

esbuild-plugin-sass #

处理 Sass/SCSS 文件:

javascript
const { sassPlugin } = require('esbuild-sass-plugin');

esbuild.build({
  plugins: [sassPlugin()],
});

esbuild-plugin-postcss #

使用 PostCSS 处理 CSS:

javascript
const postcss = require('esbuild-plugin-postcss');

esbuild.build({
  plugins: [
    postcss({
      plugins: [
        require('autoprefixer'),
        require('postcss-preset-env'),
      ],
    }),
  ],
});

esbuild-plugin-style-module #

CSS Modules 支持:

javascript
const styleModule = require('esbuild-plugin-style-module');

esbuild.build({
  plugins: [styleModule()],
});

高级插件技巧 #

链式插件 #

javascript
function createChainPlugin(plugins) {
  return {
    name: 'chain-plugin',
    setup(build) {
      for (const plugin of plugins) {
        plugin.setup(build);
      }
    },
  };
}

esbuild.build({
  plugins: [
    createChainPlugin([
      envPlugin,
      yamlPlugin,
      markdownPlugin,
    ]),
  ],
});

插件配置 #

javascript
function createConfigurablePlugin(options = {}) {
  const {
    include = /\.js$/,
    exclude = /node_modules/,
    transform = (code) => code,
  } = options;

  return {
    name: 'configurable-plugin',
    setup(build) {
      build.onLoad({ filter: include }, async (args) => {
        if (exclude.test(args.path)) return null;

        const fs = require('fs').promises;
        let contents = await fs.readFile(args.path, 'utf8');
        contents = await transform(contents, args.path);

        return { contents, loader: 'js' };
      });
    },
  };
}

// 使用
esbuild.build({
  plugins: [
    createConfigurablePlugin({
      include: /\.js$/,
      exclude: /node_modules|vendor/,
      transform: async (code, path) => {
        console.log(`Transforming ${path}`);
        return code.replace(/__VERSION__/g, '1.0.0');
      },
    }),
  ],
});

异步插件 #

javascript
const asyncPlugin = {
  name: 'async-plugin',
  setup(build) {
    build.onLoad({ filter: /\.async$/ }, async (args) => {
      const data = await fetchData(args.path);
      return {
        contents: `export default ${JSON.stringify(data)}`,
        loader: 'js',
      };
    });
  },
};

async function fetchData(path) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ path, loaded: true });
    }, 100);
  });
}

错误处理 #

javascript
const errorHandlingPlugin = {
  name: 'error-handling',
  setup(build) {
    build.onLoad({ filter: /\.custom$/ }, async (args) => {
      try {
        const contents = await processFile(args.path);
        return { contents, loader: 'js' };
      } catch (error) {
        return {
          errors: [
            {
              text: error.message,
              location: {
                file: args.path,
                line: error.line,
                column: error.column,
              },
            },
          ],
        };
      }
    });
  },
};

警告处理 #

javascript
const warningPlugin = {
  name: 'warning',
  setup(build) {
    build.onLoad({ filter: /\.js$/ }, async (args) => {
      const fs = require('fs').promises;
      const contents = await fs.readFile(args.path, 'utf8');

      const warnings = [];

      if (contents.includes('console.log')) {
        warnings.push({
          text: 'console.log found in production code',
          location: { file: args.path },
        });
      }

      return {
        contents,
        loader: 'js',
        warnings,
      };
    });
  },
};

插件调试 #

日志输出 #

javascript
const debugPlugin = {
  name: 'debug',
  setup(build) {
    build.onStart(() => {
      console.log('[DEBUG] Build started');
    });

    build.onResolve({ filter: /.*/ }, (args) => {
      console.log(`[DEBUG] Resolving: ${args.path}`);
      return null;
    });

    build.onLoad({ filter: /.*/ }, (args) => {
      console.log(`[DEBUG] Loading: ${args.path}`);
      return null;
    });

    build.onEnd((result) => {
      console.log(`[DEBUG] Build ended with ${result.errors.length} errors`);
    });
  },
};

插件顺序 #

javascript
esbuild.build({
  plugins: [
    // 1. 首先处理解析
    pathAliasPlugin,

    // 2. 然后加载文件
    yamlPlugin,
    markdownPlugin,

    // 3. 最后转换代码
    transformPlugin,

    // 4. 构建结束处理
    reportPlugin,
  ],
});

下一步 #

现在你已经掌握了 esbuild 插件系统,接下来学习 高级特性 了解代码分割、Tree-shaking 等高级功能!

最后更新:2026-03-28