likes
comments
collection
share

webpack 深入浅出之 —— compiler.compile

作者站长头像
站长
· 阅读数 40

一、前文回顾

上篇小作文完成了启动编译流程的 compiler.run 方法的详细内容讲解,所谓启动,其实是整个编译流程的统揽,看完这篇你就可以大大方方的告诉面试官,webpack 编译大致流程了。

  1. compiler.run 方法内部封装 run 方法,run 来负责具体的工作;
  2. run 方法首先触发 hooks.beforeRun,触发 NodeEnvironmentPlugin 和 ProgressPlugin
  3. 接着触发 hooks.run 钩子,暂无插件;
  4. 调用 compiler.readRecords 方法读取 records 文件,并介绍了 compiler._readRecords 方法的具体实现;
  5. 调用 compiler.compile 方法开启编译流程;
  6. 介绍了处理 compiler.compile 编译结果的 onCompiled 回调;
  7. 介绍了处理最终编译结果并负责和 webpack-cli 通信的 finalCallback 回调函数;

代码执行肯定是个 深度优先 的事儿,但是我这里是个广度优先

今天我们进入下一个重要环节——编译工作开始!前面我们叙述了很多的内容,都属于准备阶段从这里开始就进入正题了。

而实现这些工作的方法是 compiler.compile 方法,下面我们详细的学习一波~

二、compiler.compile

  1. 方法位置:webpack/lib/Compiler.js -> Compiler.prototype.compile
  2. 参数:callback,也就是上一篇小作文里的 onCompiled,该方法主要处理编译产物bundle是否写入及肯定情况下的文件写入工作;

2.1 Compiler.prototype.compile 方法总览

class Compiler {
    // ...
    compile(callback) {
        const params = this.newCompilationParams();
        
        this.hooks.beforeCompile.callAsync(params, err => {
            

            this.hooks.compile.call(params);

            const compilation = this.newCompilation(params);

           
            this.hooks.make.callAsync(compilation, err => {
               
                
                this.hooks.finishMake.callAsync(compilation, err => {
                    

                    process.nextTick(() => {
                        
                        compilation.finish(err => {
                            
                            compilation.seal(err => {
                            
                                this.hooks.afterCompile.callAsync(compilation, err => {
                                
                                  return callback(null, compilation);
                                });
                            });
                        });
                    });
                });
            });
        });
    }
        
}

下面我们快速浏览一下,看看 compile 方法都做了什么工作:

  1. 调用 compiler.newCompilationParams() 获取创建 compilation 实例对象的参数;
  2. 传入上一步获取的 param 对象,触发 compiler.hooks.beforeCompile 钩子;
  3. hooks.beforeCompile 钩子结束后触发 compiler.hooks.compile 钩子,参数同样传入的是第一步获取的 params 对象;
  4. 调用 compiler.newCompilation(params) 创建 compilation 对象;
  5. 传入 compilation 对象触发 compiler.hooks.make 钩子;
  6. compiler.hooks.make 解释后触发 compiler.hooks.finishMake 钩子;
  7. compiler.hooks.finishMake 触发结束后调用 compilation.finish;
  8. compilation.finish 调用结束后调用 compilation.seal 方法;
  9. 在 compilation.seal 方法结束后触发 compiler.hooks.afterCompile;
  10. 最后触发 compiler.compile 的回调函数 callback,也就是上文说的 onCompiled 函数;

2.2 compiler.newCompilationParams() 方法

class Compiler { 
   
    // ...
    newCompilationParams() {  
        const params = {  
            normalModuleFactory: this.createNormalModuleFactory(),  
            contextModuleFactory: this.createContextModuleFactory()  
        };  
        return params;  
    }
}

该方法用于获取 创建 compilation 对象的参数,这里面主要初始化了两个模块工厂类:

  1. NormalModuleFactory: webpack 中的常规模块工厂类;
  2. ContextModuleFactory: 上下文模块的工厂类;

这里需要先解释两个概念:

  1. 普通模块:所谓常规模块和大家认知的 ESM 或 CommonJS 或 CMD/AMD 的模块规范产物,粗暴理解就是 JS 模块;
  2. 上下文模块:webpack 支持一种叫做上下文的模块。当编译时无法确定具体某一个模块时,于是 webpack 直接引用一个目录,把目录中的所有模块都打包了,然后等运行时去具体到某一个模块,这种就叫做上下文模块,比如
const someModuleDir = fetch('some.domain.com/x-pai/get-js-module');

for (const k of someModuleDir) {
   require('../some-ctx-dir/' + k)
}

这种情况下,webpack 为了确保代码能正常工作,webpack 会把 some-ctx-dir 目录下的所有模块都打包;

没看懂对不对,这个也确实不用懂,现在业内主流逻辑是通过静态分析,在编译时就把不必要的东西移除掉;

这种场景纯科普用,不用重点关注,后面我们会自动忽略掉有关 ContextMoudle 的所有处理逻辑;

2.2.1 compiler.createNormalModuleFactory() 方法

class Compiler { 
    // ...
    createNormalModuleFactory() {
       
        const normalModuleFactory = new NormalModuleFactory({});
        
        this.hooks.normalModuleFactory.call(normalModuleFactory);
        return normalModuleFactory;
    }
}

从这里可以清晰看到,createNormalModuleFactory 方法返回了 NormalModuleFactory 这样一个实例对象;

NormalModuleFactory 类型的主要作用在模块的创建和构建过程中,主要做了以下工作:

  1. 定义 factory/resolve 等 NMF 与模块创建相关生命周期钩子;
  2. 格式化 loader 的规则信息,我们在 webpack.config.js 中声明 loader 的方式多种多样,这里有人给你做标准化;
  3. 定义 loader 和 模块的加载解析工作;
  4. 定义模块构建相关工作;

更多具体工作我们等到模块的构建工作中进一步展开,现有一个基本的认识即可;

2.2.2 compiler.createContextModuleFactory() 方法

这里的作用于上面的 NMF 类似,不做过多展开;

2.3 compiler.hooks.beforeCompile.call

触发 compiler.hooks.beforeCompile 钩子,有几个插件订阅在这个阶段:

  1. DllReferencePlugin:DLL 引用插件,即动态链接库的引用库,在 beforeCompile 阶段尝试获取 manifest.json,即 DLL 清单列表;
  2. ProgressPlugin:上文讨论过,该插件的作用是在 Terminal 中输出 webpack 构建进度;
  3. LazyCompilationPlugin:用于在webpack beforeCompile 中针对一些懒加载的入口进行后处理,这不是我们的主线剧情,无须过度关注;

2.4 compiler.hooks.compile

触发 compiler.hooks.compile 钩子,有几个插件订阅在这个阶段:

  1. DllReferencePlugin:在 compiler.hooks.compile 阶段根据配置选项生成 externals 对象,并将其应用到正常的模块工厂。这可以看出,Dll 的引用基于 External 实现类似;
  2. ExternalsPlugin:处理 external 选项,在该阶段为配置的各个 external 配置 ExternalModuleFactoryPlugin,该插件订阅 NMF 的 factory 阶段为配置的外部模块创建模块,以便这些外部模块可以正常的加入的 webpack runtime 的工作流中;

2.5 compiler.newCompilation(params)

创建 compilation 对象,该方法接收前面的 params 对象即 { normalModuleFactory, contextModuleFactory };

class Compiler { 
    // ...
    newCompilation(params) {
        const compilation = this.createCompilation(params);
        compilation.name = this.name;
        compilation.records = this.records;
        this.hooks.thisCompilation.call(compilation, params);
        this.hooks.compilation.call(compilation, params);
        return compilation;
    }	
  
}

创建 compilation 的过程是由 compiler.createCompilation 方法完成:

class Compiler {
    newCompilation (params) {}
    // ....
    createCompilation(params) {
        this._cleanupLastCompilation();
        return (this._lastCompilation = new Compilation(this, params));
    }

}

compilation 是 webpack 中最重要的对象,相当于是处理具体的编译相关事宜的对象。

包括模块的构建、生成 moduleGraph、chunkGraph、以及模块、chunk 的相关优化的工作都是由 compilation 直接或者间接完成(调度生命周期交由具体的插件)完成的。

这些工作,我们后面深度展开,先大致有个印象即可;

2.6 compiler.hooks.make

有了 compilation 之后,触发 compiler.hooks.make 钩子,从这里开始就即将进入模块的构建阶段了。这是因为我们前面在说 WebpackOptionsApply.prototype.process 方法的时候提及的一件事 ——EntryOptionPlguin 的注册。

EntryOptionPlguin 中为每个 entry 注册了 EntryPlugin,而 EntryPlugin 又订阅了 compiler.hooks.make 钩子:

2.6.1 EntryOptionPlugin

EntryOptionPlugin 订阅 compiler.hooks.entryOption 钩子,当钩子触发时调用 EntryOptionPlugin.applyEntryPlugin 根据不同类型的入口应用不同类型的入口插件;

  1. entry 为 function 类型则说明是动态入口,则应用 DynamicEntryPlugin 入口插件;
  2. 否则就是普通的入口,则应用 EntryPlguin 入口插件;
class EntryOptionPlugin {
    apply(compiler) {
        // 订阅 compiler.hooks.entryOpion 钩子
        compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
            EntryOptionPlugin.applyEntryOption(compiler, context, entry);
            return true;
        });
    }


    static applyEntryOption(compiler, context, entry) {
        if (typeof entry === "function") {
            // 处理动态入口场景
            const DynamicEntryPlugin = require("./DynamicEntryPlugin");
            new DynamicEntryPlugin(context, entry).apply(compiler);
        } else {
            // !!!! 这里是重点,这个就是我们常规的配置了
            // 初始化 EntryPlugin 插件初始化普通入口
            const EntryPlugin = require("./EntryPlugin");
            for (const name of Object.keys(entry)) {
                const desc = entry[name];
                const options = EntryOptionPlugin.entryDescriptionToOptions(
                    compiler,
                    name,
                    desc
                );
                for (const entry of desc.import) {
                    new EntryPlugin(context, entry, options).apply(compiler);
                }
            }
        }
    }
}

下面我们看看普通的 EntryPlugin 都做了哪些工作

2.6.2 EntryPlugin

现在我们看看这个大家口中的入口到底是什么实现的,它又默默做了哪些工作呢?

class EntryPlugin {
    constructor(context, entry, options) {
        this.context = context;
        this.entry = entry;
        this.options = options || "";
    }

    apply(compiler) {
        compiler.hooks.compilation.tap(
            "EntryPlugin",
            (compilation, { normalModuleFactory }) => {
                compilation.dependencyFactories.set(
                    EntryDependency,
                    normalModuleFactory
                );
            }
        );

        const { entry, options, context } = this;
        const dep = EntryPlugin.createDependency(entry, options);

        compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
            compilation.addEntry(context, dep, options, err => {
                callback(err);
            });
        });
    }
        
}

总结下来一共就两个点:

  1. 订阅 compiler.hooks.compilation 钩子,向 EntryDependency 设置工厂类为 normalModuleFactoy;
  2. 创建入口依赖并订阅 compiler.hooks.make 钩子,该钩子的回调则是调用 compilation.addEntry 方法正式向 compilation 这个“加工厂” 投放 “原材料”;

当然后面的工作就是围绕着 compilation 进行展开了,后面咱们再深入了解 compilation 对象;

三、总结

本文对 Compiler.prototype.compile 方法进行了由浅入深的解析,compile 则是 compiler 对象真正处理编译工作的地方,因此 compiler.compile 方法的内部就是整个编译过程的调度图。

注意这里称呼“调度员”,这和机场的调度塔塔是一样的,它不负责具体的的航班的起飞、降落等,它只是发布各种指令,然后地勤、机组人员共同完成具体的起降工作。

回过头来,compiler.compile 方法主要做了以下工作:

  1. 调用 compiler.newCompilationParams() 方法为创建 compilation 实例准参数;
  2. compiler.hooks.beforeCompile 钩子;
  3. 触发 compiler.hooks.compile 钩子,参数同样传入的是第一步获取的 params 对象;
  4. 执行 compiler.newCompilation(params) 创建 compilation 实例;
  5. 传入 compilation 对象触发 compiler.hooks.make 钩子;
  6. 接着介绍了订阅在 compiler.hooks.make 上的 EntryPlugin,同时介绍了 EntryPlugin 为整个构建投料的重要意义。

后面相当一段时间的重点将会转移到 compilation 对象上,这是由于具体的工作将要展开了,比如模块的编译、执行 loader,创建模块图等等。

转载自:https://juejin.cn/post/7337309863158300687
评论
请登录