Webpack——插件实现的理解

Webpack 插件是 Webpack 中非常强大的功能,Plugins贯穿整个项目构建过程。

Webpack 的插件包括内置插件和配置中的插件,Webpack 在处理插件时,会将它们都纳入到编译生命周期中。

内置插件

内置插件会在 webpack 编译过程的特定阶段自动执行。它们是由 webpack 核心团队维护的,用于实现 webpack 的核心功能。例如,当 webpack 配置中设置了 mode 为 'production' 时,webpack 会自动启用一些内置插件,如 TerserPlugin(用于代码压缩)等。

配置插件

在 webpack 配置文件中,我们可以在 plugins 数组中添加自定义插件或第三方插件。这些插件会在 webpack 初始化时注册到 Tappable 钩子上并在编译过程中执行。

Tappable 库

webpack 的插件系统是基于 Tapable 库实现的,它提供了多种钩子(hooks)类型,如 SyncHookAsyncSeriesHook 等。插件通过在这些钩子上注册事件回调来在编译过程中执行自定义逻辑。想要深入理解webpack 的插件系统就需要了解 Tappable 是怎么回事。

Tapable是一个用于事件发布订阅执行的库,类似于Node.js的EventEmitter,但更加强大,支持多种类型的事件钩子(Hook)。在webpack中,Tapable被用来创建各种钩子,这些钩子在编译过程中的不同时机被触发。插件通过注册这些钩子来介入编译过程,实现自定义功能。

安装 Tappable

bash 复制代码
npm install tapable

然后,创建一个webpack编译过程的简单示例:

  1. 引入Tapable库,并创建一种类型的钩子(例如SyncHook,同步钩子)。
  2. 定义一个插件,该插件在钩子上注册一个处理函数。
  3. 在编译过程中触发钩子,从而执行插件注册的处理函数。
javascript 复制代码
const { SyncHook } = require('tapable');

// 1. 创建一个同步钩子实例,指定参数列表
const hook = new SyncHook(['arg1', 'arg2']);

// 2. 注册插件
// 插件就是一个对象,它有一个apply方法,apply方法接收一个参数(我们这里简单用hook对象模拟编译器)
// 在apply方法中,我们在钩子上注册一个处理函数
class MyPlugin {
  apply(compiler) {
    compiler.hooks.done = hook; // 假设我们有一个done钩子
    hook.tap('MyPlugin', (arg1, arg2) => {
      console.log('MyPlugin被调用,参数为:', arg1, arg2);
    });
  }
}

// 3. 模拟webpack编译器
class Compiler {
  constructor() {
    this.hooks = {
      // 我们这里用一个SyncHook实例作为done钩子
      done: new SyncHook(['arg1', 'arg2'])
    };
  }

  run() {
    // 模拟编译过程...
    console.log('开始编译...');
    // 编译完成后触发done钩子,并传递参数
    this.hooks.done.call('参数1', '参数2');
  }
}

// 4. 使用插件
const compiler = new Compiler();
const myPlugin = new MyPlugin();
myPlugin.apply(compiler); // 插件注册,将处理函数挂载到钩子上

// 5. 开始编译,触发钩子
compiler.run();

上面使用了 Tappable 中的 SyncHook 同步钩子实例,其实 Tapable 提供了多种类型的 Hook(钩子),用于不同的场景,可以自行了解,不是本篇文章的重点,本篇文章只以比较简单的 SyncHook 同步钩子来理解插件的实现。

这里有一个东西需要区分一下:

javascript 复制代码
const hook = new SyncHook(['arg1', 'arg2']); // Tappable 的钩子
compiler.hooks.done = hook; // webpack 的钩子

Tapable 中 SyncHook 钩子的实现

javascript 复制代码
class SyncHook {
  constructor(args = []) {
    this._args = args; // 参数名称数组
    this.taps = [];    // 存储注册的 webpack 插件
  }

  // 注册同步插件
  tap(name, fn) {
    this.taps.push({
      name,
      type: 'sync',
      fn
    });
  }

  // 触发钩子执行
  call(...args) {
    // 确保参数数量正确
    const finalArgs = args.slice(0, this._args.length);

    // 依次执行所有注册的函数
    for (let i = 0; i < this.taps.length; i++) {
      const tap = this.taps[i];
      tap.fn.apply(this, finalArgs);
    }
  }
}

编译过程中插件的调用原理

Compiler 类实现

javascript 复制代码
// 简化版的 Compiler 类定义
const { Tapable, SyncHook, AsyncSeriesHook } = require('tapable');

export class Compiler extends Tapable {
  constructor(context) {
    super();

    // 1. 核心属性初始化
    this.context = context; // 上下文路径
    this.options = {}; // 配置选项
    this.hooks = this._createHooks(); // 生命周期钩子
    this.name = undefined; // 编译器名称
    this.parentCompilation = undefined; // 父级 compilation
    this.root = this; // 根编译器

    // 2. 文件系统
    this.inputFileSystem = null; // 输入文件系统
    this.outputFileSystem = null; // 输出文件系统
    this.intermediateFileSystem = null; // 中间文件系统

    // 3. 记录和缓存
    this.records = {}; // 构建记录
    this.watchFileSystem = null; // 监听文件系统
    this.cache = new Map(); // 缓存

    // 4. 状态管理
    this.running = false; // 是否正在运行
    this.watchMode = false; // 是否为监听模式
    this.idle = false; // 是否空闲
    this.modifiedFiles = undefined; // 修改的文件
    this.removedFiles = undefined; // 删除的文件
  }

  // 创建生命周期钩子
  _createHooks() {
    return {
      // 初始化阶段
      initialize: new SyncHook([]),

      // 构建开始前
      environment: new SyncHook([]),
      afterEnvironment: new SyncHook([]),
      entryOption: new SyncHook(['context', 'entry']),

      // 构建过程
      beforeRun: new AsyncSeriesHook(['compiler']),
      run: new AsyncSeriesHook(['compiler']),
      beforeCompile: new AsyncSeriesHook(['params']),
      compile: new SyncHook(['params']),
      thisCompilation: new SyncHook(['compilation', 'params']),
      compilation: new SyncHook(['compilation', 'params']),
      make: new AsyncParallelHook(['compilation']),
      afterCompile: new AsyncSeriesHook(['compilation']),

      // 输出阶段
      emit: new AsyncSeriesHook(['compilation']),
      afterEmit: new AsyncSeriesHook(['compilation']),

      // 完成阶段
      done: new AsyncSeriesHook(['stats']),
      failed: new SyncHook(['error']),
      invalid: new SyncHook(['filename', 'changeTime']),
      watchClose: new SyncHook([]),
      shutdown: new AsyncSeriesHook([])
    };
  }

  // 运行构建
  run(callback) {
    // 构建流程实现
     if (this.running) {
      return callback(new Error('Compiler is already running'));
    }

    const finalCallback = (err, stats) => {
      this.running = false;
      this._cleanup();
      if (callback) callback(err, stats);
    };

    const startTime = Date.now();
    this.running = true;

    console.log('🚀 ========== 开始构建流程 ==========\n');

    // 执行构建流程
    this._run((err) => {
      if (err) return finalCallback(err);

      // 生成统计信息
      const stats = this._getStats(startTime);
      console.log('\n📊 生成构建统计信息');

      // 触发 done 钩子
      this.hooks.done.callAsync(stats, (hookErr) => {
        if (hookErr) return finalCallback(hookErr);
        finalCallback(null, stats);
      });
    });
  }

  async _run(callback) {
    try {
      // 1. 触发 beforeRun 钩子
      console.log('📋 阶段 1: 准备构建环境');
      await this.hooks.beforeRun.promise(this);
      console.log('   ✅ beforeRun 完成\n');

      // 2. 触发 run 钩子
      console.log('📋 阶段 2: 启动构建流程');
      await this.hooks.run.promise(this);
      console.log('   ✅ run 完成\n');

      // 3. 读取记录(用于增量构建)
      console.log('📋 阶段 3: 读取构建记录');
      await this._readRecords();
      console.log('   ✅ 记录读取完成\n');

      // 4. 执行编译
      console.log('📋 阶段 4: 执行编译');
      await this._compile();
      console.log('   ✅ 编译完成\n');

      callback();

    } catch (error) {
      console.error('❌ 构建过程出错:', error);
      this.hooks.failed.call(error);
      callback(error);
    }
  }

  async _readRecords() {
    if (this.options.recordsInputPath || this.options.recordsOutputPath) {
      console.log('   📖 读取构建记录文件...');
      await new Promise(resolve => setTimeout(resolve, 50));
      console.log('   ✅ 构建记录加载完成');
    }
  }

  async _compile() {
    // 创建编译参数
    const params = {
      normalModuleFactory: this._createNormalModuleFactory(),
      contextModuleFactory: this._createContextModuleFactory()
    };

    console.log('   🔧 创建编译参数');

    // 触发 beforeCompile 钩子
    console.log('   🎯 触发 beforeCompile 钩子');
    await this.hooks.beforeCompile.promise(params);

    // 触发 compile 钩子
    console.log('   🎯 触发 compile 钩子');
    this.hooks.compile.call(params);

    // 创建 compilation 对象
    console.log('   🏗️  创建 compilation 对象');
    const compilation = this._createCompilation();
    compilation.params = params;

    // 触发 compilation 相关钩子
    this.hooks.thisCompilation.call(compilation, params);
    this.hooks.compilation.call(compilation, params);

    // 触发 make 钩子 - 核心构建阶段
    console.log('   🎯 触发 make 钩子 - 开始构建模块');
    await this.hooks.make.promise(compilation);

    // 密封 compilation(完成模块构建)
    console.log('   🔒 密封 compilation');
    await compilation.seal();

    // 触发 afterCompile 钩子
    console.log('   🎯 触发 afterCompile 钩子');
    await this.hooks.afterCompile.promise(compilation);

    // 生成资源
    console.log('   📄 生成输出资源');
    await this._emitAssets(compilation);
  }

  _createNormalModuleFactory() {
    console.log('   🏭 创建 NormalModuleFactory');
    return {
      type: 'NormalModuleFactory',
      context: this.context
    };
  }

  _createContextModuleFactory() {
    console.log('   🏭 创建 ContextModuleFactory');
    return {
      type: 'ContextModuleFactory'
    };
  }

  _cleanup() {
    console.log('🧹 清理构建环境');
    this.fileTimestamps.clear();
    this.contextTimestamps.clear();
  }

  // 创建 compilation
  createCompilation(params) {
    return new Compilation(this, params);
  }

  // 创建编译参数
  newCompilationParams() {
    return {
      normalModuleFactory: this.createNormalModuleFactory(),
      contextModuleFactory: this.createContextModuleFactory()
    };
  }
}

webpack 方法实现

javascript 复制代码
const { Compiler } = require('./Compiler');

function webpack(config) {
  // 合并配置,这里简化处理,直接使用传入的配置
  const options = config;
  // 创建Compiler实例,传入上下文(通常为当前工作目录)
  const compiler = new Compiler(options.context || process.cwd());
  // 将配置赋值给compiler
  compiler.options = options;
  // 注册配置中的插件
  if (options.plugins && Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
      if (typeof plugin === 'function') {
        plugin.call(compiler, compiler);
      } else {
        plugin.apply(compiler);
      }
    }
  }
  // 返回compiler实例
  return compiler;
}

module.exports = webpack;

运行编译

javascript 复制代码
const webpack = require('./webpack');

const config = {
  context: __dirname,
  plugins: [
    {
      apply(compiler) {
        compiler.hooks.done.tap('MyPlugin', (stats) => {
          console.log('MyPlugin: 构建完成!');
        });
      }
    }
  ]
};

const compiler = webpack(config);

compiler.run((err, stats) => {
  if (err) {
    console.error('构建失败:', err);
    return;
  }
  console.log('构建成功,统计信息:', stats.toString());
});

我们配置的自定义或第三方插件会被存储在 Tapable 钩子实例的 taps 队列中,然后最终注册到 webpack 编译器(complier)的不同的钩子里,最后在 webpack 编译过程中的不同阶段被调用。

以 webpack compile 钩子总结运行过程

  • 创建 Tappable 同步钩子实例,指定参数列表
  • 注册插件
  • 触发 compile 钩子
javascript 复制代码
// Compiler 类
export class Compiler extends Tapable {
  constructor(context) {
    super();

    this.hooks = this._createHooks(); // 生命周期钩子
  }
  // 创建生命周期钩子
  _createHooks() {
    return {
      // 创建 Tappable 同步钩子实例,指定参数列表
      initialize: new SyncHook([]),
    };
  }

  // 运行构建
  run() {
    // 触发 compile 钩子,此处会调用插件配置的回调函数
    console.log('   🎯 触发 compile 钩子');
    this.hooks.compile.call(params);
  }
}

// webpack 插件配置
const config = {
  plugins: [
    {
      // 注册插件
      // 向同一个钩子多注册几个回调函数
      apply(compiler) {
        compiler.hooks.compile.tap('MyPlugin1', (stats) => {
          console.log('MyPlugin1: compile!');
        });
        compiler.hooks.compile.tap('MyPlugin3', (stats) => {
          console.log('MyPlugin2: compile!');
        });
        compiler.hooks.compile.tap('MyPlugin3', (stats) => {
          console.log('MyPlugin3: compile!');
        });
      }
    }
  ]
};

好了,这就是我对 webpack 插件的理解包括配置、注册、回调的整个流程,如果有不对的地方敬请斧正。

相关推荐
Mapmost2 小时前
让 AI 真正看懂世界—构建具备空间理解力的智能体
前端
橙 子_2 小时前
我本以为代码是逻辑,直到遇见了HTML的“形”与“意”【一】
前端·html
二川bro2 小时前
第51节:Three.js源码解析 - 核心架构设计
开发语言·javascript·ecmascript
Kisang.2 小时前
【HarmonyOS】ArkWeb——从入门到入土
前端·华为·typescript·harmonyos·鸿蒙
沉默璇年2 小时前
tgz包批量下载脚本
前端
a***13142 小时前
python的sql解析库-sqlparse
android·前端·后端
0***R5153 小时前
前端构建工具缓存,node_modules
前端·缓存
坚持就完事了3 小时前
CSS-4:CSS的三大特性
前端·css
坚持就完事了3 小时前
CSS-3:背景设置
前端·css·html