三千字基于 HMR 插件解析 Webpack 源码

本文由团队成员 咕噜 撰写,已授权涂鸦大前端独家使用,包括但不限于编辑、标注原创等权益。

Webpack 关键对象简析

认识 Tapable

Tapable 类似 node 中的重用到的 events 库,其本质就是一个 发布订阅模式 。

var EventEmitter = require('events')

var ee = new EventEmitter()
ee.on('message', function (text) {
  console.log(text)
})
ee.emit('message', 'hello world')
复制代码

与 events 的一些区别

1. 订阅、发布的接口名不同

基于 tapbale 实例化:

const { SyncHook } = require("tapable");

// 1. 创建钩子实例
const sleep = new SyncHook();

// 2. 调用订阅接口注册回调
sleep.tap("test", () => {
  console.log("callback A");
});

// 3. 调用发布接口触发回调
sleep.call();

// 4. 在 webpack compiler 对象上的订阅,一般在 webpack 里都挂载在 hooks 命名空间下
compiler.hooks.someHook.tap('MyPlugin', (params) => {
  /* ... */
});
复制代码

基于 node EventEmitter 实例化:

const EventEmitter = require('events');

// 1. 创建钩子实例
const sleep = new EventEmitter();

// 2. 调用订阅接口注册回调
sleep.on('test', () => {
  console.log("callback A");
});

// 3. 调用发布接口触发回调
sleep.emit('test');
复制代码

2. 实例化时传入的数组其代表的为参数的语义

// 1. 创建钩子实例时
class Compiler {
  constructor() {
    this.hooks = {
      compilation: new SyncHook(["compilation", "params"]),
    };
  }
  /* ... */
}

// 2. 调用发布接口触发回调时
newCompilation(params) {
  const compilation = this.createCompilation();
  compilation.name = this.name;
  compilation.records = this.records;
  this.hooks.thisCompilation.call(compilation, params);
  this.hooks.compilation.call(compilation, params);
  return compilation;
}
复制代码

3. 拓展了异步钩子

// 异步钩子
compiler.hooks.beforeCompile.tapAsync(
  'MyPlugin',
  (params, callback) => {
    console.log('Asynchronously tapping the run hook.');
    callback();
  }
);

// 异步 promise 钩子
compiler.hooks.beforeCompile.tapPromise('MyPlugin', (params) => {
  return new Promise((resolve) => setTimeout(resolve, 1000)).then(() => {
    console.log('Asynchronously tapping the run hook with a delay.');
  });
});
复制代码

另外在 Webpack 中基于 Tapable 实现的类,都是 强耦合 的,如下面 Webpack 官方提供的 demo 所示,在 beforeCompile 阶段,其参数是可被修改的,参考官方文档的描述 This hook can be used to add/modify the compilation parameters

而 Webpack 也正是基于这种 强耦合 的方式,下面的 Compiler 和 Compilation 实例在特定时机触发钩子时会附带上足够的上下文信息,使得 Plugin 能够订阅并且基于当前上下文信息和业务逻辑进而产生副作用(修改上下文状态),从而影响后续的编译流程。

// 强耦合的基于 Tapable 类实现 compiler 实例
compiler.hooks.beforeCompile.tapAsync('MyPlugin', (params, callback) => {
  params['MyPlugin - data'] = 'important stuff my plugin will use later';
  callback();
});
复制代码
  1. 拓展了 HookMap 这种集合操作的特性

源码详见 JavascriptParser – hooks

this.hooks = Object.freeze({
  /** @type {HookMap<SyncBailHook<[CallExpressionNode, BasicEvaluatedExpression | undefined], BasicEvaluatedExpression | undefined | null>>} */
  evaluateCallExpressionMember: new HookMap(
    () => new SyncBailHook(["expression", "param"])
  ),
  ...otherHooks,
});
复制代码

在使用 HookMap 的情况下:

// JavascriptParser.js
const property =
    expr.callee.property.type === "Literal"
        ? `${expr.callee.property.value}`
        : expr.callee.property.name;
const hook = this.hooks.evaluateCallExpressionMember.get(property);
if (hook !== undefined) {
    return hook.call(expr, param);
}

// index.js
const a = expression.myFunc();

// MyPlugin.js
parser.hooks.evaluateCallExpressionMember
  .for('myFunc')
  .tap('MyPlugin', (expression, param) => {
    /* ... */
    return expressionResult;
  });
复制代码

在没有 HookMap 的情况下:

// JavascriptParser.js
const property =
    expr.callee.property.type === "Literal"
        ? `${expr.callee.property.value}`
        : expr.callee.property.name;
return this.hooks[`evaluateCallExpressionMember${property.toFirstUpperCase()}`].call();

// index.js
const a = expression.myFunc();

// MyPlugin.js
parser.hooks.evaluateCallExpressionMemberMyFunc
  .tap('MyPlugin', (expression, param) => {
    /* ... */
    return expressionResult;
  });
复制代码

Compiler

Compiler 即编译管理器,它记录了完整的 Webpack 环境及配置信息,负责编译,在 Webpack 从启动到结束,compiler 只会生成一次。贯穿了 Webpack 打包的整个生命周期。在 compiler 对象上可以拿到 当前 Webpack 配置信息,具体可以查看下面 compiler.options 里的一些数据。

image.png

compiler 内部使用了 Tapable 类去实现插件的发布和订阅,可以看下面一个最简的例子。

const { AsyncSeriesHook, SyncHook } = require('tapable');

// 创建类
class Compiler {
  constructor() {
    this.hooks = {
      run: new AsyncSeriesHook(['compiler']), // 异步钩子
      compile: new SyncHook(['params']), // 同步钩子
    };
  }
  run() {
    // 执行异步钩子
    this.hooks.run.callAsync(this, (err) => {
      this.compile(onCompiled);
    });
  }
  compile() {
    // 执行同步钩子 并传参
    this.hooks.compile.call(params);
  }
}

module.exports = Compiler;
复制代码
const Compiler = require('./Compiler');

class MyPlugin {
  apply(compiler) {
    // 接受 compiler参数
    compiler.hooks.run.tap('MyPlugin', () => console.log('开始编译...'));
    compiler.hooks.complier.tapAsync('MyPlugin', (name, age) => {
      setTimeout(() => {
        console.log('编译中...');
      }, 1000);
    });
  }
}

// 这里类似于 webpack.config.js 的 plugins 配置
// 向 plugins 属性传入 new 实例

const myPlugin = new MyPlugin();

const options = {
  plugins: [myPlugin],
};

const compiler = new Compiler(options);

compiler.run();
复制代码

完整的实现可参考 Webpack v5.38.1 Compiler.js 源码

常用钩子

compiler 的大部分 hooks 详细描述文档可参考 Webpack 官网提供的 Compiler Hooks

  • environment
  • afterEnvironment
  • entryOptions
  • afterPlugins
  • normalModuleFactory
  • compilation
  • make

Compilation

Compilation 代表了一次资源版本构建。当运行 webpack 时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 Compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息,简单来讲就是把本次打包编译的内容存到内存里。Compilation 对象也提供了插件需要自定义功能的回调,以供插件做自定义处理时选择使用拓展。

Compilation 类也继承自 Tapable 类并提供了一些生命周期钩子,compilation 实例的具体结构可参考下图。

image.png

常用钩子

compilation 的大部分 hooks 详细描述文档可参考 Webpack 官网提供的 Compilation Hooks

  • seal
  • finishModules
  • record
  • optimize

NormalModuleFactory

NormalModuleFactory 模块被 Compiler 编译用于生成各类模块。从入口点开始,NormalModuleFactory 会分解每个请求,解析文件内容以查找进一步的请求,然后通过分解所有请求以及解析新的文件来爬取全部文件。在最后阶段,每个依赖项都会成为一个模块 (Module) 实例。

NormalModuleFactory 类扩展了 Tapable 并提供了以下的生命周期钩子,NormalModuleFactory 实例可参考下图。

image.png

常用钩子

normalModuleFactory hooks 详细描述文档可参考 Webpack 官网提供的 NormalModuleFactory Hooks

  • NormalModuleFactory Hooks
  • factorize
  • resolve
  • resolveForScheme
  • afterResolve
  • createModule
  • module
  • createParser
  • parser
  • createGenerator
  • generator

JavascriptParser

parser 实例在 webpack 中被用于解析各类模块,parser 是另一个 webpack 中的继承自 Tapable 的类,并基于此提供了一系列的钩子函数方便插件开发者在解析模块的过程中进行一些自定义的操作,而 JavascriptParser 则是在 webpack 中用到相对来说最多的一个解析器,具体使用方式及其实例结构可参考下方;

compiler.hooks.normalModuleFactory.tap('MyPlugin', (factory) => {
  factory.hooks.parser
    .for('javascript/auto')
    .tap('MyPlugin', (parser, options) => {
      parser.hooks.someHook.tap(/* ... */);
    });
});
复制代码

image.png

常用钩子

JavscriptParser hooks 详细描述文档可参考 Webpack 官网提供的 Javascript Hooks

  • evaluateTypeof
  • evaluate
  • evaluateIdentifier
  • evaluateDefinedIdentifier
  • evaluateCallExpressionMember
  • statement
  • statementIf
  • label
  • import
  • importSpecifier
  • export
  • exportImport

看到上面这些如果对 babel 或者 ast 有过简单的了解是不是有一丝眼熟?

关键对象总结

  • Tapable: 一个适用于 webpack 插件体系的小型发布订阅库;
  • Compiler: 编译管理器,webpack 启动后会创建 compiler 对象,该对象一直存在直到退出。
  • Compilation: 单次编辑过程的管理器,运行过程中只有一个 compiler 但每次文件变更触发重新编译时,都会创建一个新的 compilation 对象。
  • NormalModuleFactory: 模块生成工厂类,其实例在 compiler 中生成,用于从入口文件开始处理依赖关系并生成模块的关系;
  • JavascriptParser: JS 模块解析器,生成的 JavascriptParser 实例在 NormalFactory 中,用于解析 JS 模块。

Webpack 构建流程图

webpack()

实例化 compiler 对象

image.png

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享