返回首页

是否写过 Plugin?简单描述一下编写 Plugin 的思路?

问题解析

Plugin 是 Webpack 的支柱功能,用于在构建过程中执行更广泛的任务。与 Loader 专注于文件转换不同,Plugin 可以访问整个构建生命周期,实现更强大的功能。

核心概念

Plugin 的工作原理

Webpack 构建流程:
    |
    v
初始化参数 -> 开始编译 -> 确定入口 -> 编译模块 -> 完成编译
    |                                              |
    v                                              v
输出资源 <---------------------------------- 输出完成
    |
    v
构建完成

Plugin 在关键节点监听事件,执行自定义逻辑

核心对象

对象 作用 生命周期
Compiler Webpack 实例,暴露整个生命周期钩子 全局唯一
Compilation 当前编译过程,暴露模块和依赖相关事件 每次构建创建

Tapable 事件流机制

Webpack 使用 Tapable 库实现事件流,保证插件有序执行:

钩子类型 特点 使用场景
SyncHook 同步串行 简单同步操作
SyncBailHook 同步串行,可中断 需要提前返回
AsyncParallelHook 异步并行 并行执行任务
AsyncSeriesHook 异步串行 按顺序执行异步任务

详细解答

最简单的 Plugin

// my-plugin.js
class MyPlugin {
  constructor(options) {
    this.options = options || {};
  }

  // 必须定义 apply 方法
  apply(compiler) {
    // 在编译完成时触发
    compiler.hooks.done.tap('MyPlugin', (stats) => {
      console.log('编译完成!');
      console.log('统计信息:', stats.toJson());
    });
  }
}

module.exports = MyPlugin;

使用 Plugin

// webpack.config.js
const MyPlugin = require('./my-plugin');

module.exports = {
  plugins: [
    new MyPlugin({
      message: 'Hello Webpack!'
    })
  ]
};

访问 Compilation

class MyPlugin {
  apply(compiler) {
    // 在编译创建时触发
    compiler.hooks.compilation.tap('MyPlugin', (compilation) => {
      console.log('Compilation 创建');

      // 访问 compilation 的钩子
      compilation.hooks.optimize.tap('MyPlugin', () => {
        console.log('资源优化中...');
      });
    });
  }
}

异步事件处理

class AsyncPlugin {
  apply(compiler) {
    // 异步串行钩子
    compiler.hooks.emit.tapAsync('AsyncPlugin', (compilation, callback) => {
      console.log('开始输出资源...');

      setTimeout(() => {
        console.log('异步操作完成');
        // 必须调用 callback 通知 Webpack 继续
        callback();
      }, 1000);
    });

    // 使用 Promise 的异步钩子
    compiler.hooks.emit.tapPromise('AsyncPlugin', (compilation) => {
      return new Promise((resolve) => {
        setTimeout(() => {
          console.log('Promise 异步完成');
          resolve();
        }, 1000);
      });
    });
  }
}

深入理解

Compiler 生命周期钩子

class LifecyclePlugin {
  apply(compiler) {
    // 1. 环境初始化完成
    compiler.hooks.environment.tap('LifecyclePlugin', () => {
      console.log('1. environment');
    });

    // 2. 编译器准备就绪
    compiler.hooks.afterEnvironment.tap('LifecyclePlugin', () => {
      console.log('2. afterEnvironment');
    });

    // 3. 开始编译(创建 Compilation)
    compiler.hooks.beforeCompile.tap('LifecyclePlugin', (params) => {
      console.log('3. beforeCompile');
    });

    // 4. 编译中
    compiler.hooks.compile.tap('LifecyclePlugin', (params) => {
      console.log('4. compile');
    });

    // 5. 创建 Compilation 后
    compiler.hooks.thisCompilation.tap('LifecyclePlugin', (compilation) => {
      console.log('5. thisCompilation');
    });

    // 6. 创建 Compilation(每次构建都会触发)
    compiler.hooks.compilation.tap('LifecyclePlugin', (compilation) => {
      console.log('6. compilation');
    });

    // 7. make 阶段:分析模块依赖
    compiler.hooks.make.tapAsync('LifecyclePlugin', (compilation, callback) => {
      console.log('7. make');
      callback();
    });

    // 8. 模块构建完成
    compiler.hooks.afterCompile.tap('LifecyclePlugin', (compilation) => {
      console.log('8. afterCompile');
    });

    // 9. 输出资源前
    compiler.hooks.emit.tapAsync('LifecyclePlugin', (compilation, callback) => {
      console.log('9. emit');
      callback();
    });

    // 10. 输出资源后
    compiler.hooks.afterEmit.tap('LifecyclePlugin', (compilation) => {
      console.log('10. afterEmit');
    });

    // 11. 编译完成
    compiler.hooks.done.tap('LifecyclePlugin', (stats) => {
      console.log('11. done');
    });

    // 12. 编译失败
    compiler.hooks.failed.tap('LifecyclePlugin', (error) => {
      console.log('12. failed:', error);
    });
  }
}

Compilation 关键钩子

class CompilationPlugin {
  apply(compiler) {
    compiler.hooks.compilation.tap('CompilationPlugin', (compilation) => {
      // 1. 模块构建前
      compilation.hooks.buildModule.tap('CompilationPlugin', (module) => {
        console.log('构建模块:', module.identifier());
      });

      // 2. 模块构建后
      compilation.hooks.succeedModule.tap('CompilationPlugin', (module) => {
        console.log('模块构建成功:', module.identifier());
      });

      // 3. 密封阶段:不再接收新模块
      compilation.hooks.seal.tap('CompilationPlugin', () => {
        console.log('开始密封');
      });

      // 4. 优化阶段
      compilation.hooks.optimize.tap('CompilationPlugin', () => {
        console.log('优化资源');
      });

      // 5. 优化 chunk
      compilation.hooks.optimizeChunks.tap('CompilationPlugin', (chunks) => {
        console.log('优化 chunks:', chunks.size);
      });

      // 6. 生成资源
      compilation.hooks.assets.tap('CompilationPlugin', (assets) => {
        console.log('生成资源:', Object.keys(assets));
      });

      // 7. 生成 hash
      compilation.hooks.chunkHash.tap('CompilationPlugin', (chunk, chunkHash) => {
        console.log('生成 chunk hash:', chunk.name);
      });
    });
  }
}

操作输出资源

const { RawSource } = require('webpack-sources');

class GenerateFilePlugin {
  constructor(options) {
    this.options = options || {};
  }

  apply(compiler) {
    compiler.hooks.emit.tapAsync('GenerateFilePlugin', (compilation, callback) => {
      // 获取输出资源
      const assets = compilation.assets;

      // 生成文件列表
      const fileList = Object.keys(assets)
        .map(filename => {
          const size = assets[filename].size();
          return `- ${filename}: ${size} bytes`;
        })
        .join('\n');

      // 添加新资源
      const content = `
# 构建文件列表

生成时间: ${new Date().toLocaleString()}

${fileList}
      `.trim();

      // 使用 webpack-sources 创建资源
      compilation.assets['file-list.md'] = new RawSource(content);

      callback();
    });
  }
}

最佳实践

实用的 Plugin 示例

1. 构建时间统计插件

class BuildTimePlugin {
  constructor(options = {}) {
    this.options = {
      name: 'BuildTimePlugin',
      ...options
    };
    this.startTime = null;
  }

  apply(compiler) {
    // 记录开始时间
    compiler.hooks.compile.tap(this.options.name, () => {
      this.startTime = Date.now();
      console.log('开始编译...');
    });

    // 计算耗时
    compiler.hooks.done.tap(this.options.name, (stats) => {
      const endTime = Date.now();
      const duration = (endTime - this.startTime) / 1000;

      console.log(`编译完成,耗时: ${duration.toFixed(2)}s`);

      if (stats.hasErrors()) {
        console.error('编译失败!');
      } else if (stats.hasWarnings()) {
        console.warn('编译成功,但有警告');
      } else {
        console.log('编译成功!');
      }
    });
  }
}

module.exports = BuildTimePlugin;

2. 复制文件插件

const fs = require('fs');
const path = require('path');

class CopyFilePlugin {
  constructor(options) {
    this.options = options || {};
  }

  apply(compiler) {
    const { from, to } = this.options;

    compiler.hooks.afterEmit.tapAsync('CopyFilePlugin', (compilation, callback) => {
      const outputPath = compilation.outputOptions.path;
      const fromPath = path.resolve(from);
      const toPath = path.resolve(outputPath, to);

      fs.copyFile(fromPath, toPath, (err) => {
        if (err) {
          compilation.errors.push(err);
        } else {
          console.log(`已复制: ${from} -> ${to}`);
        }
        callback();
      });
    });
  }
}

module.exports = CopyFilePlugin;

3. 清理控制台插件

class ClearConsolePlugin {
  constructor(options = {}) {
    this.options = {
      clearOnStart: true,
      ...options
    };
  }

  apply(compiler) {
    if (this.options.clearOnStart) {
      compiler.hooks.compile.tap('ClearConsolePlugin', () => {
        // 清理控制台
        process.stdout.write(
          process.platform === 'win32' ? '\x1B[2J\x1B[0f' : '\x1B[2J\x1B[3J\x1B[H'
        );
      });
    }
  }
}

module.exports = ClearConsolePlugin;

4. 资源大小分析插件

class BundleAnalyzerPlugin {
  constructor(options = {}) {
    this.options = {
      maxSize: 244 * 1024, // 244KB
      ...options
    };
  }

  apply(compiler) {
    compiler.hooks.emit.tap('BundleAnalyzerPlugin', (compilation) => {
      const assets = compilation.assets;
      const warnings = [];

      console.log('\n===== 资源大小分析 =====\n');

      Object.keys(assets).forEach(filename => {
        const asset = assets[filename];
        const size = asset.size();
        const sizeInKB = (size / 1024).toFixed(2);
        const isOversized = size > this.options.maxSize;

        const status = isOversized ? '⚠️ 过大' : '✓';
        console.log(`${status} ${filename}: ${sizeInKB} KB`);

        if (isOversized) {
          warnings.push(`${filename} 超过 ${(this.options.maxSize / 1024).toFixed(0)}KB`);
        }
      });

      console.log('\n=======================\n');

      // 添加警告
      warnings.forEach(warning => {
        compilation.warnings.push(new Error(warning));
      });
    });
  }
}

module.exports = BundleAnalyzerPlugin;

完整的 Plugin 配置

// webpack.config.js
const BuildTimePlugin = require('./plugins/build-time-plugin');
const BundleAnalyzerPlugin = require('./plugins/bundle-analyzer-plugin');
const ClearConsolePlugin = require('./plugins/clear-console-plugin');

module.exports = {
  plugins: [
    // 清理控制台
    new ClearConsolePlugin(),

    // 统计构建时间
    new BuildTimePlugin({
      name: 'MyBuildTimePlugin'
    }),

    // 分析资源大小
    new BundleAnalyzerPlugin({
      maxSize: 200 * 1024  // 200KB
    })
  ]
};

开发 Plugin 的注意事项

class BestPracticePlugin {
  constructor(options) {
    // 1. 合并默认选项
    this.options = {
      verbose: false,
      ...options
    };
  }

  apply(compiler) {
    const pluginName = 'BestPracticePlugin';

    // 2. 使用具名函数,便于调试
    compiler.hooks.compile.tap(pluginName, () => {
      if (this.options.verbose) {
        console.log('开始编译');
      }
    });

    // 3. 异步钩子正确处理回调
    compiler.hooks.emit.tapAsync(pluginName, (compilation, callback) => {
      try {
        // 执行操作
        this.processAssets(compilation);

        // 成功时调用 callback,不传错误
        callback();
      } catch (error) {
        // 错误时传递错误对象
        callback(error);
      }
    });

    // 4. 或使用 tapPromise
    compiler.hooks.emit.tapPromise(pluginName, (compilation) => {
      return this.asyncProcess(compilation);
    });
  }

  processAssets(compilation) {
    // 处理资源
  }

  asyncProcess(compilation) {
    return new Promise((resolve, reject) => {
      // 异步操作
      setTimeout(() => {
        resolve();
      }, 100);
    });
  }
}

面试要点

  1. Plugin 与 Loader 的区别

    • Loader:文件转换,处理单个文件
    • Plugin:生命周期扩展,处理整个构建过程
  2. 核心概念

    • Compiler:Webpack 实例,全局唯一
    • Compilation:每次构建创建,包含模块信息
    • Tapable:事件流机制,保证插件有序执行
  3. 编写 Plugin 的基本结构

    class MyPlugin {
      apply(compiler) {
        compiler.hooks.done.tap('MyPlugin', (stats) => {
          // 插件逻辑
        });
      }
    }
    
  4. 异步事件处理

    • tapAsync:使用 callback
    • tapPromise:返回 Promise
  5. 常用钩子

    • compiler.hooks.emit:输出资源前
    • compiler.hooks.done:编译完成
    • compilation.hooks.optimize:优化阶段
  6. 注意事项

    • 必须定义 apply 方法
    • 异步事件需要调用回调或返回 Promise
    • 使用 webpack-sources 操作资源
    • compilercompilation 不是同一个引用