Webpack源码解析
// 使用webpack版本
"html-webpack-plugin": "^4.5.0",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12"
打包主流程分析
上一篇文章 文件的resolve流程 分析了一个普通文件需要经过大量的 plugins 进行解析处理,处理完毕就一层一层执行回调,将最终的模块信息一层一层的传递到顶层调用者,创建一个 module,然后进行最终的 buildModule, 这一篇就来看看最终的整个回调链路是怎样的。
doResolve最终回调链路
// node_modules/enhanced-resolve/lib/Resolver.js
doResolve () {
...
return hook.callAsync(request, innerContext, (err, result) => {
if (err) return callback(err);
if (result) return callback(null, result);
callback();
});
...
}
to
// node_modules/enhanced-resolve/lib/Resolver.js
resolve () {
...
this.doResolve(
...,
...,
...,
...,
(err, result) => {
if (!err && result) {
return callback(
null,
result.path === false ? false : result.path + (result.query || ""),
result
);
}
...
};
)
...
}
to
// node_modules/webpack/lib/NormalModuleFactory.js
this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
...
asyncLib.parallel(
[
...,
(callback) => {
if (resource === "" || resource[0] === "?") {
return callback(null, {
resource,
});
}
normalResolver.resolve(
contextInfo,
context,
resource,
{},
(err, resource, resourceResolveData) => {
if (err) return callback(err);
callback(null, {
resourceResolveData,
resource,
});
}
);
},
],
...
);
...
})
to
// node_modules/webpack/lib/NormalModuleFactory.js
this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
...
// asyncLib.parallel 并行执行两个函数,一个是处理Loader的,另一个是resolve普通文件的,最终将结果合并成一个数组返回
// 注意这里处理的是同一个request
// results[0] 就是处理Loader的结果
// results[1] 就是加载普通文件的结果
asyncLib.parallel(
[
...,
...
],
(err, results) => {
// 如果我们配置了Loader的话results大致上是这样的(经过第一个函数resolveRequestArray处理过,内联Loader会处理成以下格式)
// {
// "results": [
// [
// {
// "loader": "loader的绝对路径1",
// "options": "loader参数1"
// },
// {
// "loader": "loader的绝对路径2",
// "options": "loader参数2"
// }
// ],
// {
// "resource": "模块绝对路径",
// "resourceResolveData": "模块基本信息(即doResolve执行结果)"
// }
// ]
// }
// results 是一个数组,第一个是Loader的解析结果
// 第二个数组元素是一个对象,对象包含两个属性,一个resource是资源路径,另一个resourceResolveData是资源加载内容
// resource:'/Users/---/lagou-edu/webpack-entry/src/index.js'
// resourceResolveData:
// context:{issuer: '', compiler: undefined}
// file:false
// module:false
// path:'/Users/---/lagou-edu/webpack-entry/src/index.js'
// query:''
// relativePath:undefined
// request:undefined
if (err) return callback(err);
// 取出所有的loaders
let loaders = results[0];
const resourceResolveData = results[1].resourceResolveData;
resource = results[1].resource;
...
// 下面是处理 Loader 的,Loader有两种设置方式,一种是配置化,一种是内联
// 配置化:
// {
// test: /\.css$/,
// use: [
// // [style-loader](/loaders/style-loader)
// { loader: 'style-loader' },
// // [css-loader](/loaders/css-loader)
// {
// loader: 'css-loader',
// options: {
// modules: true
// }
// },
// // [sass-loader](/loaders/sass-loader)
// { loader: 'sass-loader' }
// ]
// }
// 内联:
// 可以在 import 语句或任何 与 "import" 方法同等的引用方式 中指定 loader。使用 ! 将资源中的 loader 分开。每个部分都会相对于当前目录解析。
// import Styles from 'style-loader!css-loader?modules!./styles.css';
// 详细请查看:https://webpack.docschina.org/concepts/loaders/#root
// 我们以上 resolve 的 ./src/index.js 它也会去查找匹配的 Loader 进行解析
// 下面代码就是处理这两种配置方式的load,最终asyncLib.parallel执行处理所有Loaders
// 最终返回data
// 将rules中的loader和options拼接
// data格式如下
// {
// "loader": "loader的绝对路径1",
// "options": "loader参数1"
// },
// const loaderToIdent = data => {
// if (!data.options) {
// return data.loader;
// }
// if (typeof data.options === "string") {
// return data.loader + "?" + data.options;
// }
// if (typeof data.options !== "object") {
// throw new Error("loader options must be string or object");
// }
// if (data.ident) {
// return data.loader + "??" + data.ident;
// }
// return data.loader + "?" + JSON.stringify(data.options);
// };
// Loader识别,配置loader和options,最终加上请求的路径,组合成一个Loader request
// /Users/***/lagou-edu/webpack-entry/node_modules/babel-loader/lib/loader.js?{"module":false}'!/Users/***/lagou-edu/webpack-entry/src/index.js'
const userRequest =
(matchResource !== undefined ? `${matchResource}!=!` : "") +
loaders.map(loaderToIdent).concat([resource]).join("!");
// 分解请求路劲和参数
let resourcePath = matchResource !== undefined ? matchResource : resource;
let resourceQuery = "";
const queryIndex = resourcePath.indexOf("?");
if (queryIndex >= 0) {
resourceQuery = resourcePath.substr(queryIndex);
resourcePath = resourcePath.substr(0, queryIndex);
}
// 进行rule匹配,找到符合条件的Loader模块
// 具体可以参考文章[https://github.com/CommanderXL/Biu-blog/issues/30](https://github.com/CommanderXL/Biu-blog/issues/30)
const result = this.ruleSet.exec({
resource: resourcePath, // 路径
realResource:
matchResource !== undefined ? resource.replace(/\?.*/, "") : resourcePath,
resourceQuery, // 参数
issuer: contextInfo.issuer,
compiler: contextInfo.compiler,
});
// result 为最终根据 module 的 resourcePath 及相关匹配规则过滤后得到的 loaders,为 webpack.config 进行配置的
// 输出的数据格式为:
/**
* [{
* type: 'use',
* value: {
* loader: 'babel-loader',
* options: {}
* },
* enforce: undefined // 可选值还有 pre/post 分别为 pre-loader 和 post-loader
* }]
*/
// 当获取到 webpack.config 当中配置的 loader 后,再根据 loader 的类型进行分组(enforce 配置类型)
// postLoader 存储到 useLoadersPost 内部 后置Loader
// preLoader 存储到 useLoadersPre 内部 前置Loader
// normalLoader 存储到 useLoaders 内部 普通Loader
// `pre > normal > inline > post`
// 这些分组最终会决定加载一个 module 时不同 loader 之间的调用顺序
const settings = {};
const useLoadersPost = [];
const useLoaders = [];
const useLoadersPre = [];
for (const r of result) {
if (r.type === "use") {
if (r.enforce === "post" && !noPrePostAutoLoaders) {
useLoadersPost.push(r.value);
} else if (
r.enforce === "pre" &&
!noPreAutoLoaders &&
!noPrePostAutoLoaders
) {
useLoadersPre.push(r.value);
} else if (!r.enforce && !noAutoLoaders && !noPrePostAutoLoaders) {
useLoaders.push(r.value);
}
} else if (
typeof r.value === "object" &&
r.value !== null &&
typeof settings[r.type] === "object" &&
settings[r.type] !== null
) {
settings[r.type] = cachedCleverMerge(settings[r.type], r.value);
} else {
settings[r.type] = r.value;
}
}
// 当分组过程进行完之后,即开始 loader 模块的 resolve 过程
asyncLib.parallel(
[
this.resolveRequestArray.bind(
this,
contextInfo,
this.context,
useLoadersPost,
loaderResolver
),
this.resolveRequestArray.bind(
this,
contextInfo,
this.context,
useLoaders,
loaderResolver
),
this.resolveRequestArray.bind(
this,
contextInfo,
this.context,
useLoadersPre,
loaderResolver
),
],
(err, results) => {
if (err) return callback(err);
if (matchResource === undefined) {
loaders = results[0].concat(loaders, results[1], results[2]);
} else {
loaders = results[0].concat(results[1], loaders, results[2]);
}
...
// results[0] -> postLoader
// results[1] -> normalLoader
// results[2] -> preLoader
// 这里将构建 module 需要的所有类型的 loaders 按照一定顺序组合起来,对应于:
// [postLoader, inlineLoader, normalLoader, preLoader]
// 最终 loader 所执行的顺序对应为: preLoader -> normalLoader -> inlineLoader -> postLoader
// 不同类型 loader 上的 pitch 方法执行的顺序为: postLoader.pitch -> inlineLoader.pitch -> normalLoader.pitch -> preLoader.pitch
process.nextTick(() => {
const type = settings.type;
const resolveOptions = settings.resolve;
// 执行回调(factory钩子提供的factory函数中调用resolver的回调)
callback(null, {
context: context,
request: loaders.map(loaderToIdent).concat([resource]).join("!"),
dependencies: data.dependencies,
userRequest,
rawRequest: request,
loaders,
resource,
matchResource,
resourceResolveData,
settings,
type,
parser: this.getParser(type, settings.parser),
generator: this.getGenerator(type, settings.generator),
resolveOptions,
});
});
}
);
};
);
...
})
to
// node_modules/webpack/lib/NormalModuleFactory.js
this.hooks.factory.tap("NormalModuleFactory", () => (result, callback) => {
...
resolver(result, (err, data) => {
if (err) return callback(err);
// Ignored
if (!data) return callback();
// 如果是一个直接的module就直接执行回调
if (typeof data.source === "function") return callback(null, data);
// Resolver拿到的是模块的依赖关系和文件的Loader关系,这里执行resolve后的钩子
this.hooks.afterResolve.callAsync(data, (err, result) => {
// result 如下
/**
* context:'/Users/---/lagou-edu/webpack-entry'
* ▼ dependencies:(1) [SingleEntryDependency]
* ▼ 0:SingleEntryDependency {…}
* loc:{name: 'main'}
* module:null
* optional:false
* request:'./src/index.js'
* type (get):ƒ type() {\n\t\treturn "single entry";\n\t}
* userRequest:'./src/index.js'
* weak:false
* ▶__proto__:ModuleDependency
* length:1
* ▶ __proto__:Array(0)
* ▶ generator:JavascriptGenerator
* ▼ loaders:(1) [{…}]
* ▼ 0: {...}
* ident:'ref--4'
* loader:'/Users/---/lagou-edu/webpack-entry/node_modules/babel-loader/lib/index.js'
* ▼ options:{presets: Array(1)}
* presets:(1) ['@babel/preset-env']
* 0:'@babel/preset-env'
* length:1
* ▶ __proto__:Array(0)
* ▶ __proto__:Object
* ▶ __proto__:Object
* length:1
* ▶ __proto__:Array(0)
* matchResource:undefined
* ▶ parser:Parser {_pluginCompat: SyncBailHook, hooks: {…}, options: {…}, sourceType: 'auto', scope: undefined, …}
* rawRequest:'./src/index.js'
* request:'/Users/---/lagou-edu/webpack-entry/node_modules/babel-loader/lib/index.js??ref--4!/Users/---/lagou-edu/webpack-entry/src/index.js'
* ▶ resolveOptions:{}
* resource:'/Users/---/lagou-edu/webpack-entry/src/index.js'
* resourceResolveData:{…}
* ▶ context:{issuer: '', compiler: undefined}
* ▶ descriptionFileData:{name: 'webpack-code', version: '1.0.0', main: './src/index.js', license: 'MIT', devDependencies: {…}, …}
* descriptionFilePath:'/Users/---/lagou-edu/webpack-entry/package.json'
* descriptionFileRoot:'/Users/---/lagou-edu/webpack-entry'
* file:false
* module:false
* path:'/Users/---/lagou-edu/webpack-entry/src/index.js'
* query:''
* relativePath:'./src/index.js'
* request:undefined
* __innerRequest:'./src/index.js'
* __innerRequest_relativePath:'./src/index.js'
* __innerRequest_request:undefined
* ▶ __proto__:Object
* ▶ settings:{type: 'javascript/auto', resolve: {…}}
* type:'javascript/auto'
* userRequest:'/Users/---/lagou-edu/webpack-entry/src/index.js'
* ▶ __proto__:Object
*/
if (err) return callback(err);
// Ignored
if (!result) return callback();
// 钩子执行完毕就根据结果创建module
// createModule钩子上找有没有创建好的module
let createdModule = this.hooks.createModule.call(result);
if (!createdModule) {
if (!result.request) {
return callback(new Error("Empty dependency (no request)"));
}
// 没有的话实例化NormalModule,创建一个module
createdModule = new NormalModule(result);
}
// 将module和result交由module钩子处理
createdModule = this.hooks.module.call(createdModule, result);
// createdModule如下
// ...
// _buildHash:''
// _cachedSources:Map(0)
// _chunks:Set(0) {_sortFn: ƒ, _lastActiveSortFn: null, _cache: undefined, _cacheOrderIndependent: undefined}
// _lastSuccessfulBuildMeta:{}
// _rewriteChunkInReasons:undefined
// _source:null
// _sourceSize:null
// arguments (get):ƒ arguments() {\n\t\tthrow new Error("Module.arguments was removed, there is no replacement.");\n\t}
// arguments (set):ƒ arguments(value) {\n\t\tthrow new Error("Module.arguments was removed, there is no replacement.");\n\t}
// binary:false
// blocks:(0) []
// buildInfo:undefined
// buildMeta:undefined
// buildTimestamp:undefined
// built:false
// chunksIterable (get):ƒ chunksIterable() {\n\t\treturn this._chunks;\n\t}
// context:'/Users/---/lagou-edu/webpack-entry/src'
// debugId:1001
// dependencies:(0) []
// depth:null
// entry (get):ƒ get() {\n\t\tthrow new Error("Module.entry was removed. Use Chunk.entryModule");\n\t}
// entry (set):ƒ set() {\n\t\tthrow new Error("Module.entry was removed. Use Chunk.entryModule");\n\t}
// error:null
// ...
// 执行回调,返回创建好的module
return callback(null, createdModule);
});
});
});
to
// node_modules/webpack/lib/NormalModuleFactory.js
create(data, callback) {
...
this.hooks.beforeResolve.callAsync(
...,
(err, result) => {
...
factory(result, (err, module) => {
if (err) return callback(err);
// 依赖缓存
if (module && this.cachePredicate(module)) {
for (const d of dependencies) {
dependencyCache.set(d, module);
}
}
// 执行回调
callback(null, module);
});
}
);
}
to
// node_modules/webpack/lib/Compilation.js
_addModuleChain(context, dependency, onModule, callback) {
...
moduleFactory.create(
...,
(err, module) => {
// 如果出错就从编译队列中取出下一个执行
if (err) {
this.semaphore.release();
return errorAndCallback(new EntryModuleNotFoundError(err));
}
let afterFactory;
if (currentProfile) {
afterFactory = Date.now();
currentProfile.factory = afterFactory - start;
}
// 添加到 _modules 和 cache 中,最终返回module
// build:true
// dependencies:true
// issuer:true
// module: NormalModule{...}
const addModuleResult = this.addModule(module);
// 取出module,此时的module有可能是一个cache
module = addModuleResult.module;
// onModule 对应调用者的第三个参数:
// module => {
// this.entries.push(module);
// },
// 将module添加到entries中
onModule(module);
// 替换entry的module
// loc:{name: 'main'}
// module:NormalModule {…}
// optional:false
// request:'./src/index.js'
// type (get):ƒ type() {\n\t\treturn "single entry";\n\t}
// userRequest:'./src/index.js'
// weak:false
dependency.module = module;
// 调用Module模块的addReason方法(node_modules/webpack/lib/Module.js 第216行)
// addReason(module, dependency, explanation) {
// this.reasons.push(new ModuleReason(module, dependency, explanation));
// }
// Module模块的reasons中添加一个ModuleReason实例
// ModuleReason {module: null, dependency: SingleEntryDependency, explanation: undefined, _chunks: null}
// _chunks:null
// dependency:SingleEntryDependency {module: NormalModule, weak: false, optional: false, loc: {…}, request: './src/index.js', …}
// explanation:undefined
// module:null
module.addReason(null, dependency);
// build之后做的事情,到这里一个文件的Loader和Loader的pitch、normal都已经执行完毕,build hash生成
// 此时会判断当前module是否存在依赖,如果存在依赖就会去加载依赖
const afterBuild = () => {
if (addModuleResult.dependencies) {
// processModuleDependencies 方法就是去加载依赖
// 它会调用Compilation的addModuleDependencies方法,遍历依赖,递归加载所有依赖
// 每一个依赖是一个对象:
// dependencies:(2) [HarmonyImportSideEffectDependency, HarmonyImportSpecifierDependency]
// factory:NormalModuleFactory {_pluginCompat: SyncBailHook, hooks: {…}, resolver ...}
// dependencies里面是依赖的基本信息
// factory是NormalModule工厂类,它下面存在create方法,addModuleDependencies中通过调用factory.create就开始执行模块的处理
// 流程和加载file类似,最终回调中afterBuild会继续判断当前依赖是否存在其它依赖,然后递归加载,直到查找到所有依赖链,最终将解析结果返回
this.processModuleDependencies(module, (err) => {
if (err) return callback(err);
// 依赖加载完毕后执行最终回调至addEntry
callback(null, module);
});
} else {
return callback(null, module);
}
};
// 关于issuer请看:[https://juejin.cn/post/6955878306981871623](https://juejin.cn/post/6955878306981871623)
// 很少使用,这里忽略不计
if (addModuleResult.issuer) {
if (currentProfile) {
module.profile = currentProfile;
}
}
// 进行打包(build为true说明resolver处理、创建module的工作都已经完成,可以进入打包)
// 这里build具体流程下一篇在分析
if (addModuleResult.build) {
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();
// buildModule执行完毕执行afterBuild
afterBuild();
});
} else {
// 否则就取出编译队列的下一个执行,并等待build结束
this.semaphore.release();
this.waitForBuildingFinished(module, afterBuild);
}
}
);
}
to
// node_modules/webpack/lib/Compilation.js
addEntry(context, entry, name, callback) {
...
this._addModuleChain(
context,
entry,
(module) => {
this.entries.push(module);
},
(err, module) => {
// 如果出现错误,则调用failedEntry钩子,处理错误信息
// 然后执行回调
if (err) {
this.hooks.failedEntry.call(entry, name, err);
return callback(err);
}
// 将slot中的module信息替换为最终解析的module
if (module) {
slot.module = module;
} else {
// 如果没有返回module信息,就将其中入口文件中删除掉,最终在seal封存编译结果时会从_preparedEntrypoints中取出所有入口文件进行合并处理
const idx = this._preparedEntrypoints.indexOf(slot);
if (idx >= 0) {
this._preparedEntrypoints.splice(idx, 1);
}
}
// 调用succeedEntry钩子处理解析完毕的module
this.hooks.succeedEntry.call(entry, name, module);
// 执行回调,回到compiler的make钩子
return callback(null, module);
}
);
}
to
// node_modules/webpack/lib/Compiler.js
compile(callback) {
...
this.hooks.beforeCompile.callAsync(params, (err) => {
...
this.hooks.make.callAsync(compilation, (err) => {
if (err) return callback(err);
// 调用compilation的finish和seal方法进行代码的封存,直至最终的输出
compilation.finish((err) => {
if (err) return callback(err);
compilation.seal((err) => {
if (err) return callback(err);
this.hooks.afterCompile.callAsync(compilation, (err) => {
if (err) return callback(err);
return callback(null, compilation);
});
});
});
});
});
}
to
// node_modules/webpack/lib/Compiler.js
const onCompiled = (err, compilation) => {
if (err) return finalCallback(err);
if (this.hooks.shouldEmit.call(compilation) === false) {
...
return;
}
this.emitAssets(compilation, (err) => {
if (err) return finalCallback(err);
if (compilation.hooks.needAdditionalPass.call()) {
...
}
this.emitRecords((err) => {
...
});
});
};
到此一个 compilation module 处理的最终回调链路就结束了,后面做的事就是 seal 对 module 的封存处理,直到最终的结果输出。
整个回调最终回到了 compiler 的 make hook 中,我们总结一下整个编译流程:
-
webpack 创建 compiler 实例,调用
compiler.run开始编译 -
读取编译 records,最终回到 run 的回调中执行 compiler
-
创建 compilation 实例,执行 make 钩子开始正式编译
-
调用make的PersistentChildCompilerSingletonPlugig函数创建一个子编译,进行HTML模板的编译
-
调用make的SingleEntryPlugin钩子进行入口文件解析
-
addEntry是入口文件的开始
-
执行一个并行任务,第一个进行配置Loader和行内Loader的收集,整合成一个Loader集合,集合中存放一个个对象,包含Loader的名称和路径;第二个就是入口文件的resolve
-
执行file的doResolve,期间会执行一个个hook来对文件内容进行处理,包括路径的映射、alias的处理、文件扩展的添加、symLink的替换等等,最终就是替换掉一切非真实的文件引用关系,便于后面的依赖收集
-
Loader收集和doResolve完成后就是对Loader的分析,将Loader进行整合,分为preLoaders、postLoaders、loaders等
-
Loader最终整合完毕就去以Entry和Loader信息创建一个module
-
module创建完毕就进行build
-
build核心就是Loader的pitch和normal方法的执行,通过这两个方法对资源进行处理
-
资源被Loader处理完毕就进行最终的parse,生成AST树,迭代AST Body,对每一个node进行处理,这里会通过node的type(AST上每一个node都有一个标识符,标识这个node属于哪种类型)进入不同的分支进行解析(判断是块级、条件语句、成员方法调用、循环迭代语句、try/catch、function、class等等),创建依赖,然后将依赖添加到module的dep中,如果存在import会将import替换成文件导入,方法调用的参数也会作为一个标识符进行替换处理。。。
-
AST Body上所有的Node处理完毕就执行回调,到_addModuleChain的afterBuild中判断当前module上是否存在依赖,存在就会进行依赖加载
-
首先会在Compilation中的processModuleDependencies方法里对依赖进行整合排序(依赖的创建时都设置了index),整理出最终需要加载的依赖
-
然后就是在Compilation中addModuleDependencies方法里遍历dependencies,取出依赖的factory(NormalModuleFactory,依赖它也是一个NormalModuleFactory),然后调用NormalModuleFactory的create方法,执行factory\resolver钩子进行doResolve
-
这里的doResolve执行完毕后会回到addModuleDependencies的回调中执行afterBuild,判断依赖是否存在依赖,比如Vue就又有96个dependencies,此时就会回到Compilation中的processModuleDependencies再次就行依赖处理,执行addModuleDependencies,就这样进行依赖的递归收集解析
-
最终依赖的依赖也收集解析完毕执行回调回到_addModuleChain中,然后就到addEntry中完成module的解析