Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【webpack进阶系列】构建module流程 #99

Open
amandakelake opened this issue Jan 30, 2020 · 2 comments
Open

【webpack进阶系列】构建module流程 #99

amandakelake opened this issue Jan 30, 2020 · 2 comments

Comments

@amandakelake
Copy link
Owner

amandakelake commented Jan 30, 2020

【webpack进阶系列】核心原理与流程中我们顺着webpack的核心脉络走了一遍run的流程,本篇我们详细读一下webpack如何从入口entry找到所有的依赖,并转为module的

总览

下图为本篇文章的总览流程图
Snipaste_2020-01-30_17-16-13

从入口开始,初始化Compiler控制整个webpack的编译流程,它的生命周期中暴露了很多方法,如run,make,compile,finish,seal,emit等,Compiler中会实例化Compilation,它负责把所有的文件构建处理成module,一个Compiler周期内会反复触发 Compilation 实例

无论入口文件是什么,都要从compilation.addEntry开始,执行_addModuleChain,将入口文件加入需要编译的队列中,将队列里的文件依次处理,通过import/require等方法依赖的文件也会通过addModuleDependencies被添加到编译队列中,当整个编译队列完成时,从文件到module的转换就完成了

编译队列

编译队列的控制是由Semaphore(信号量)来完成的,它的代码量很少,先看下它的实现

// Semaphore.js
class Semaphore {
    constructor(available) {
        //  最大的并发数量
        this.available = available;
        // 等待编译的队列
        this.waiters = [];
        this._continue = this._continue.bind(this);
    }
    acquire(callback) {
        // 有可用资源,直接进行编译处理
        if (this.available > 0) {
            this.available--;
            callback();
        } else {
            // 无可用资源,丢进等待编译的队列
            this.waiters.push(callback);
        }
    }
    // 释放资源
    release() {
        this.available++;
        if (this.waiters.length > 0) {
            process.nextTick(this._continue);
        }
    }
    _continue() {
        if (this.available > 0) {
            if (this.waiters.length > 0) {
                this.available--;
                const callback = this.waiters.pop();
                callback();
            }
        }
    }
}

module.exports = Semaphore;

通过this.available来控制同时处理资源的并发数,默认100

// Compilation.js
this.semaphore = new Semaphore(options.parallelism || 100);

这里的并发概念只是代码中的并发,跟JS单线程并不是同一个概念

只对外暴露了acquire(申请资源)release(释放资源)两个方法

申请资源成功则this.available--,可用资源减1,申请资源失败则把任务丢进等待编译的队列this.waiters

释放资源则this.available++,同时从等待队列中拿出最后一个任务,并在process.nextTick后执行

E17A1021-ABA1-4CBF-AC0B-221335FB3613

入口与模块工厂

通过webpack官网文档,可以看到入口entry可以有string[string]objectfunction等几种形式
976DD5FA-88DF-4F68-A1E6-B8B4D917D8A7

通过前面的文章,我们知道用户自定义的配置都在WebpackOptionsApply.process(options, compiler)中处理了,而对于入口的处理在EntryOptionPlugin插件中

// lib/WebpackOptionsApply.js
new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);

这个插件的代码也很简单

// lib/EntryOptionPlugin.js
const itemToPlugin = (context, item, name) => {
    if (Array.isArray(item)) {
        return new MultiEntryPlugin(context, item, name);
    }
    return new SingleEntryPlugin(context, item, name);
};

module.exports = class EntryOptionPlugin {
    /**
     * @param {Compiler} compiler the compiler instance one is tapping into
     * @returns {void}
     */
    apply(compiler) {
        compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
            if (typeof entry === "string" || Array.isArray(entry)) {
                itemToPlugin(context, entry, "main").apply(compiler);
            } else if (typeof entry === "object") {
                for (const name of Object.keys(entry)) {
                    itemToPlugin(context, entry[name], name).apply(compiler);
                }
            } else if (typeof entry === "function") {
                new DynamicEntryPlugin(context, entry).apply(compiler);
            }
            return true;
        });
    }
};
  • string -> SingleEntryPlugin
  • array -> MultiEntryPlugin
  • function -> DynamicEntryPlugin
  • object -> 遍历后,根据不同类型选择SingleEntryPlugin/MultiEntryPlugin

然后我们再观察一下这个三个插件SingleEntryPlugin、MultiEntryPlugin、DynamicEntryPlugin,它们都会做的两件事情

1、注册compilation钩子,设置dependencyFactories,在后面的module.create()时候可以根据拿到正确的moduleFactorymoduleFactory有可能是normalModuleFactorymultiModuleFactory

2、注册make钩子回调,在make被触发时,调用compilation.addEntry() -> compilation._addModuleChain开始编译工作

// XXXEntryPlugin.js
compiler.hooks.compilation.tap('XXXEntryPlugin', (...) => {
	compilation.dependencyFactories.set(XXXEntryDependency, XXXModuleFactory)
})

compiler.hooks.make.tapAsync('XXXEntryPlugin', (...) => {
	const dep = XXXEntryPlugin.createDependency(entry, name);
	compilation.addEntry(context, dep, name, callback);
})

198223F8-8590-4BF3-BE1F-B9DD71859656

// lib/Compilation.js
const Dep = /** @type {DepConstructor} */ (dependency.constructor);
const moduleFactory = this.dependencyFactories.get(Dep);
// moduleFactory有可能是normalModuleFactory、multiModuleFactory
this.semaphore.acquire(() => {
	moduleFactory.create(...)
})

这一部分的整个流程串起来如下图
189B555B-BC06-459B-9C7F-A005CF36DC5C

compilation构建

进入module.create后,跟着断点走能够比较清晰的看到compilation实例中modulesmodules.dependencies中的数据变化,下面的流程图比较清晰的画出了几个函数方法的调用过程
Compilation生成module

具体runLoadersParser.parse部分的调用逻辑没有深入往下读,后续有时间再另写文章细读
下一篇是封装生成chunk的流程解读,敬请期待

@amandakelake amandakelake changed the title 【webpack进阶系列】Compilation核心流程 -> 构建生成module 【webpack进阶系列】构建module流程 Jan 31, 2020
@ghost
Copy link

ghost commented Oct 10, 2022

good

@ciloi
Copy link

ciloi commented Oct 10, 2022 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants