前言#
之前几篇文章里分析了整个流程,但是少了很多东西,就比如这章要讲的NormalFactory等相关的流程。
这里通过追踪loader的流程来看下normalFactory、Compilation.constructor、plugin等的逻辑
正文#
介绍#
先来看下loader官方文档是如何介绍的

看前两句就行,简单的说就是一个js模块导出function的规范,然后会被loader runner调用执行,参数为上一个loader的return或者源文件的source code。
环境配置#
这里基于vue-loader来做调试,所以需要做些配置
yarn add webpack webpack-cli webpack-dev-server -D
yarn add vue --save
yarn add vue-loader -D然后配置webpack.config.js
const path = require('path');
const { VueLoaderPlugin } = require('vue-loader');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
entry: './main.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: '[name].[fullhash].js'
},
mode: 'development',
module: {
rules: [
{
test: /\.vue$/,
use: ['vue-loader']
},
]
},
plugins: [
new VueLoaderPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html' // 可移除
}),
new CleanWebpackPlugin(),
],
devServer: {
static: './dist',
open: true,
hot: true,
port: 3000,
},
}
配置mian.js, vue文件随便搞一个即可
import { createApp } from 'vue'
import App from './index/index.vue'
createApp({
...App
}).mount('#app')
然后配置下scripts
"scripts": {
"serve": "webpack-dev-server",
"debug": "node --inspect-brk=3100 ./node_modules/webpack/bin/webpack.js"
},接着直接终端yarn serve即可。
配置完毕之后,直接vscode F5进入调试状态,如果你还不知道如何调试,你可以先去看下我之前的文章:
获取配置文件#
如果想加载这个loader,那就得先拿到配置文件。
在文章1中提到过,配置文件webpack.config.xx是在cli中获得
进入webpack-cli/lib/webpack-cli.js文件中,找到createCompiler方法
async createCompiler(options, callback) {
...省略
let config = await this.loadConfig(options);
config = await this.buildConfig(config, options);
...省略
try {
compiler = this.webpack(config.options, callback
? (error, stats) => {
if (error && this.isValidationError(error)) {
this.logger.error(error.message);
process.exit(2);
}
callback(error, stats);
}
: callback);
// @ts-expect-error error type assertion
}
...省略
}前面分析过了,所以我们这里直接看下数据

loadConfig拿到的是原始的配置buildConfig对当前的配置做一些字段补充,根据配置加载一些内部plugin
然后我们顺着this.webpack(config.options, callback)进入到webpack/lib/webpack.js里看下
配置文件加工#
进入webpack/lib/webpack.js
const webpack = (
(options, callback) => {
const create = () => {
...省略
const webpackOptions = /** @type {WebpackOptions} */ (options);
/** @type {Compiler} */
compiler = createCompiler(webpackOptions);
...省略
}
}) 这里其实就一句compiler = createCompiler(webpackOptions)
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}在这里执行了plugin,由于vue-loader需要vueLoaderPlugin 配合,所以我们这里提一下,但是不分析代码,只简单说下做了什么。
简单的说就是判断你是否有对vue文件的拦截或者对vue文件没有使用vue-loader,没有直接报错。然后重写其它非使用到vueUse的rule的resource和resourceQuery,给他们加上了fakeResourcePath(query + lang)判断,然后方便自己的loader去过滤。
接着我们来到NormalModuleFactory里,上篇文章中提到了肖恩大佬在文章中说NormalModuleFactory模块是resolver、loader、creation of NormalModule instances的粘合剂,所以这里自然不能放过。
NormalModuleFactory#
class NormalModuleFactory extends ModuleFactory {
constructor({
context,
fs,
resolverFactory,
options,
associatedObjectForCache,
layers = false
}) {
super();
this.hooks = Object.freeze({
factorize: new AsyncSeriesBailHook(["resolveData"]),
resolve: new AsyncSeriesBailHook(["resolveData"]),
...省略
});
...省略
}
}hooks可以去看官方文档 ,这里只展示resolve和factorize,resolve的执行时间是在解析请求之前,而factorize的执行时间是在解析器初始化之前。
然后我们接着往下看NormalModuleFactory的constructor里的代码
this.hooks.factorize.tapAsync(
{
name: "NormalModuleFactory",
stage: 100
},
(resolveData, callback) => {
this.hooks.resolve.callAsync(resolveData, (err, result) => {
if (err) return callback(err);
// Ignored
if (result === false) return callback();
// direct module
if (result instanceof Module) return callback(null, result);
...省略
this.hooks.afterResolve.callAsync(resolveData, (err, result) => {
if (err) return callback(err);
...省略
// Ignored
if (result === false) return callback();
const createData = resolveData.createData;
this.hooks.createModule.callAsync(
createData,
resolveData,
(err, createdModule) => {
if (!createdModule) {
...省略
createdModule = new NormalModule(
/** @type {NormalModuleCreateData} */ (createData)
);
}
createdModule = this.hooks.module.call(
createdModule,
createData,
resolveData
);
return callback(null, createdModule);
}
);
});
});
}
);这里做了注册了factorize的事件回调,很直观的看到了执行注册resolve的事件队列。
先来看下这里的resolveData时什么,看名字和字段应该都是和resolve相关,这里先不说是什么,后面会提到。

然后这里对result的返回值做了判断,来看下官方的说法:这个hook在一个请求被解析前执行。如果返回值为false则会掉过这个依赖项,如果返回的是一个module,那么这个进程将结束执行。如果还想继续执行,就需要返回undefined。

接着我们往下看,在resolve的hook中执行了afterResolve的hook,然后这个hook又先是对返回值进行判断,这里就不说明了
接下来的代码是重点代码
const createData = resolveData.createData;
this.hooks.createModule.callAsync(
createData,
resolveData,
(err, createdModule) => {
if (!createdModule) {
if (!resolveData.request) {
return callback(new Error("Empty dependency (no request)"));
}
createdModule = new NormalModule(
/** @type {NormalModuleCreateData} */(createData)
);
}
createdModule = this.hooks.module.call(
createdModule,
createData,
resolveData
);
return callback(null, createdModule);
}
);createData,看数据会有些懵,后面会说到是干什么用的。

createModule,这个hook看名字就能知道做什么用的,生成一个模块。- 接着看到
new NormalModdule这里,就不看里面做了什么了,来看下数据。由于数据过长,这里仅展示一些必要的字段。

这里你会发现有个request字段,这个字段就是上面提到的所谓的“请求” 。来看下这个字段的内容是什么
data:text/javascript,__webpack_public_path__ = __webpack_base_uri__ = htmlWebpackPluginPublicPath;看着有些怪,但不影响我们找到关键点:htmlWebpackPluginPublicPath,再一步提取关键点:htmlWebpackPlugin,这是一个我们引入的插件,那么在哪里用到了这个插件呢?就是之前的配置文件中。
回到webpack.config.js文件中

那么就豁然开朗了,这里的request就是加工过的你需要引入的资源路径。而这资源也能被称为:依赖项,当前文件(模块)的依赖项。
结合factorize和resolve这俩hook的官方描述,那基本上能猜出这一大段是在根据请求的资源路径然后转换成模块。
ok,那么问题来了,这一切是什么时候发生的呢?很简单,Ctrl + F 在NormalModuleFactory 中查找fatorize.call,然后就会发现这个hook在create方法里,实际上看调用堆栈会很快。
/**
* @param {ModuleFactoryCreateData} data data object
* @param {function(Error=, ModuleFactoryResult=): void} callback callback
* @returns {void}
*/
create(data, callback) {
const dependencies = /** @type {ModuleDependency[]} */ (data.dependencies);
const context = data.context || this.context;
const resolveOptions = data.resolveOptions || EMPTY_RESOLVE_OPTIONS;
const dependency = dependencies[0];
const request = dependency.request;
const assertions = dependency.assertions;
const contextInfo = data.contextInfo;
const fileDependencies = new LazySet();
const missingDependencies = new LazySet();
const contextDependencies = new LazySet();
const dependencyType =
(dependencies.length > 0 && dependencies[0].category) || "";
/** @type {ResolveData} */
const resolveData = {
...上面这些字段
};
this.hooks.beforeResolve.callAsync(resolveData, (err, result) => {
...对result状态的判断
this.hooks.factorize.callAsync(resolveData, (err, module) => {
...err状态判断
const factoryResult = {
module,
fileDependencies,
missingDependencies,
contextDependencies,
cacheable: resolveData.cacheable
};
callback(null, factoryResult);
});
});
}resolveData原来在这里初始化的beforeResolve这个hook就不多说了factoryResult看下数据

然后我们再跟着找到调用create方法的地方,通过搜索compilation文件的_factorizeModule方法。这个放方法没什么好说的,就是调用这个factory的实例,然后传入数据和回调方法,将返回的数据进行分类合并。
我们顺藤摸瓜,找到调用factorizeModule方法的地方,在compilation类的构造函数中
/** @type {AsyncQueue<Module, Module, Module>} */
this.processDependenciesQueue = new AsyncQueue({
name: "processDependencies",
parallelism: options.parallelism || 100,
processor: this._processModuleDependencies.bind(this)
});
/** @type {AsyncQueue<Module, string, Module>} */
this.addModuleQueue = new AsyncQueue({
name: "addModule",
parent: this.processDependenciesQueue,
getKey: module => module.identifier(),
processor: this._addModule.bind(this)
});
/** @type {AsyncQueue<FactorizeModuleOptions, string, Module | ModuleFactoryResult>} */
this.factorizeQueue = new AsyncQueue({
name: "factorize",
parent: this.addModuleQueue,
processor: this._factorizeModule.bind(this)
});
/** @type {AsyncQueue<Module, Module, Module>} */
this.buildQueue = new AsyncQueue({
name: "build",
parent: this.factorizeQueue,
processor: this._buildModule.bind(this)
});这一大段的套娃队列,就类似下面这个(不指执行顺序)
processDependenciesQueue -> addModuleQueue -> factorizeQueue -> buildQueue
{
processDependenciesQueue {
addModuleQueue {
factorizeQueue {
buildQueue
}
}
}
}这个AsyncQueue就简单的分析下,毕竟我们得知道processor在哪里会被执行。
_startProcessing(entry) {
this.hooks.beforeStart.callAsync(entry.item, err => {
...省略
let inCallback = false;
try {
this._processor(entry.item, (e, r) => {
inCallback = true;
this._handleResult(entry, e, r);
});
} catch (err) {
if (inCallback) throw err;
this._handleResult(entry, err, null);
}
this.hooks.started.call(entry.item);
});
}处理器processor接收一个参数entry.item,看下个entry长啥样

数据看着依旧是一头雾水,那么我们就还是得去找调用这个方法的地方_ensureProcessing, 这个方法看名字就知道是确保能处理?简单的说就是先执行parent的 queue,然后再执行child的queue。
然后再往上追,找到add方法,发现在这里有调用到。这个add方法就不多说了。然后再往上连续追踪几个方法compilation中的processModuleDependencies方法,再接着追,找到buildModule方法,这个方法看着就很重要,果然再往上追_handleModuleBuildAndDependencies、handleModuleCreation、addModuleTree、_addEntryItem、addEntry跟到这里时就断了。但是没关系,如果你是跟着调用栈的话你会发现刚跟踪代码找到的方法实际上都在调用栈中,所以我们只需要看下调用栈即可。
然后发现是在compiler中的compile方法中的make这个hook里。然后线索就断了,因为什么时候被放进这个hook里的是不会被加入到调用栈中的,所以我们可以换一个方法来跟踪调用栈。
既然它是在make的hook中调用的,你当然可以在tapable中打断点,但是你同时需要加上一段判断是make的hook,不然就相当于大海捞针。
我们可以换个方法来调用,既然是跟踪到compilation的addEntry方法就不见了,那完全可以在这个方法上打断点,这样的话只要有地方调用它就会进入到调用栈中,果不其然,这里entryPlguin在make的hook中注册的回调就有这个方法。

这个plugin是做什么用的?有是在什么时候执行的?
先来看下什么时候注册, 前面的文章中提到过webpack中的createCompiler中将plugin进行执行,那么跟着调用栈发现确实在这之后存在一段代码new WebpackOptionsApply()。再跟着调用栈往里面找发现这里面的procecss方法里有一段代码 new EntryOptionPlugin().apply(compiler);然后在这个entryOptionPlugin中找到了我们想要的entryPlugin。
终于这一大段流程都说完了 ,但是因为是冒泡的方式去看的,所以从头懵到尾,所以在这里从外到内的总结一下这一大段做了什么。
- 在创建
compiler实例时执行了entryOptionPlguin,来看下这个plguin是干什么用的。

这个hook会在这个插件执行后执行。这个plguin主要是将入口路径传入到entryPlugin中。而后在entryPlugin插件中注册了compiler的make的hook回调利用闭包将入口路径等传给compilation.addEntry中。
\2. compiler会在compile执行时先创建compilation实例对象,而在compilation被new的构造函数中创建前面说到过的几个AsyncQueue。
\3. compilation实例创建完毕后compiler的compile方法接着触发了make的hook,开始触发addEntry方法
\4. addEntry触发_addEntryItem方法,触发addEntry的hook之后又触发addModuleTree方法,addModuleTree中根据entry拿到对应的moduleFactory之后执行handleModuleCreation方法 。因为我的配置只有一个入口entry,所以这几个方法里有关多入口相关的逻辑我就都省略不说了。
\5. handleModuleCreation方法触发this.factorizeModule方法,这个方法实际上是factorizeQueue的add方法,看着是不是很眼熟?而这个add方法会触发processor,也就是_factorizeModule方法。先不说这个回调,后面会提到。
\6. _factorizeModule被触发后执行了factory.create的方法,将回调中返回的依赖项进行拆分,合并到对应的map中。
\7. 进入NormalModuleFactory的create方法中。先是触发beforeResolve,又在其回调中触发factorize的hook,而这个hook中的回调中又触发了resolve -> afterResolve -> createModule的hook。
\8. 在createModule的回调中new Normodule,创建了module、在module的hook返回值中拿到module 并最终传入callback中。
\9. 这里面的callback 在多个方法中传来传去并且中间还有套娃,很容易就蒙了。还记得第五点中有个回调还没说吧,是的,这货就是被传来传去中间还被套娃的callback。
\10. 这个callback中执行了this.addModule方法,而作为参数的newModule就是之前传过来的createModule 。这里面的回调又干了什么也不说,和要讲的没有很大关系,虽然写到这我也不知道我要讲的是啥了。
\11. 然后这个addModule实际上是addModuleQueue.add,没想到吧。然后这个add又去触发了_addModule方法。
\12. _addModule里面将module存放到modules中,module是一个key, value对象,来看下数据


key是资源路径value是对应的module
到这里这一块的流程就说完了,但是这里没有包含loader?实际上有的,让我们再看下module的value
这里以vue的文件资源例子看下

这不就是loader么,但是这个loader又是从哪来的?那就得先找到这个module的最开始传入位置, 既然是addModuleQueue的processor,那就得进到AsyncQueue里对应的方法里看,果然是一样的数据,那就得继续网上追,这次就直接看调用栈即可。
原来他被藏在NormalModuleFactory里的create方法里的resolveData,然后你会发现这个数据在create甚至是factorize.call的回调中是空的。
直到执行了resolve这个hook,所以来看下在constructor中的resolve.tap回调里是如何处理这个createData的。
this.hooks.resolve.tapAsync({
name: 'NormalModuleFactory',
state: 100
}, (data, callback) => {
...省略
const defaultResolve = context => {
if (/^($|\?)/.test(unresolvedResource)) {
resourceData = {
resource: unresolvedResource,
data: {},
...cacheParseResource(unresolvedResource)
};
continueCallback();
}
// resource without scheme and with path
else {
const normalResolver = this.getResolver(
"normal",
dependencyType
? cachedSetProperty(
resolveOptions || EMPTY_RESOLVE_OPTIONS,
"dependencyType",
dependencyType
)
: resolveOptions
);
this.resolveResource(
contextInfo,
context,
unresolvedResource,
normalResolver,
resolveContext,
(err, resolvedResource, resolvedResourceResolveData) => {
if (err) return continueCallback(err);
if (resolvedResource !== false) {
resourceData = {
resource: resolvedResource,
data: resolvedResourceResolveData,
...cacheParseResource(resolvedResource)
};
}
continueCallback();
}
);
}
};
...省略
defaultResolve(context);
}) 由于仅跟踪vue-loader相关的,所以这里去掉了一些无关逻辑的代码。
resolveResource来看下它的代码
resolveResource(
contextInfo,
context,
unresolvedResource,
resolver,
resolveContext,
callback
) {
resolver.resolve(
contextInfo,
context,
unresolvedResource,
resolveContext,
(err, resolvedResource, resolvedResourceResolveData) => {
...省略错误执行逻辑
callback(err, resolvedResource, resolvedResourceResolveData);
}
);
}这段代码的关键代码在resolver.resolve方法。跟着调用栈,发现这个resolver来自compiler的new ResolverFactory() ,这里的resolver是通过resolverFactory.get中的_create获取。来看下这个_create做了什么。
_create(type, resolveOptionsWithDepType) {
const originalResolveOptions = { ...resolveOptionsWithDepType };
const resolveOptions = convertToResolveOptions(
this.hooks.resolveOptions.for(type).call(resolveOptionsWithDepType)
);
const resolver = (
Factory.createResolver(resolveOptions)
);
...省略
this.hooks.resolver
.for(type)
.call(resolver, resolveOptions, originalResolveOptions);
return resolver;
} 这一段重点在于这个resolver执行了call,至于type是normal是路径,这个hook暂时不知道在哪里有注册,有大佬知道可以告知下,谢谢。然后代码往下执行返回到resolveResource ,这时回调中拿到的数据如下

接着代码进入到continueCallback中 ,代码挺长,但我们只关心我们想要的那一段代码,也就是Object.assign(data.createData, {...}),你会发现这时的loader也已经获取到了
const continueCallback = needCalls(2, err => {
...省略
const userRequest =
(matchResourceData !== undefined
? `${matchResourceData.resource}!=!`
: "") +
stringifyLoadersAndResource(loaders, resourceData.resource);
const settings = {};
const useLoadersPost = [];
const useLoaders = [];
const useLoadersPre = [];
// handle .webpack[] suffix
let resource;
let match;
...省略
settings.type = "javascript/auto";
const resourceDataForRules = matchResourceData || resourceData;
const result = this.ruleSet.exec({
resource: resourceDataForRules.path,
realResource: resourceData.path,
resourceQuery: resourceDataForRules.query,
resourceFragment: resourceDataForRules.fragment,
scheme,
assertions,
mimetype: matchResourceData
? ""
: resourceData.data.mimetype || "",
dependency: dependencyType,
descriptionData: matchResourceData
? undefined
: resourceData.data.descriptionFileData,
issuer: contextInfo.issuer,
compiler: contextInfo.compiler,
issuerLayer: contextInfo.issuerLayer || ""
});
for (const r of result) {
if (r.type === "use") {
if (!noAutoLoaders && !noPrePostAutoLoaders) {
useLoaders.push(r.value);
}
}
...省略use - post和use - pre,仅保留use
}
let postLoaders, normalLoaders, preLoaders;
const continueCallback = needCalls(3, err => {
...省略
const allLoaders = postLoaders;
if (matchResourceData === undefined) {
for (const loader of loaders) allLoaders.push(loader);
for (const loader of normalLoaders) allLoaders.push(loader);
} else {
for (const loader of normalLoaders) allLoaders.push(loader);
for (const loader of loaders) allLoaders.push(loader);
}
for (const loader of preLoaders) allLoaders.push(loader);
let type = settings.type;
const resolveOptions = settings.resolve;
const layer = settings.layer;
...省略
try {
Object.assign(data.createData, {
layer:
layer === undefined ? contextInfo.issuerLayer || null : layer,
request: stringifyLoadersAndResource(
allLoaders,
resourceData.resource
),
userRequest,
rawRequest: request,
loaders: allLoaders,
resource: resourceData.resource,
context:
resourceData.context || getContext(resourceData.resource),
matchResource: matchResourceData
? matchResourceData.resource
: undefined,
resourceResolveData: resourceData.data,
settings,
type,
parser: this.getParser(type, settings.parser),
parserOptions: settings.parser,
generator: this.getGenerator(type, settings.generator),
generatorOptions: settings.generator,
resolveOptions
});
} catch (e) {
return callback(e);
}
callback();
});
...省略use - post和use - pre,仅保留use
this.resolveRequestArray(
contextInfo,
this.context,
useLoaders,
loaderResolver,
resolveContext,
(err, result) => {
normalLoaders = result;
continueCallback(err);
}
);
});
result,令人热泪盈眶的数据,看了这么久代码终于有熟悉的了useLoader,type为use的loader数组,也就是合并到resolveData.createData中的loader
loader的匹配(?)终于知道在哪里了。
那么就剩下最后一个问题了,loader在什么时候加载执行。
回到compilation中,之前我们说到module都生成完毕并且存放到modules中之后就没下文了,然后就去分析loader的匹配(?) 。但实际上回到_addModule方法中你会发现上面的解释中遗忘了它的第二个参数callback,而这个callback又是来自addModuleQueue.add,而这个addModuleQueue.add实际触发是在
factorizeModule方法中的addModule的第二个参数callback,而这个callback关键代码在this._handleModuleBuildAndDependencies方法中,来看下相关代码。
_handleModuleBuildAndDependencies(originModule, module, recursive, callback) {
...省略
this.buildModule(module, err => {
if (creatingModuleDuringBuildSet !== undefined) {
creatingModuleDuringBuildSet.delete(module);
}
if (err) {
if (!err.module) {
err.module = module;
}
this.errors.push(err);
return callback(err);
}
if (!recursive) {
this.processModuleDependenciesNonRecursive(module);
callback(null, module);
return;
}
// This avoids deadlocks for circular dependencies
if (this.processDependenciesQueue.isProcessing(module)) {
return callback();
}
this.processModuleDependencies(module, err => {
if (err) {
return callback(err);
}
callback(null, module);
});
});
}额,实际上这个回调也没必要发出来,也就是说这个方法最重要的代码就是this.buildModule这一段,然后这个方法实际上是 this.buildQueue.add(module, callback); ,所以又触发了_buildModule方法。
_buildModule(module, callback) {
...省略
module.needBuild(
{
compilation: this,
fileSystemInfo: this.fileSystemInfo,
valueCacheVersions: this.valueCacheVersions
},
(err, needBuild) => {
...省略
this.hooks.buildModule.call(module);
this.builtModules.add(module);
module.build(
this.options,
this,
this.resolverFactory.get("normal", module.resolveOptions),
this.inputFileSystem,
err => {
...省略
this._modulesCache.store(module.identifier(), null, module, err => {
...省略
this.hooks.succeedModule.call(module);
return callback();
});
}
);
}
);
} module.needBuild:这里的module是来自NormalModule,而NormalModule又继承于Module类,needBuild以及等会会提到的build都是继承的Module类的方法,但是自己又重写了这两个方法。needBuild:顾名思义,就是判断是否需要进行build操作,代码就不看了。build:代码就不说了,简单地说就是先对数据状态进行初始化,然后执行_doBuild方法并传入回调_doBuild:
_doBuild(options, compilation, resolver, fs, hooks, callback) {
const loaderContext = this._createLoaderContext(
resolver,
options,
compilation,
fs,
hooks
);
const processResult = (err, result) => {
...省略error
const source = result[0];
const sourceMap = result.length >= 1 ? result[1] : null;
const extraInfo = result.length >= 2 ? result[2] : null;
...省略error
this._source = this.createSource(
options.context,
this.binary ? asBuffer(source) : asString(source),
sourceMap,
compilation.compiler.root
);
if (this._sourceSizes !== undefined) this._sourceSizes.clear();
this._ast =
typeof extraInfo === "object" &&
extraInfo !== null &&
extraInfo.webpackAST !== undefined
? extraInfo.webpackAST
: null;
return callback();
};
this.buildInfo.fileDependencies = new LazySet();
this.buildInfo.contextDependencies = new LazySet();
this.buildInfo.missingDependencies = new LazySet();
this.buildInfo.cacheable = true;
try {
hooks.beforeLoaders.call(this.loaders, this, loaderContext);
} catch (err) {
processResult(err);
return;
}
if (this.loaders.length > 0) {
this.buildInfo.buildDependencies = new LazySet();
}
runLoaders(
{
resource: this.resource,
loaders: this.loaders,
context: loaderContext,
processResource: (loaderContext, resourcePath, callback) => {
const resource = loaderContext.resource;
const scheme = getScheme(resource);
hooks.readResource
.for(scheme)
.callAsync(loaderContext, (err, result) => {
...省略error
return callback(null, result);
});
}
},
(err, result) => {
// Cleanup loaderContext to avoid leaking memory in ICs
loaderContext._compilation =
loaderContext._compiler =
loaderContext._module =
loaderContext.fs =
undefined;
...省略error
this.buildInfo.fileDependencies.addAll(result.fileDependencies);
this.buildInfo.contextDependencies.addAll(result.contextDependencies);
this.buildInfo.missingDependencies.addAll(result.missingDependencies);
for (const loader of this.loaders) {
this.buildInfo.buildDependencies.add(loader.loader);
}
this.buildInfo.cacheable = this.buildInfo.cacheable && result.cacheable;
processResult(err, result.result);
}
);
}loaderContext:都是些熟悉的字段

hooks.beforeLoaders:这个hooks来自compilationrunLoaders:顾名思义,就是执行loaders,这个方法来自loader-runners,这里就不分析了,感兴趣的大佬可以看下。
loader-runnerwww.npmjs.com/package/loader-runner
我们来看下传入的数据resource指的是需要处理的资源的绝对路径,loaders就更不必说了,也就是前面module里的loaders,至于context则是上边的loaderContext。processResource看名字就知道是处理resource的function,来看下三个参数数据长啥样

而readResource这个hook的回调的参数result则是一个buffer对象

而callback参数字段是用来处理执行loader的,这里就绕过了。
来看下runLoaders的回调返回的数据
result:返回的结果,可能是一个字符串数组,也可能是buffer数组,具体看loader如何处理vue-loader处理后的resource则是以下这样的,了解过vue-loader的大佬应该对这个很熟悉了。
'import { render } from "./index.vue?vue&type=template&id=01a29953"\nimport script from "./index.vue?vue&type=script&setup=true&lang=js"\nexport * from "./index.vue?vue&type=script&setup=true&lang=js"\n\nimport exportComponent from "D:\\\\vscode\\\\webpack-study\\\\node_modules\\\\.pnpm\\\\vue-loader@17.0.0_webpack@5.74.0\\\\node_modules\\\\vue-loader\\\\dist\\\\exportHelper.js"\nconst __exports__ = /*#__PURE__*/exportComponent(script, [['render',render],['__file',"views/index.vue"]])\n/* hot reload */\nif (module.hot) {\n __exports__.__hmrId = "01a29953"\n const api = __VUE_HMR_RUNTIME__\n module.hot.accept()\n if (!api.createRecord('01a29953', __exports__)) {\n api.reload('01a29953', __exports__)\n }\n \n module.hot.accept("./index.vue?vue&type=template&id=01a29953", () => {\n api.rerender('01a29953', render)\n })\n\n}\n\n\nexport default __exports__' 拿到资源后回到_doBuild中,来看下processResult
const processResult = (err, result) => {
...省略error
const source = result[0];
const sourceMap = result.length >= 1 ? result[1] : null;
const extraInfo = result.length >= 2 ? result[2] : null;
...省略error
this._source = this.createSource(
options.context,
this.binary ? asBuffer(source) : asString(source),
sourceMap,
compilation.compiler.root
);
if (this._sourceSizes !== undefined) this._sourceSizes.clear();
this._ast =
typeof extraInfo === "object" &&
extraInfo !== null &&
extraInfo.webpackAST !== undefined
? extraInfo.webpackAST
: null;
return callback();
}; source即使刚刚那一大串字符串sourceMap,如果webpack.config.xx中开启了sourceMap那这里就不会是nullextraInfo这个我不太清楚,是loader自身拓展的?_source

return callback这个callback自然又是一层套一层传来传去的,就不多说了。
那么说到这里,loader是在哪里执行的以及在哪里和资源匹配上的就已经说完了。总结也不想总结了,觉得还行,有get到大佬你的点点个赞可否?
参考#
- loader-interface https://webpack.js.org/api/loaders/
- normalmodulefactory-hooks https://webpack.js.org/api/normalmodulefactory-hooks/
- afterResolve https://webpack.js.org/api/normalmodulefactory-hooks/#afterresolve
- createModule https://webpack.js.org/api/normalmodulefactory-hooks/#createmodule
- beforeResolve https://webpack.js.org/api/normalmodulefactory-hooks/#beforeresolve
- compiler-hooks-make https://webpack.js.org/api/compiler-hooks/#make
编辑于 2022-09-23 14:10
