webpack源码执行过程分析,loader+plugins

webpack运行于node js之上,了解源码的执行,不仅可以让我们对webpack的使用更为熟悉,更会增强我们对应用代码的组织能力,

本篇文章重点从webpack核心的两个特性loader,plugin,进行深入分析,

我们从一个例子出发来分析webpack执行过程,地址

我们使用 vscode 调试工具来对webpack进行调试,

首先我们从入口出发

"build":"webpack --config entry.js"

示例项目通过npm run build 进行启动,npm run 会新建一个shell,并将 node_modules/.bin 下的所有内容加入环境变量,我们查看下.bin 文件夹下内容

webpack
webpack-cli
webpack-dev-server

可以看到webpack便在其中,
打开文件,可以看到文件头部

#!/usr/bin/env node

使用node执行此文件内容,webpack 文件的主要内容是判断webpack-cli或者webpack-command有没有安装,如果有安装则执行对应文件内容,本例安装了webpack-cli,所以通过对目标cli的require,进入到对应cli的执行,

webpack-cli
webpack-cli是一个自执行函数,对我们在命令行传入的一些参数进行了解析判断,核心内容是把webpack入口文件作为参数,执行webpack,生成compiler

       try {
                compiler = webpack(options);
            } catch (err) {
                if (err.name === "WebpackOptionsValidationError") {
                    if (argv.color) console.error(`\u001b[1m\u001b[31m${err.message}\u001b[39m\u001b[22m`);
                    else console.error(err.message);
                    // eslint-disable-next-line no-process-exit
                    process.exit(1);
                }

                throw err;
            }

生成compiler后,执行compiler.run()或者compiler.watch(),
本例未启动热更新所以执行的是 compiler.run()

            if (firstOptions.watch || options.watch) {
                const watchOptions = firstOptions.watchOptions || firstOptions.watch || options.watch || {};
                if (watchOptions.stdin) {
                    process.stdin.on("end", function(_) {
                        process.exit(); // eslint-disable-line
                    });
                    process.stdin.resume();
                }
                compiler.watch(watchOptions, compilerCallback);
                if (outputOptions.infoVerbosity !== "none") console.error("\nwebpack is watching the files…\n");
                if (compiler.close) compiler.close(compilerCallback);
            } else {
                compiler.run(compilerCallback);
                if (compiler.close) compiler.close(compilerCallback);
            }

既然已经知道核心是这两个参数的执行,我们即可模拟一个webpack的执行过程,本例中,我们创建一个debug.js

const webpack = require('webpack');
const options = require('./entry.js');

const compiler = webpack(options);

我们在webpack()函数前面加上断点,即可通过vscode开始debug
我们先对生成compiler过程进行分析,

webpack函数

const webpack = (options, callback) => {
    const webpackOptionsValidationErrors = validateSchema(
        webpackOptionsSchema,
        options
    );
    if (webpackOptionsValidationErrors.length) {
        throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
    }
    let compiler;
    if (Array.isArray(options)) {
        compiler = new MultiCompiler(options.map(options => webpack(options)));
    } else if (typeof options === "object") {
        options = new WebpackOptionsDefaulter().process(options);

        compiler = new Compiler(options.context);
        compiler.options = options;
        new NodeEnvironmentPlugin().apply(compiler);
        if (options.plugins && Array.isArray(options.plugins)) {
            for (const plugin of options.plugins) {
                if (typeof plugin === "function") {
                    plugin.call(compiler, compiler);
                } else {
                    plugin.apply(compiler);
                }
            }
        }
        compiler.hooks.environment.call();
        compiler.hooks.afterEnvironment.call();
        compiler.options = new WebpackOptionsApply().process(options, compiler);
    } else {
        throw new Error("Invalid argument: options");
    }
    if (callback) {
        if (typeof callback !== "function") {
            throw new Error("Invalid argument: callback");
        }
        if (
            options.watch === true ||
            (Array.isArray(options) && options.some(o => o.watch))
        ) {
            const watchOptions = Array.isArray(options)
                ? options.map(o => o.watchOptions || {})
                : options.watchOptions || {};
            return compiler.watch(watchOptions, callback);
        }
        compiler.run(callback);
    }
    return compiler;
};

我们可以看到,有对options参数的验证validateSchema(webpackOptionsSchema,options);
有对默认配置的合并 options = new WebpackOptionsDefaulter().process(options);
合并内容
然后对所有的plugins配置进行注册操作

if (options.plugins && Array.isArray(options.plugins)) {
            for (const plugin of options.plugins) {
                if (typeof plugin === "function") {
                    plugin.call(compiler, compiler);
                } else {
                    plugin.apply(compiler);
                }
            }
        }

关于这里的注册,我们可以通过写一个plugin来描述执行过程,
本例中我们新建一个testplugin文件,

testplugin

module.exports = class testPlugin{
    apply(compiler){
        console.log('注册')
        compiler.hooks.run.tapAsync("testPlugin",(compilation,callback)=>{
            console.log("test plugin")
            callback()
        })
    }
}

关于插件的编写,我们只需要提供一个类,prototype上含有apply函数,同时拥有一个compiler参数,之后通过tap注册compiler上的hook,使得webpack执行到指定时机执行回调函数,具体编写方法参考写一个插件

本示例插件中,我们在compiler的run hook上注册了testplugin插件,回调的内容为打印 “test plugin”,并且,在注册的时候我们会打印 ”注册“,来跟踪plugin的注册执行流程,

回到webpack 函数,可以看到,进行完插件的注册,就会执行两个hook的回调,

compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();

这时,就会执行我们注册在environment,afterEnvironment上的plugin的回调,其他插件的回调执行也是通过call或者callAsync 来触发执行,webpack整个源码执行过程中会在不同的阶段执行不同的hook的call函数,所以,在我们编写插件的过程中要对流程有些了解,从而将插件注册在合适的hook上,

webpack函数的最后,就是执行compiler.run函数,我们在这里加上断点,进入compiler.run函数,

 this.hooks.beforeRun.callAsync(this, err => {
            if (err) return finalCallback(err);

            this.hooks.run.callAsync(this, err => {
                if (err) return finalCallback(err);

                this.readRecords(err => {
                    if (err) return finalCallback(err);

                    this.compile(onCompiled);
                });
            });
        });

compiler.run 函数中也是执行了一系列的hook,我们编写的testplugin就会在this.hooks.run.callAsync处执行,关于plugin的注册和运行具体细节,本篇先不讲,只需知道注册通过tap,运行通过call即可,,
到了这里,基本的plugin的运行过程我们已经了解,接下来我们通过几个目标来对loader的执行过程进行分析,

  1. 模块如何匹配到相对应loader
  2. 模块是如何递归的解析当前模块引用模块的
  3. loader是在哪里执行的

回到源代码,执行完一些hooks后,进入到compile,

compile(callback) {
        const params = this.newCompilationParams();
        this.hooks.beforeCompile.callAsync(params, err => {
            if (err) return callback(err);

            this.hooks.compile.call(params);

            const compilation = this.newCompilation(params);

            this.hooks.make.callAsync(compilation, err => {
                if (err) return callback(err);

                compilation.finish();

                compilation.seal(err => {
                    if (err) return callback(err);

                    this.hooks.afterCompile.callAsync(compilation, err => {
                        if (err) return callback(err);

                        return callback(null, compilation);
                    });
                });
            });
        });
    }

依旧是一些hooks的执行,重点是make 的hook,我们进入,make hook通过htmlWebpackPlugin注册了一个回调,回调中又注册了一个SingleEntryPlugin,然后又重新执行了make.callAsync,进入了SingleEntryPlugin的回调

compiler.hooks.make.tapAsync(
            "SingleEntryPlugin",
            (compilation, callback) => {
                const { entry, name, context } = this;

                const dep = SingleEntryPlugin.createDependency(entry, name);
                compilation.addEntry(context, dep, name, callback);
            }
        );

可以看到,主要执行了addEntry方法,addEntry中执行addEntry hook,然后调用_addModuleChain,

addEntry(context, entry, name, callback) {
        this.hooks.addEntry.call(entry, name);

        const slot = {
            name: name,
            // TODO webpack 5 remove `request`
            request: null,
            module: null
        };

        if (entry instanceof ModuleDependency) {
            slot.request = entry.request;
        }

        // TODO webpack 5: merge modules instead when multiple entry modules are supported
        const idx = this._preparedEntrypoints.findIndex(slot => slot.name === name);
        if (idx >= 0) {
            // Overwrite existing entrypoint
            this._preparedEntrypoints[idx] = slot;
        } else {
            this._preparedEntrypoints.push(slot);
        }
        this._addModuleChain(
            context,
            entry,
            module => {
                this.entries.push(module);
            },
            (err, module) => {
                if (err) {
                    this.hooks.failedEntry.call(entry, name, err);
                    return callback(err);
                }

                if (module) {
                    slot.module = module;
                } else {
                    const idx = this._preparedEntrypoints.indexOf(slot);
                    if (idx >= 0) {
                        this._preparedEntrypoints.splice(idx, 1);
                    }
                }
                this.hooks.succeedEntry.call(entry, name, module);
                return callback(null, module);
            }
        );
    }

然后_addModuleChain中通过moduleFactory.create 创建modeuleFactory对象,然后执行buildModule

this.buildModule(module, false, null, null, err => {
                            if (err) {
                                this.semaphore.release();
                                return errorAndCallback(err);
                            }

                            if (currentProfile) {
                                const afterBuilding = Date.now();
                                currentProfile.building = afterBuilding - afterFactory;
                            }

                            this.semaphore.release();
                            afterBuild();
                        });

对于loader的匹配,发生于moduleFactory.create()中,其中执行beforeResolve hook,执行完的回调函数中执行factory,factory中执行resolver,resolver是 resolver hook的回调函数,其中通过this.ruleSet.exec和request的分割分别完成loader的匹配,对module匹配到的loader的生成即在这里完成,之后注入到module对象中,接下来我们回到moduleFactory.create的回调函数
此时生成的module对象中有几个显著的属性,

userRequest:
loaders

即当前模块的路径和匹配到的loader,本例中index.js模块即匹配到了testloader,我们编写的测试loader,

testloader

module.exports = function(source){
    console.log("test loader")
    return source+";console.log(123)"
}

关于loader的编写本篇也不细讲,借用一句文档的描述

A loader is a node module that exports a function. This function is called when a resource should be transformed by this loader. The given function will have access to the Loader API using the thiscontext provided to it.

如何写一个loader

我们回到源码,moduleFactory.create回调函数中,执行了buildModule,
buildModule中执行了module.build(),build中执行doBuild,doBuild中执行runloaders,自此开始即为对loader的执行,runloaders中执行iteratePitchingLoaders,然后执行loadLoader,通过import或者require等模块化方法加载loader资源,这里分为几种loaders,根据不同情况,最终执行runSyncOrAsync,runSyncOrAsync中

var result = (function LOADER_EXECUTION() {
            return fn.apply(context, args);
        }());

通过LOADER_EXECUTION()方法对loader进行,执行,返回执行结果,继续执行其他loader,loader的执行即为此处,
loader执行完成之后,buildModule执行完成,进行callback的执行,其中执行了moduleFactory.create中定义的afterBuild函数,afterBuild函数执行了processModuleDependencies函数,processModuleDependencies函数中通过内部定义的addDependency和addDependenciesBlock方法,生成当前module所依赖的module,执行addModuleDependencies

this.addModuleDependencies(
            module,
            sortedDependencies,
            this.bail,
            null,
            true,
            callback
        );

传入此模块的依赖,addModuleDependencies中循环对sortedDependencies进行了factory.create,factory.create中又执行了beforeResolve hook,从而又执行上面流程,匹配loader,执行loader,对依赖进行遍历等步骤,所以,通过这个深度优先遍历,即可对所有模块及其依赖模块进行loade的匹配和处理,自此,loader学习的三个目标已经达成

make hook主要内容即是这些,之后又执行了seal,afterCopile等等等hook,这些即为一些关于代码分割,抽离等等插件的执行时机,为我们插件的编写提供了一些入口,compiler和compilation执行过程中的所有hook可以查看文档,一共有九十多个(汗颜💧)compiler hook
compilation hook

至此,loader的执行过程和plugin的执行过程已经非常清晰,本篇文章目的也已达到,如果大家对某些hook的执行位置感兴趣或者对某些插件某些loader感兴趣,即可使用debugger根据此流程进行跟踪,从而对插件,loader的使用更加得心应手,

本篇文章示例代码github地址

如果本篇文章对你了解webpack有一定的帮助,顺便留个star ><

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容

  • 说在前面:这些文章均是本人花费大量精力研究整理,如有转载请联系作者并注明引用,谢谢本文的受众人群不是webpack...
    RockSAMA阅读 6,897评论 2 7
  • 前几篇文章中,我们介绍了webpack v4.20.2相关的内容,但是很多老项目,还在使用webpack 3,也要...
    何幻阅读 2,657评论 0 1
  • 写在开头 先说说为什么要写这篇文章, 最初的原因是组里的小朋友们看了webpack文档后, 表情都是这样的: (摘...
    Lefter阅读 5,273评论 4 31
  • GitChat技术杂谈 前言 本文较长,为了节省你的阅读时间,在文前列写作思路如下: 什么是 webpack,它要...
    萧玄辞阅读 12,671评论 7 110
  • 全民健身政策实施多年,关注健身的人越来越多,人们已经意识到良好的健身习惯对于保持健康的重要性,更多的人开始走进健身...
    火鸟健身阅读 105评论 0 0