Webpack 5分钟:compilation.addEntry

一、前文回顾

上文详述了 compilation 对象的创建过程,期间讨论了以下内容:

  1. Compilation 类型的信息,包括定义文件模块、参数等;
  2. Compilation 的构造函数的主要逻辑:
    • 2.1 compilation.hooks 定义;
    • 2.2 compilation.mainTemplate 的作用------生成主入口 chunk 的模板;
    • 2.3 compilation.chunkTemplate 的作用------生成非入口 chunk 的模板;
    • 2.4 compilation.runteimTemplate 的作用 ------ 生成 webpack 运行时相关代码(runtime)模板;
    • 2.5 compilation.moduleTemplate 的作用 ------ 生成 module 相关代码的模板;
    • 2.6 模块构建的四队列的作用,并简述了各个队列的 processor 函数的作用,另外大致讲述了这四个队列的协同关系;

从这篇文章开始,我们将进入到正式模块构建工作。这一切的开始源自 EntryOptionPlugin 注册的 EntryPlugin 被触发,在 EntryPlugin 中调用了 compilation.addEntry 方法。

而我们今天的重点从 compililation.addEntry 开始;

二、复习 EntryPlugin 的注册

我们面试常常回答 "webpack 会从入口开始递归解析依赖"。

那这个 入口 到底怎么加入到共建流程中的呢?前面我们在说 compiler 的创建过程中其实已经大致说过,这里为方便后面的行文,这里进行详细讲解。

2.1 WebpackOptionsApply

在创建 Compiler 的实例过程中,有一个重要步骤实例化 WebpackOptionsApply 并调用 process 方法实 例化,在该方法中会实例化 EntryOptionPlugin

js 复制代码
new WebpackOptionsApply().process(compiler, options);


class WebpackOptionsApply {
    constructor () {}
    
    process () {
       // 注册 EntryOptionPlugin 
        new EntryOptionPlugin().apply(compiler);
    }

}

2.2 EntryOptionPlugin

EntryOptionPlugin 的作用是注册 compiler.hooks.entryOption 钩子,当钩子被触发时,调用 EntryOptionPlugin.applyEntryOption 方法注册 DynamicEntryPlugin 或者 EntryPlugin

js 复制代码
class EntryOptionPlugin {

    apply(compiler) {
        compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
            // 调用静态方法 applyEntryOption
            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 {
            // 普通入口
            const EntryPlugin = require("./EntryPlugin");
            for (const name of Object.keys(entry)) {
                    
                    for (const entry of desc.import) {
                        new EntryPlugin(context, entry, options).apply(compiler);
                    }
            }
        }
    }

}

2.3 EntryPlugin

EntryPlugin 主要做了两件事;

  1. 订阅 compiler.hooks.compilation 钩子,触发时设置 EntryDependency 的 ModuleFactory normalModuleFactory 工厂。这个用于创建入口模块用,后面讲;

  2. 订阅 compiler.hooks.make 钩子,触发时调用 compilation.addEntry 方法,将入口模块加入到 factorizeQueue 队列中,如此依赖就启动了以入口模块为起点的模块构建流程;

js 复制代码
class EntryPlugin {

    constructor(context, entry, options) {}

    apply(compiler) {
        // 1.

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

        // 2.
        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);
            });
        });
    }

}

2.4 EntryPlugin 的触发

在调用 compiler.run() -> compiler.compile() 方法时会触发 compiler.hooks.make 钩子,进而触发 EntryPlugin 钩子;

js 复制代码
class Compiler {
    
    compile(callback) {
        this.hooks.beforeCompile.callAsync(params, err => {
            this.hooks.compile.call(params);

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

三、compilation.addEntry

结合前面的对 EntryPlugin 的复习,我们得到了这样一条从 npm run build 到 compilation.addEntry 的主线剧情

text 复制代码
$: npm run build
    -> webpack
        -> webpack-cli
            -> compiler.run()
                -> compiler.compile()
                    -> compiler.hooks.make.callAsync()
                        -> EntryPlugin 被触发
                            -> compilation.addEntry() 

到这里我们开始进入 compilation.addEntry 方法的讲解:

js 复制代码
class Compilation {
    
    addEntry(context, entry, optionsOrName, callback) {
    
        const options =
            typeof optionsOrName === "object"
                    ? optionsOrName
                    : { name: optionsOrName };

        this._addEntryItem(context, entry, "dependencies", options, callback);
    }


    _addEntryItem(context, entry, target, options, callback) {}

}

3.1 方法参数:

  1. context: 上下文目录,构建中就是当前项目的项目目录;
  2. entry: 入口对象,结合 EntryPlugin 可以看到传入的是由 EntryPlugin.createDependency 方法返回值对象;
  3. optionsOrName: 选项或者名字;
  4. callback: entry 处理的回调函数,用于和 compiler 通信进行后续的流程;

以下为调用时的实参:

3.2 方法逻辑

方法内部其实很简单,只做了两件事:

  1. 标准化处理 options,根据 optionsOrName 类型格式化成不同对象;
  2. 调用 compilation._addEntryItem() 方法;

下面我们开始进入compilation._addEntryItem 方法

四、compilation._addEntryItem

js 复制代码
class Compilation {
    _addEntryItem(context, entry, target, options, callback) {
        const { name } = options;
        let entryData =
            name !== undefined ? this.entries.get(name) : this.globalEntry;
        if (entryData === undefined) {
            entryData = {
                dependencies: [],
                includeDependencies: [],
                options: {
                    name: undefined,
                    ...options
                }
            };
            entryData[target].push(entry);
            this.entries.set(name, entryData);
        } else {
            // 这里不成立,先忽略
        }

        this.hooks.addEntry.call(entry, options);

        this.addModuleTree(/* ... */);
    }
}

4.1 参数

  1. context: webpack 构建上下文目录,及项目目录
  2. entry: 入口对象,上文中 EntryPlugin.createDependency() 返回的 EntryDependeny 类型的实例对象;
  3. target: 目标类型,用于在 entryData 中分类类型进行缓存的标识;
  4. options: webpack 配置对象或者 nameOptions 对象,由 compilation.addEntry 标准化;
  5. callback: 回调函数,这个回调是 compilation.addEntry 接收到 callback,同理用于和 compiler 通信用;

下图为该方法执行时接到的实参图片:

4.2 逻辑

该方法执行分以下几个步骤:

  1. 根据 options 中是否有 name 属性从 compilation.entries 或者 compilation.globalEntry 中尝试获取缓存的入口 entryData;
  2. 根据上一步的 entryData 是否获取成功进行后续操作,这里我们重点关注没有获取到的情况(因为初次构建时没有缓存);
  3. 当没有 entryData 时,我们构建 entryData 对象,并将其缓存到 compilation.entries,另外 entryData.dependency 设为原始 entry 入口对象;即入口数据的依赖为 entry ;
  4. 触发 this.hooks.addEntry 钩子,传入 entry 和 options 对象;
  5. 调用 this.addModuleTree 方法;

4.3 _addEntryItem 的回调

这里的回调是指 _addEntryItem 调用时接收到的回调函数:

js 复制代码
addEntry(context, entry, optionsOrName, callback) {
    this._addEntryItem(context, entry, "dependencies", options, callback);
}

其实这里可以很清楚的看到 callback 来自 addEntry 方法,这个 callback 就是在 EntryPlugin 调用 compilation.addEntry 时传入的 callback;

五、compilation.addModuleTree

5.1 方法参数(或参数已经结构的属性)

  1. context:上下文,当前项目的目录;
  2. dependency:依赖对象,处理入口时就是 EntryPlugin 中创建的 入口依赖 EntryDependency 了;
  3. contextInfo:上下文信息,解析入口传入的 undefined;

以下是解析入口时的实参:

5.2 方法逻辑

  1. 该方法也用户根据用户传入的 depencency 获取对应的 moduleFactory;
  2. 然后组织 moduleFactory 和 dependency 等信息传给 compilation.handleModuleCreation 方法;而 ompilation.handleModuleCreation 方法内部会调用 factorizeModule 方法执行模块 及其依赖的子模块的创建、构构建工作;
js 复制代码
class Compilation {
    // ...
    
    addModuleTree({ context, dependency, contextInfo }, callback) {
        const Dep =  dependency.constructor;
        const moduleFactory = this.dependencyFactories.get(Dep);

        this.handleModuleCreation(
            {
                factory: moduleFactory,
                dependencies: [dependency],
                originModule: null,
                contextInfo,
                context
            },
            (err, result) => {
                if (err && this.bail) {
                
                } else if (!err && result) {
                    callback(null, result);
                } else {
                    callback();
                }
            }
        );
    }
}

5.3 addModuleTree 回调

严格来讲,addModuleTree 回调并不属于 addModoleTree 方法,但是在这里说这个事儿会更清晰;

js 复制代码
this.addModuleTree(
    {
      // .... 参数对象
    },
    (err, module) => {
        // 回调
        this.hooks.succeedEntry.call(entry, options, module);
        return callback(null, module);
    }
);

回调函数中就干了两件事:

  1. 触发 compilation.hooks.succeedEntry 钩子,并传入 entry 对象等参数;
  2. 调用 callback,这个 callback 是前面 compilation._addEntryItem 调用时传递的;这个 callback 的作用是 addEntry 的时候收到用于和 compiler 通信的 (详见:compilation.addEntry 3.1 方法参数)

六、总结

本文开始进入到了 webpack 处理入口模块的创建工作,今天主要复习compilation 和 compiler 协同启动模块构建工作:

  1. 复用 EntryPlugin 的注册过程及关键 compiler.hooks.make,此时 EntryPlugin 调用 compilation.addEntry 方法传入入口;
  2. 学习了 compilation.addEntry 方法作用,其核心调用 compilation._addEntryItem 方法加入 entry 项;
  3. 学习了 compilation._addEntryItem 方法,其内部处理 entryData 并调用 compilation.addModuleTree 加入入口模块,入口模块相当于 ModuleTree 的 root 节点;
  4. 学习了 compilation.addModuleTree 方法内部则是调用 compilation._handleModuleCreation 方法开始处理入口模块及其依赖的子模块的创建工作;

那么下文呢,我们就要进入 compilation._handleModuleCreation 探寻 webpack 宇宙的模块创建星球!

相关推荐
栈老师不回家1 小时前
Vue 计算属性和监听器
前端·javascript·vue.js
前端啊龙1 小时前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js
一颗松鼠1 小时前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
小远yyds1 小时前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
阿伟来咯~2 小时前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端2 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱2 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai2 小时前
uniapp
前端·javascript·vue.js·uni-app
也无晴也无风雨2 小时前
在JS中, 0 == [0] 吗
开发语言·javascript
58沈剑3 小时前
80后聊架构:架构设计中两个重要指标,延时与吞吐量(Latency vs Throughput) | 架构师之路...
架构