[FE] webpack群侠传(四):加载资源

经过前面几篇的努力,我们终于debug进了webpack源码中,
目前位置,位于 webpack/lib/Compiler.js 中,第198行,
https://github.com/webpack/webpack/blob/v4.20.2/lib/Compiler.js#L198

本文我们要探索一下,webpack到底是怎样载入资源的,
babel-loader在怎样被使用的。

一图胜千言,


[FE] webpack群侠传(四):加载资源_第1张图片

webpack(v4.20.2)
loader-runner(v2.3.1)
babel-loader(v8.0.4)

1. webpack源码分析(v4.20.2)

1.1 剧情回顾

首先我们得回顾一下历史内容,
(1)我们先找到了 npm run build 命令实际执行的Node.js代码
(2)然后分析代码,发现它动态require 了所安装的CLI工具的地址
(3)通过debug 模块写入日志,我们找到了CLI模块的源码
(4)最后分析CLI工具源码,发现它执行了webpack 模块的compiler.run

​调用链路如下,

[FE] webpack群侠传(四):加载资源_第2张图片

下面我们来看 run 方法。

1.2 compiler.run 方法

它位于 webpack/lib/Compiler.js 第198行。

run(callback) {
    ...
    this.hooks.beforeRun.callAsync(this, err => {
        ...
        this.hooks.run.callAsync(this, err => {
            ...
            this.readRecords(err => {
                ...
                this.compile(onCompiled);
            });
        });
    });
}

run 方法在第268行调用了this.compile

this.compile(onCompiled);

1.2 compiler.compile方法

compile方法是Compiler的实例方法,在第527行

compile(callback) {
    ...
}

然后在第536行,调用了this.hooks.make.callAsync

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

1.3 hooks.make

关于hooks的解析,我准备放在下一篇,
这里我们需要知道的是,
this.hooks.make.callAsync调用的是某个插件中实现的hooks.make.tapAsync方法。

hooks提供了一种能力,解耦了该hooks的调用者实现方式
我们可以认为代码从调用hooks的地方,直接goto到了具体实现那里。

通过在webpack源码中进行搜索,我们找到了hooks.make的具体实现。
注:实现可能不止一处,webpack会加入队列中按顺序执行。

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

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

它的位置在这里,webpack/lib/SingleEntryPlugin.js 第40行。

其中compiler.hooks.make.tapAsync的第一个参数"SingleEntryPlugin"只是一个名字。
hooks的具体实现是它的第二个参数,

(compilation, callback) => {
    ...
}

在这个hooks中,我们看到它调用了compilation.addEntry
日志信息如下,

debug-webpack webpack webpack.js cliPath: ~/Test/debug-webpack/node_modules/[email protected]@webpack-cli/bin/cli.js +0ms
debug-webpack webpack-cli cli.js start: compiler.run +0ms
debug-webpack webpack Compiler.js start: this.hooks.make.callAsync +0ms
debug-webpack webpack SingleEntryPlugin.js in: compiler.hooks.make.tapAsync +0ms
debug-webpack webpack SingleEntryPlugin.js start: compilation.addEntry +0ms
...
debug-webpack webpack Compiler.js end: this.hooks.make.callAsync +397ms

其中,我们在每个函数的调用之前,加入了start: xxx
然后在函数内第一行加入了in: xxx
再函数的回调函数中,加入了end: xxx

(1)Compiler.js compile方法内部

log('start: this.hooks.make.callAsync');
this.hooks.make.callAsync(compilation, err => {
    log('end: this.hooks.make.callAsync');

(2)SingleEntryPlugin.js apply方法内部

compiler.hooks.make.tapAsync(
    "SingleEntryPlugin",
    (compilation, callback) => {
        log('in: compiler.hooks.make.tapAsync');

1.4 compilation.addEntry

[FE] webpack群侠传(四):加载资源_第3张图片

好了,目前为止,如图所示,
(1)compiler.run调用了hooks.make
(2)hooks.make调用了compilation.addEntry

compilation.addEntry是Compilation对象的addEntry实例方法,
它位于webpack/lib/Compilation.js 第1027行。

addEntry(context, entry, name, callback) {
    ...
}

它内部调用了this._addModuleChain
然后_addModuleChain调用了moduleFactory.create
moduleFactory.create会返回一个module对象,源码位于第943行。

moduleFactory.create(
    {
        contextInfo: {
            issuer: "",
            compiler: this.compiler.name
        },
        context: context,
        dependencies: [dependency]
    },
    (err, module) => {
        ...
    });

1.5 moduleFactory.create

我们看到的很多webpack源码,都是使用回调方式编写的。

// 返回值方式
a = f(v);

// 回调方式
f(v, (a)=>{
    ...
});

在返回值方式中,我们用赋值语句来接收函数的返回值。
而在回调方式中,我们直接调用f,然后在它回调函数的参数中得到返回值。
moduleFactory.create 就是回调方式的一个例子。

它的回调函数参数module,代表了moduleFactory.create 的返回值。
我们在addEntrymoduleFactory.create前后写入log,

debug-webpack webpack webpack.js cliPath: ~/Test/debug-webpack/node_modules/[email protected]@webpack-cli/bin/cli.js +0ms
debug-webpack webpack-cli cli.js start: compiler.run +0ms
debug-webpack webpack Compiler.js start: this.hooks.make.callAsync +0ms
debug-webpack webpack SingleEntryPlugin.js in: compiler.hooks.make.tapAsync +0ms
debug-webpack webpack SingleEntryPlugin.js start: compilation.addEntry +0ms
debug-webpack webpack Compilation.js in: addEntry +0ms
debug-webpack webpack Compilation.js start: this._addModuleChain +1ms
debug-webpack webpack Compilation.js in: _addModuleChain +0ms
debug-webpack webpack Compilation.js start: moduleFactory.create +0ms
debug-webpack webpack Compilation.js end: moduleFactory.create +35ms
debug-webpack webpack Compilation.js module.resource: ~/Test/debug-webpack/src/index.js +0ms
debug-webpack webpack Compilation.js module.loaders: [{"options":{"presets":["@babel/preset-env"]},"ident":"ref--4","loader":"~/Test/debug-webpack/node_modules/[email protected]@babel-loader/lib/index.js"}] +0ms
...
debug-webpack webpack Compilation.js end: this._addModuleChain +0ms
debug-webpack webpack Compiler.js end: this.hooks.make.callAsync +254ms

于是看到,moduleFactory.create完成后得到的module对象中,
包含了待webpack载入的资源地址resource和载入它所用的loaders

即webpack即将使用以下loaders,

[{"options":{"presets":["@babel/preset-env"]},"ident":"ref--4","loader":"~/Test/debug-webpack/node_modules/[email protected]@babel-loader/lib/index.js"}]

载入这个文件,

~/Test/debug-webpack/src/index.js

它正是我们debug-webpack工程(我们调试webpack用的测试工程)中的源代码文件src/index.js

1.6 this.buildModule(module...

得到了module对象之后,webpack使用了this.buildModule方法,第996行,

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();
});

它会调用module对象自身的build方法。
这里跳的就比较远了,
代码跳转到了NormalModule.js中的build方法中,
源码位置位于,https://github.com/webpack/webpack/blob/v4.20.2/lib/NormalModule.js#L396

最后build方法(通过doBuild)又调用了函数runLoaders
日志信息如下,

debug-webpack webpack webpack.js cliPath: ~/Test/debug-webpack/node_modules/[email protected]@webpack-cli/bin/cli.js +0ms
debug-webpack webpack-cli cli.js start: compiler.run +0ms
debug-webpack webpack Compiler.js start: this.hooks.make.callAsync +0ms
debug-webpack webpack SingleEntryPlugin.js in: compiler.hooks.make.tapAsync +0ms
debug-webpack webpack SingleEntryPlugin.js start: compilation.addEntry +0ms
debug-webpack webpack Compilation.js in: addEntry +0ms
debug-webpack webpack Compilation.js start: this._addModuleChain +1ms
debug-webpack webpack Compilation.js in: _addModuleChain +0ms
debug-webpack webpack Compilation.js start: moduleFactory.create +0ms
debug-webpack webpack Compilation.js end: moduleFactory.create +35ms
debug-webpack webpack Compilation.js module.resource: ~/Test/debug-webpack/src/index.js +0ms
debug-webpack webpack Compilation.js module.loaders: [{"options":{"presets":["@babel/preset-env"]},"ident":"ref--4","loader":"~/Test/debug-webpack/node_modules/[email protected]@babel-loader/lib/index.js"}] +0ms
debug-webpack webpack Compilation.js start: this.buildModule +1ms
debug-webpack webpack Compilation.js in: buildModule +0ms
debug-webpack webpack Compilation.js start: module.build +0ms
debug-webpack webpack NormalModule.js in: build +0ms
debug-webpack webpack NormalModule.js start: this.doBuild +0ms
debug-webpack webpack NormalModule.js in: doBuild +1ms
debug-webpack webpack NormalModule.js start: runLoaders +0ms    <---- 进入loader-runner模块
...
debug-webpack webpack NormalModule.js end: runLoaders +208ms
debug-webpack webpack NormalModule.js end: this.doBuild +1ms
debug-webpack webpack Compilation.js end: module.build +215ms
debug-webpack webpack Compilation.js end: this.buildModule +1ms
debug-webpack webpack Compilation.js end: this._addModuleChain +0ms
debug-webpack webpack Compiler.js end: this.hooks.make.callAsync +254ms

runLoaders方法却不在webpack代码库中,
它是一个外部依赖,github: loader-runner。

2. loader-runner源码分析(v2.3.1)

[FE] webpack群侠传(四):加载资源_第4张图片

loader-runner由于涉及到了两个递归函数,iteratePitchingLoadersiterateNormalLoaders
因此显得比较繁琐。

我猜测,这里之所以用递归实现,是因为loader-runner不仅要加载单独的一个文件,
还要加载它依赖的文件,因此是一个递归加载过程。

2.1 入口文件路径

由于loader-runner是在文件webpack/lib/NormalModule.js中首次加载的,
因此,loader-runner模块的本地路径在这里,

~/Test/debug-webpack/node_modules/[email protected]@webpack/node_modules/loader-runner

通过查看它的package.json文件中的main字段,

"main": "lib/LoaderRunner.js",

我们知道实际加载的文件是loader-runner/lib/LoaderRunner.js。
关于main字段我们可以查阅这里,package.json - main

2.2 runLoaders

在文件LoaderRunner.js 第242行,

exports.runLoaders = function runLoaders(options, callback) {
    ...
}

我们看到了runLoaders函数实现。

(1)loadLoader函数
runLoaders执行过程会分为两个环节,先载入loader
然后再使用loader加载文件

因此,runLoaders函数中,首先调用了iteratePitchingLoaders
iteratePitchingLoaders调用了loader-runner/lib/loadLoader.js文件中导出的loadLoader函数。

loadLoader(currentLoaderObject, function(err) {
    ...
}

而loadLoader函数中,在第13行使用了require,载入了loader,

var module = require(loader.path);

(2)iteratePitchingLoaders递归
loadLoader执行完了之后,又会递归的调用iteratePitchingLoaders,位于第173行。

if(!fn) return iteratePitchingLoaders(options, loaderContext, callback);

然后又会递归调用一次,第165行,

if(currentLoaderObject.pitchExecuted) {
    loaderContext.loaderIndex++;
    return iteratePitchingLoaders(options, loaderContext, callback);
}

最终,在iteratePitchingLoaders函数中,调用了processResource,位于第158行

return processResource(options, loaderContext, callback);

(3)processResource函数
processResource函数在第202行调用了iterateNormalLoaders

iterateNormalLoaders(options, loaderContext, [buffer], callback);

iterateNormalLoaders在第229行调用了runSyncOrAsync

runSyncOrAsync(fn, loaderContext, args, function(err) {
    ...
}

(4)runSyncOrAsync函数
该函数执行runLoaders的后半部分逻辑,即使用loader载入用户的源码文件。
我们看到,它在第118行调用了LOADER_EXECUTION函数,来完成操作。

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

webpack loader实际上是一个导出的一个函数,这里的fn就是那个loader函数。

注:
LOADER_EXECUTION实际上是一个异步函数,
fn中会通过调用this.async()得到一个callback函数,见babel-loader/src/index.js 第44行。

const callback = this.async();

而这个this就是fn.apply(context, args)中的context
它的async方法位于,loader-runner/lib/LoaderRunner.js 第95行

context.async = function async() {
    ...
    return innerCallback;
}

因此,end: LOADER_EXECUTION日志需要写到该函数返回的callback中,
即,loader-runner/lib/LoaderRunner.js 第103行 innerCallback函数里。

var innerCallback = context.callback = function() {
    log('end: LOADER_EXECUTION');
    ...
}

(5)iterateNormalLoaders递归
值得注意的是iterateNormalLoaders也是一个递归函数,
runSyncOrAsync执行完之后,会在第233行递归调用iterateNormalLoaders

iterateNormalLoaders(options, loaderContext, args, callback);

然后,进入iterateNormalLoaders函数后,又会在第218行再次递归调用iterateNormalLoaders

return iterateNormalLoaders(options, loaderContext, args, callback);

最终loader-runner调用了babel-loader完成了用户源码的转换操作。

2.3 日志

debug-webpack webpack NormalModule.js start: runLoaders +0ms
debug-webpack loader-runner LoaderRunner.js in: runLoaders +0ms
debug-webpack loader-runner LoaderRunner.js start: iteratePitchingLoaders +1ms
debug-webpack loader-runner LoaderRunner.js in: iteratePitchingLoaders +0ms
debug-webpack loader-runner LoaderRunner.js start: loadLoader +0ms
debug-webpack loader-runner loadLoader.js in: loadLoader +0ms
debug-webpack loader-runner loadLoader.js loader.path: ~/Test/debug-webpack/node_modules/[email protected]@babel-loader/lib/index.js +0ms
debug-webpack loader-runner LoaderRunner.js end: loadLoader +61ms         <---- 1. 完成加载loader
debug-webpack loader-runner LoaderRunner.js start: 1. iteratePitchingLoaders +0ms
debug-webpack loader-runner LoaderRunner.js in: iteratePitchingLoaders +0ms
debug-webpack loader-runner LoaderRunner.js start: 2. iteratePitchingLoaders +0ms
debug-webpack loader-runner LoaderRunner.js in: iteratePitchingLoaders +1ms
debug-webpack loader-runner LoaderRunner.js start: processResource +0ms
debug-webpack loader-runner LoaderRunner.js in: processResource +0ms
debug-webpack loader-runner LoaderRunner.js start: iterateNormalLoaders +0ms
debug-webpack loader-runner LoaderRunner.js in: iterateNormalLoaders +0ms
debug-webpack loader-runner LoaderRunner.js start: runSyncOrAsync +1ms
debug-webpack loader-runner LoaderRunner.js in: runSyncOrAsync +0ms
debug-webpack loader-runner LoaderRunner.js start: LOADER_EXECUTION +0ms
...
debug-webpack loader-runner LoaderRunner.js in: async +0ms
debug-webpack loader-runner LoaderRunner.js in: innerCallback +264ms
debug-webpack loader-runner LoaderRunner.js end: LOADER_EXECUTION +0ms    <---- 2. 完成用loader载入文件
debug-webpack loader-runner LoaderRunner.js end: runSyncOrAsync +0ms
debug-webpack loader-runner LoaderRunner.js start: 1. iterateNormalLoaders +0ms
debug-webpack loader-runner LoaderRunner.js in: iterateNormalLoaders +0ms
debug-webpack loader-runner LoaderRunner.js start: 2. iterateNormalLoaders +0ms
debug-webpack loader-runner LoaderRunner.js in: iterateNormalLoaders +0ms
debug-webpack loader-runner LoaderRunner.js end: iteratePitchingLoaders +0ms
debug-webpack webpack NormalModule.js end: runLoaders +328ms

3. babel源码分析

[FE] webpack群侠传(四):加载资源_第5张图片

3.1 babel-loader(v8.0.4)

在上一节的日志中,我们看到载入的loader文件地址为,

~/Test/debug-webpack/node_modules/[email protected]@babel-loader/lib/index.js

位于我们debug-webpack工程目录的node_modules文件夹中。

注意由于node_modules中的babel-loader不包含src目录,是编译后的结果
我们可以从github仓库中找到源码,babel-loader/src/index.js。
这里我们需要在编译后的结果中进行debug。

我们看到,在babel-loader/lib/index.js 第198行,loader调用了transform函数,

result = await transform(source, options);

transform函数是由babel-loader/lib/transform.js导出的,
其中调用了@babel/core模块导出对象的transform方法。

注:其中source的值就是源文件中的内容,例如,

const a = 1;

result是如下这样的对象,result.code字段中包含了转换后的代码(而不是AST),

{
    "ast": null,
    "code": "var a = 1;",
    "map": null,
    "metadata": {},
    "sourceType": "module"
}

3.2 @babel/core(v7.1.2)

导入的模块位于,

~/Test/debug-webpack/node_modules/_@[email protected]@@babel/core/

通过查看模块的package.json的main字段,

"main": "lib/index.js",

我们得知实际加载的文件是,

~/Test/debug-webpack/node_modules/_@[email protected]@@babel/core/lib/index.js

babel内部的transform过程,这里暂且略过。

3.3 日志

debug-webpack loader-runner LoaderRunner.js start: LOADER_EXECUTION +0ms
debug-webpack loader-runner LoaderRunner.js in: async +0ms
debug-webpack babel-loader index.js in: loader +0ms
debug-webpack babel-loader index.js this.resourcePath: ~/Test/debug-webpack/src/index.js +0ms
debug-webpack babel-loader index.js start: transform +185ms
debug-webpack babel-loader index.js source: alert(); +0ms
debug-webpack babel-loader index.js end: transform +77ms
debug-webpack babel-loader index.js result: {"ast":null,"code":"alert();","map":null,"metadata":{},"sourceType":"module"} +0ms
debug-webpack loader-runner LoaderRunner.js in: innerCallback +264ms
debug-webpack loader-runner LoaderRunner.js end: LOADER_EXECUTION +0ms

4. 回顾

从webpack-cli经历webpack,到loader-runner,最后到babel-loader的调用链路如下,


[FE] webpack群侠传(四):加载资源_第6张图片

整个webpack打包过程,总过涉及了这几层调用,
(1)webpack-cli
(2)webpack
(3)loader-runner
(4)babel-loader

webpack-cli调用webpack完成打包工作,
webpack,调用loader-runner,加载loader,然后用loader加载用户代码,
当前我们的例子中,用到了babel-loader,因此,加载用户代码时调用了babel-loader。

debug-webpack webpack webpack.js cliPath: ~/Test/debug-webpack/node_modules/[email protected]@webpack-cli/bin/cli.js +0ms

debug-webpack webpack-cli cli.js start: compiler.run +0ms

debug-webpack webpack Compiler.js start: this.hooks.make.callAsync +0ms
debug-webpack webpack SingleEntryPlugin.js in: compiler.hooks.make.tapAsync +0ms
debug-webpack webpack SingleEntryPlugin.js start: compilation.addEntry +1ms
debug-webpack webpack Compilation.js in: addEntry +0ms
debug-webpack webpack Compilation.js start: this._addModuleChain +0ms
debug-webpack webpack Compilation.js in: _addModuleChain +0ms
debug-webpack webpack Compilation.js start: moduleFactory.create +0ms
debug-webpack webpack Compilation.js end: moduleFactory.create +65ms
debug-webpack webpack Compilation.js module.resource: ~/Test/debug-webpack/src/index.js +0ms
debug-webpack webpack Compilation.js module.loaders: [{"options":{"presets":["@babel/preset-env"]},"ident":"ref--4","loader":"~/Test/debug-webpack/node_modules/[email protected]@babel-loader/lib/index.js"}] +0ms
debug-webpack webpack Compilation.js start: this.buildModule +1ms
debug-webpack webpack Compilation.js in: buildModule +0ms
debug-webpack webpack Compilation.js start: module.build +0ms
debug-webpack webpack NormalModule.js in: build +0ms
debug-webpack webpack NormalModule.js start: this.doBuild +0ms
debug-webpack webpack NormalModule.js in: doBuild +1ms
debug-webpack webpack NormalModule.js start: runLoaders +0ms

debug-webpack loader-runner LoaderRunner.js in: runLoaders +0ms
debug-webpack loader-runner LoaderRunner.js start: iteratePitchingLoaders +1ms
debug-webpack loader-runner LoaderRunner.js in: iteratePitchingLoaders +0ms
debug-webpack loader-runner LoaderRunner.js start: loadLoader +0ms
debug-webpack loader-runner loadLoader.js in: loadLoader +0ms
debug-webpack loader-runner loadLoader.js loader.path: ~/Test/debug-webpack/node_modules/[email protected]@babel-loader/lib/index.js +0ms
debug-webpack loader-runner LoaderRunner.js end: loadLoader +61ms
debug-webpack loader-runner LoaderRunner.js start: 1. iteratePitchingLoaders +0ms
debug-webpack loader-runner LoaderRunner.js in: iteratePitchingLoaders +0ms
debug-webpack loader-runner LoaderRunner.js start: 2. iteratePitchingLoaders +0ms
debug-webpack loader-runner LoaderRunner.js in: iteratePitchingLoaders +1ms
debug-webpack loader-runner LoaderRunner.js start: processResource +0ms
debug-webpack loader-runner LoaderRunner.js in: processResource +0ms
debug-webpack loader-runner LoaderRunner.js start: iterateNormalLoaders +0ms
debug-webpack loader-runner LoaderRunner.js in: iterateNormalLoaders +0ms
debug-webpack loader-runner LoaderRunner.js start: runSyncOrAsync +1ms
debug-webpack loader-runner LoaderRunner.js in: runSyncOrAsync +0ms
debug-webpack loader-runner LoaderRunner.js start: LOADER_EXECUTION +0ms
debug-webpack loader-runner LoaderRunner.js in: async +0ms

debug-webpack babel-loader index.js in: loader +0ms
debug-webpack babel-loader index.js this.resourcePath: ~/Test/debug-webpack/src/index.js +0ms
debug-webpack babel-loader index.js start: transform +185ms
debug-webpack babel-loader index.js source: alert(); +0ms
debug-webpack babel-loader index.js end: transform +77ms
debug-webpack babel-loader index.js result: {"ast":null,"code":"alert();","map":null,"metadata":{},"sourceType":"module"} +0ms

debug-webpack loader-runner LoaderRunner.js in: innerCallback +264ms
debug-webpack loader-runner LoaderRunner.js end: LOADER_EXECUTION +0ms
debug-webpack loader-runner LoaderRunner.js end: runSyncOrAsync +0ms
debug-webpack loader-runner LoaderRunner.js start: 1. iterateNormalLoaders +0ms
debug-webpack loader-runner LoaderRunner.js in: iterateNormalLoaders +0ms
debug-webpack loader-runner LoaderRunner.js start: 2. iterateNormalLoaders +0ms
debug-webpack loader-runner LoaderRunner.js in: iterateNormalLoaders +0ms
debug-webpack loader-runner LoaderRunner.js end: iteratePitchingLoaders +0ms

debug-webpack webpack NormalModule.js end: runLoaders +328ms
debug-webpack webpack NormalModule.js end: this.doBuild +0ms
debug-webpack webpack Compilation.js end: module.build +339ms
debug-webpack webpack Compilation.js end: this.buildModule +0ms
debug-webpack webpack Compilation.js end: this._addModuleChain +1ms
debug-webpack webpack Compiler.js end: this.hooks.make.callAsync +407ms

参考

github: nvm
github: debug
github: webpack
github: webpack-cli
github: loader-runner
github: babel-loader
github: @babel/core
package.json - main

你可能感兴趣的:([FE] webpack群侠传(四):加载资源)