Skip to content

webpack-plugin工作原理

Webpack 插件的工作原理基于 Tapable 事件流系统(事件驱动的插件系统),允许开发者在 Webpack 构建流程的特定阶段插入自定义逻辑。

一、核心概念

  1. Tapable
    • Webpack 内部使用的事件发布 - 订阅库,提供多种钩子(Hook)类型。
    • 钩子类型包括:同步钩子(SyncHook)、异步串行钩子(AsyncSeriesHook)、异步并行钩子(AsyncParallelHook)等。
  2. 钩子(Hook)
    • 定义在 Webpack 关键流程节点的事件点,插件通过监听这些钩子来执行代码。
    • 例如:compiler.hooks.compile(编译开始前)、compilation.hooks.optimize(优化资源时)。
  3. Compiler 与 Compilation
    • Compiler:全局单例,包含 Webpack 环境的所有配置信息,生命周期贯穿整个构建过程。
    • Compilation:每次构建生成一个新实例,包含当前构建的模块、依赖和资源信息。

二、插件工作流程

  1. 注册阶段
    • 在 Webpack 配置中引入插件并实例化,插件通过 apply 方法注册到 Webpack。
js
// webpack.config.js
const MyPlugin = require('./MyPlugin');

module.exports = {
  plugins: [
    new MyPlugin(options)
  ]
};
  1. 监听钩子
    • 插件在 apply 方法中监听特定钩子,绑定回调函数。
js
class MyPlugin {
  constructor(options) {
    this.options = options;
  }

  apply(compiler) {
    // 监听编译开始钩子
    compiler.hooks.compile.tap('MyPlugin', (params) => {
      console.log('编译开始:', params);
    });

    // 监听资源生成完成钩子(异步)
    compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
      // 修改或添加资源
      compilation.assets['custom-asset.txt'] = {
        source: () => '这是自定义资源',
        size: () => 14
      };
      callback();
    });
  }
}
  1. 执行阶段
    • 当 Webpack 运行到注册的钩子时,插件的回调函数被调用。
    • 可以在回调中访问 Compilation 实例,修改资源、添加新资源、记录日志等。

三、关键钩子

  1. Compiler 钩子
    • compile:编译开始前触发。
    • compilation:创建新的 compilation 实例时触发。
    • emit:生成资源到输出目录前触发。
    • done:编译完成后触发。
  2. Compilation 钩子
    • buildModule:开始构建模块时触发。
    • normalModuleLoader:模块加载时触发(可修改 loader 配置)。
    • optimize:优化模块时触发。
    • chunkAsset:生成 chunk 资源时触发。

webpack常见的plugin

  1. htmlWebpackPlugin:用于创建html
  2. clearWebpackPlugin:用于清理旧的dist目录
  3. MiniCssExtraPlugin:用于提取css到单独文件
  4. definePlugin: 用于定义全局变量
  5. CopyWebpackPlugin:用于复制文件
  6. UglifyjsWebpackPlugin:用于压缩js
  7. moduleFedurationPlugin:模块联邦相关
  8. HotModuleReplacementPlugin:热更新

webpack 插件示例

找出js文件中的连续中文

js
class ChineseCharacterDetectorPlugin {
  constructor(options = {}) {
    this.options = {
      threshold: 2, // 连续中文字符的最小数量
      logLevel: 'warning', // 日志级别: 'info' | 'warning' | 'error'
      outputFile: null, // 输出报告文件路径
      ...options
    };
  }

  apply(compiler) {
    // 中文正则表达式
    const chineseRegex = /[\u4e00-\u9fa5]/;
    
    // 编译完成后处理资源
    compiler.hooks.emit.tapAsync('ChineseCharacterDetectorPlugin', (compilation, callback) => {
      const warnings = [];
      const errors = [];
      
      // 遍历所有JS资源
      Object.keys(compilation.assets).forEach(filename => {
        if (filename.endsWith('.js')) {
          const source = compilation.assets[filename].source();
          const matches = this.findConsecutiveChinese(source, filename);
          
          if (matches.length > 0) {
            if (this.options.logLevel === 'error') {
              errors.push(...matches);
            } else {
              warnings.push(...matches);
            }
          }
        }
      });
      
      // 输出警告
      if (warnings.length > 0) {
          console.log('发现连续中文字符:');
        warnings.forEach(warning => {
          console.log(`  - ${warning.file}:${warning.line}:${warning.column} - "${warning.text}"`);
        });
      }
      
      // 输出错误
      if (errors.length > 0) {
        console.log('发现连续中文字符:');
        errors.forEach(error => {
          console.log(`  - ${error.file}:${error.line}:${error.column} - "${error.text}"`);
        });
        compilation.errors.push(new Error('发现不符合规范的连续中文字符'));
      }
      
      // 输出报告文件
      if (this.options.outputFile && (warnings.length > 0 || errors.length > 0)) {
        const report = [...warnings, ...errors].map(item => 
          `${item.file}:${item.line}:${item.column} - "${item.text}"`
        ).join('\n');
        
        compilation.assets[this.options.outputFile] = {
          source: () => report,
          size: () => report.length
        };
      }
      
      callback();
    });
  }
  
  // 查找连续的中文字符
  findConsecutiveChinese(source, filename) {
    const lines = source.split('\n');
    const results = [];
    
    lines.forEach((line, lineIndex) => {
      let currentMatch = '';
      let startColumn = -1;
      
      for (let i = 0; i < line.length; i++) {
        const char = line[i];
        
        if (chineseRegex.test(char)) {
          if (currentMatch === '') {
            startColumn = i;
          }
          currentMatch += char;
        } else {
          // 连续中文中断,检查长度
          this.checkAndAddMatch(results, filename, lineIndex + 1, startColumn + 1, currentMatch);
          currentMatch = '';
          startColumn = -1;
        }
      }
      
      // 检查行尾的连续中文
      this.checkAndAddMatch(results, filename, lineIndex + 1, startColumn + 1, currentMatch);
    });
    
    return results;
  }
  
  // 检查匹配并添加到结果
  checkAndAddMatch(results, filename, line, column, text) {
    if (text.length >= this.options.threshold) {
      results.push({
        file: filename,
        line,
        column,
        text
      });
    }
  }
}

module.exports = ChineseCharacterDetectorPlugin;

引用插件

js
const ChineseCharacterDetectorPlugin = require('./ChineseCharacterDetectorPlugin');

module.exports = {
  // 其他配置...
  plugins: [
    new ChineseCharacterDetectorPlugin({
      threshold: 3, // 连续中文最小数量,默认为2
      logLevel: 'error', // 日志级别:'info' | 'warning' | 'error'
      outputFile: 'chinese-matches.log' // 输出报告文件路径
    })
  ]
};