webpack 4 源码主流程分析(四):reslove 前的准备

compiler.run

compiler.run是整个编译过程启动的入口,执行:

this.hooks.beforeRun.callAsync(this, (err) => {
  //...this.hooks.run.callAsync(this, (err) => {
    //...// recordsInputPath是webpack配置中指定的读取上一组records的文件路径this.readRecords((err) => {
      //...this.compile(onCompiled);
    });
  });
});
复制代码

在方法中先触发 compiler.hooks: beforeRun,执行之前注册的 NodeEnvironmentPlugin(该插件此时判断 inputFileSystem 是否被配置,如未配置则执行 purge 清理方法),然后在回调里触发 compiler.hooks: run,然后回调里 this.readRecords 是用于读取之前的 records 的方法,再在它的回调里执行 this.compile(onCompiled)。

onCompiled 在 compile 过程后调用,主要用于输出构建资源。

compiler.compile

compile 是真正进行编译的过程,最终会把所有原始资源编译为目标资源。实例化了一个 compilation,并将 compilation 传给 make 钩子上的方法,注册在这些钩子上的方法会调用 compilation 上的 addEntry,执行构建。

获取 compilation 所需 params

this.compile 先执行:

const params = this.newCompilationParams();
复制代码

即:

newCompilationParams() {
    const params = {
        normalModuleFactory: this.createNormalModuleFactory(),
        contextModuleFactory: this.createContextModuleFactory(),
        compilationDependencies: newSet()
    };
    return params;
}
复制代码

该方法先实例化了 NormalModuleFactory 类和 ContextModuleFactory 类,两个类均扩展于 tapable。ContextModuleFactory 类除了兼容老版本之外的代码,没有什么特别需要注意的。接下来具体说明 NormalModuleFactory。

实例化 NormalModuleFactory

NormalModuleFactory 类用于创建一个 normalModule 实例。

实例化 RuleSet

在实例化 NormalModuleFactory 执行 constructor 的过程中,执行:

this.ruleSet = newRuleSet(options.defaultRules.concat(options.rules));
复制代码
  • options.defaultRules 是在之前文件 WebpackOptionsDefaulter.js 中被初始化,然后与项目配置的 module.rules 合并;

  • 每个规则可以分为三部分 - 条件 condition (如 test, include, exclude),结果 result (如应用的 loader,parse 选项) 和嵌套规则 nested rule(如 rules);

  • 条件可接受 正则表达式,字符串,函数等。

  • new RuleSet 实例化过程中,会对每一项 rule 进行进行处理,递归调用静态方法 normalizeCondition 处理 condition 相关,最终每一个 condition 都处理为一个 newRule.resource 函数;递归调用 normalizeUse 处理 result 相关,最终每一个 result 都处理为一个 use 数组,数组的每一项包含 loader 和 options;

  • 调用 ruleSet 的实例 exec 时,传入目标路径和相关信息后,在内部 _run 里,进行递归过滤匹配出对应的 loader,最终得到 result 数组,数组每一项包含 type,value(loader 和 options) 等;

注册 normalModuleFactory.hooks:factory
this.hooks.factory.tap('NormalModuleFactory', () =>(result, callback) => {
  let resolver = this.hooks.resolver.call(null);
  //...resolver(result, (err, data) => {
      //...
    });
  });
});
复制代码

此时注册了 normalModuleFactory.hooks:factory,当后面触发该 hooks 时,该回调返回一个函数。函数内的运行须先触发 normalModuleFactory.hooks:resolver,然后执行其回调结果。

注册 normalModuleFactory.hooks:resolver
this.hooks.resolver.tap('NormalModuleFactory', () =>(data, callback) => {
  //...
});
复制代码

此时注册了 normalModuleFactory.hooks:resolver,跟normalModuleFactory.hooks:factory 相同,当后面触发该 hooks 时,该回调返回一个函数。

在这两个类实例化完成后,分别触发 compiler.hooks: normalModuleFactory ,contextModuleFactory。

实例化 compilation

this.compile 继续执行,先后触发 compiler.hooks: beforeCompile,compile, 这两个钩子都会传入上文 params 作为参数,可用于修改 normalModuleFactory,contextModuleFactory 等。然后在回调中执行:

const compilation = this.newCompilation(params);
复制代码

该方法实例化了一个 Compilation,也是扩展于 tapable。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息,代表了一次资源的构建

在实例化的过程中,除了初始化一些自身资源属性,并实例化了 MainTemplate, ChunkTemplate, HotUpdateChunkTemplate, RuntimeTemplate, ModuleTemplate。用于提供不同的编译模板。

回到 compiler.js,在添加了一些属性后,触发compiler.hooks : thisCompilation, compilation。回忆在 编译前的准备 - 注册plugins阶段 - WebpackOptionsApply.js 的文件里注册了大量该 hooks 的事件,在此时拿到 compilation 对象后,开始执行这一系列事件。

  • compiler.hooks:thisCompilation 会在 compilation 对象的 hooks 里注册一些新的事件;

  • compiler.hooks:compilation 会在 compilation、normalModuleFactory 对象的 hooks 里注册一些新的事件,同时还会往 compilation.dependencyFactories(工厂类),compilation.dependencyTemplates(模板类) 增加依赖模块。

为什么这里需要 thisCompilation,compilation 两个钩子?原因是跟子编译器有关。在 Compiler 的 createChildCompiler 方法里创建子编译器,其中 thisCompilation 钩子不会被复制,而 compilation 会被复制。子编译器拥有完整的 module 和 chunk 生成,通过子编译器可以独立于父编译器执行一个核心构建流程,额外生成一些需要的 module 和 chunk。

开始构建

this.compile 继续执行,触发 compiler.hooks : make ,执行之前在 SingleEntryPlugin | MultiEntryPlugin 注册的的 make 事件,执行:

compilation.addEntry(context, dep, name, callback); //其中 dep 为 SingleEntryDependency 实例复制代码

compilation.addEntry

来到 Compilation.js 文件,addEntry 触发了 compilation.hooks:addEntry 后,定义了入口对象 _preparedEntrypoints,然后直接执行了 this._addModuleChain。

在该方法里,执行:

//...constDep = /** @type {DepConstructor} */ (dependency.constructor);
const moduleFactory = this.dependencyFactories.get(Dep);
复制代码

因 dependency = SingleEntryPlugin.createDependency(entry, name) 即 new SingleEntryDependency(entry),则 Dep 则为 SingleEntryDependency 类,而在之前 compiler.hooks:compilation 的注册事件中添加了依赖: compilation.dependencyFactories.set(SingleEntryDependency, normalModuleFactory),所以 moduleFactory 为 normalModuleFactory。

开始创建 module

编译队列控制:semaphore.acquire

执行:

this.semaphore.acquire(() => {
  moduleFactory.create(
    {
      //...
    },
    (err, module) => {
      //...
    }
  );
});
复制代码

this.semaphore 这个类是一个编译队列控制,原理很简单,对执行进行了并发控制,默认并发数为 100,超过后存入 semaphore.waiters,根据情况再调用 semaphore.release 去执行存入的事件 semaphore.waiters。

moduleFactory.create

this.semaphore.acquire 里执行了 moduleFactory.create。(注:递归解析依赖的重复也从此处开始

本章小结

  1. 从编译过程启动的入口 compiler.run 开始,触发了一系列的生命周期钩子后,执行 compiler.compile。

  1. 获取 compilation 所需 params,实例化 NormalModuleFactory 类(插件会去注册其钩子) 及 ContextModuleFactory 类,在实例化 NormalModuleFactory 的过程中,会实例化 RuleSet 及注册钩子 factory 和 resolver。

  1. 实例化 Compilation,传入 params 参数,触发之前在注册 plugin 阶段所注册的 NormalModuleFactory 下的 hooks。

  1. 触发 make 钩子执行 compilation.addEntry->_addModuleChain,通过编译队列控制 semaphore.acquire 执行 moduleFactory.create 开始创建 module,而递归解析依赖的重复点亦从 create 开始。

作者:Korey

链接:https://juejin.cn/post/6844904047225339918

来源:稀土掘金

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值