webpack5 源码详解 - 编译模块
构建模块
上一篇讲了关于webpack初始化做了哪些工作,之后会调用make hook分步骤进行处理模块。
Make
make hooks注册了EntryPlugin
,它会调用compilation.addEntry
处理入口模块
//Compiler.js
this.hooks.make.callAsync(compilation, err=> {
//...
})
//EntryPlugin.js
const { entry, options, context } = this;
const dep = EntryPlugin.createDependency(entry, options);
compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
//context是运行目录,dep是 Entrydependecy,包含name, 入口路径
compilation.addEntry(context, dep, options, err => {
callback(err);
});
});
首先会创建依赖,这个依赖叫EntryDependency,之后会将dep,options,context作为传参到compilation.addEntry
addEntry
//Compilation.js
addEntry(context, entry, optionsOrName, callback) {
//如果options不为对象就转换成对象
const options =
typeof optionsOrName === "object"
? optionsOrName
: { name: optionsOrName };
this._addEntryItem(context, entry, "dependencies", options, callback);
}
_addEntryItem(context, entry, target, options, callback) {
const { name } = options;
//如果从entries拿不到就会创建entryData
let entryData =
name !== undefined ? this.entries.get(name) : this.globalEntry;
if (entryData === undefined) {
entryData = {
dependencies: [],
includeDependencies: [],
options: {
name: undefined,
...options
}
};
//target: dependencies | includeDependencies
entryData[target].push(entry);
//保存到Map
this.entries.set(name, entryData);
} else {
//..
}
this.addModuleTree(//...);
}
因为支持MPA(multiple entry points),所以会判断有无设置过的entry。因为配置的时候是只有一个入口为entry: "./index.js"
,所以在前面处理options时会转成对象,name为"main"
const getNormalizedEntryStatic = entry => {
if (typeof entry === "string") {
return { main: { import: [entry] }
}
};
addModuleTree
保存完EntryData后就会执行addModuleTree
addModuleTree({ context, dependency, contextInfo }, callback) {
const moduleFactory = this.dependencyFactories.get(Dep);
this.handleModuleCreation(
{
factory: moduleFactory,
dependencies: [dependency],
originModule: null,
contextInfo,
context
},
(err, result) => {
//...
}
);
}
首先会拿到对应的moduleFactory,它可以为NormalModuleFactory
或ContextModuleFactory
。不同的依赖对应不同的处理方式 比如EntryDependency和ImportDependency对应NormalModuleFactory
, AMDRequireContextDependency 对应ContextModuleFactory
,它们都是之前初始化插件的时候注入到dependencyFactories
里的。
handleModuleCreation
得到对应的moduleFactory后,handleModuleCreation将正式开始处理模块了,包括后续递归处理引入的外部依赖也是调用它
handleModuleCreation(
{
factory,
dependencies,
originModule,
contextInfo,
context,
recursive = true,
connectOrigin = recursive
},
callback
) {
const moduleGraph = this.moduleGraph;
this.factorizeModule(
{
currentProfile,
factory,
dependencies,
factoryResult: true,
originModule,
contextInfo,
context
},
() => {
//...
}
)
}
factorizeModule = (
function (options, callback) {
this.factorizeQueue.add(options, callback);
}
);
moduleGraph是个实例化对象,用于保存依赖,模块之间的引用信息等,用以后续的分析。
moduleGraph: {
/** @type {WeakMap<Dependency, ModuleGraphConnection>} */
_dependencyMap:WeakMap
/** @type {Map<Module, ModuleGraphModule>} */
_moduleMap:Map(0) {size: 0}
//...
}
factorizeModule会调用this.factorizeQueue.add
,用来解析模块,它是一个AsyncQueue。 Compilation包含几个AsyncQueue用来分步骤处理模块,每一个AsyncQueue就是一个task
AsyncQueue
Compilation为模块不同的操作分别定义了异步队列,队列提供了很多方法,比如可以判断队列是否在运行,停止,清除队列等, 可以传入parallelism控制并行运行的任务数量等(用于微调性能或者让处理模块时序正确保证分析结果)。AsyncQueue是webpack5才有的代码,Commit message如下
add queues to Compilation
remove Semaphore and use AsyncQueue instead
deprecate Module.needRebuild, add Module.needBuild
remove Module.unbuild
add Module.invalidateBuild
//AsyncQueue
//处理模块内的外部依赖
this.processDependenciesQueue = new AsyncQueue({
name: "processDependencies",
parallelism: options.parallelism || 100,
processor: this._processModuleDependencies.bind(this)
});
//添加依赖到对应的moduleGraph
this.addModuleQueue = new AsyncQueue({
name: "addModule",
parent: this.processDependenciesQueue,
getKey: module => module.identifier(),
processor: this._addModule.bind(this)
});
//解析模块信息
this.factorizeQueue = new AsyncQueue({
name: "factorize",
parent: this.addModuleQueue,
processor: this._factorizeModule.bind(this)
});
//编译模块
this.buildQueue = new AsyncQueue({
name: "build",
parent: this.factorizeQueue,
processor: this._buildModule.bind(this)
});
//重构建
this.rebuildQueue = new AsyncQueue({
name: "rebuild",
parallelism: options.parallelism || 100,
processor: this._rebuildModule.bind(this)
});
this.factorizeQueue.add会调用AsyncQueue的add方法。接着创建AsyncQueueEntry保存在AsyncQueue的_entries和_queued里。当可以运行时,会执行setImmediate(root._ensureProcessing)。
//AsyncQueue.js
add(item, callback) {
if (this._stopped) return callback(new WebpackError("Queue was stopped"));
this.hooks.beforeAdd.callAsync(item, err => {
//实例化AsyncQueueEntry
const newEntry = new AsyncQueueEntry(item, callback);
if (this._stopped) {
//...
} else {
this._entries.set(key, newEntry);
this._queued.enqueue(newEntry);
const root = this._root;
root._needProcessing = true;
if (root._willEnsureProcessing === false) {
root._willEnsureProcessing = true;
setImmediate(root._ensureProcessing);
}
this.hooks.added.call(item);
}
});
}
_ensureProcessing() {
//...
this._willEnsureProcessing = false;
if (this._queued.length > 0) return;
if (this._children !== undefined) {
//_children是根据实例化AysncQueue的传参parent生成的
//_children: Array<addModuleQueue , factorizeQueue , buildQueue >
for (const child of this._children) {
while (this._activeTasks < this._parallelism) {
//将队列任务取出
const entry = child._queued.dequeue();
if (entry === undefined) break;
this._activeTasks++;
entry.state = PROCESSING_STATE;
//将entry丢进_startProcessing里
child._startProcessing(entry);
}
if (child._queued.length > 0) return;
}
}
if (!this._willEnsureProcessing) this._needProcessing = false;
}
_startProcessing(entry) {
this.hooks.beforeStart.callAsync(entry.item, err => {
if (err) {
//...
}
let inCallback = false;
try {
//Async.add 最终会调用Async._processor
this._processor(entry.item, (e, r) => {
inCallback = true;
this._handleResult(entry, e, r);
});
} catch (err) {
//...
}
this.hooks.started.call(entry.item);
});
}
当执行_ensureProcessing的时候,会循环将队列拿出来,因为我们调用的是factorizeQueue,所以第一次循环取出来的addModule的队列是空的会被跳过。所以会执行factorizeQueue
的_processor, 它是this._factorizeModule.bind(this)
factorizeModule
factorizeModule用于resolve模块信息,比如相对,绝对路径,packjson内容等。
_factorizeModule(
{
currentProfile,
factory,
dependencies,
originModule,
factoryResult,
contextInfo,
context
},
callback
) {
//因为EntryDdependency对应的是NormalModuleFactory,所以调用NormalModuleFactory.create
factory.create(
{
contextInfo: {
issuer: originModule ? originModule.nameForCondition() : "",
issuerLayer: originModule ? originModule.layer : null,
compiler: this.compiler.name,
...contextInfo
},
resolveOptions: originModule ? originModule.resolveOptions : undefined,
context: context
? context
: originModule
? originModule.context
: this.compiler.context,
dependencies: dependencies
},
(err, result) => {
//...
callback(null, factoryResult ? result : result.module);
}
);
}
第一步会先调用factory.create,然后将结果传回去,originModule表示当前模块是被谁引用的,因为处理的是入口文件,所以originModule为undefiend。所以factory.created的参数只用关注dependencies为Array<EntryDependency>
,context为程序运行目录。
NormalModuleFactory.create
create(data, callback) {
//dependencies: Array<EntryDependency>
const dependencies = /** @type {ModuleDependency[]} */ (data.dependencies);
//运行目录
const context = data.context || this.context;
//resolve配置
const resolveOptions = data.resolveOptions || EMPTY_RESOLVE_OPTIONS;
//依赖
const dependency = dependencies[0];
//request: "/index.js"
const request = dependency.request;
const assertions = dependency.assertions;
const contextInfo = data.contextInfo;
//依赖类型 如:esm
const dependencyType = (dependencies.length > 0 && dependencies[0].category) || "";
//解析数据
const resolveData = {
contextInfo,
resolveOptions,
context,
request,
assertions,
dependencies,
dependencyType,
createData: {},
cacheable: true
};
this.hooks.beforeResolve.callAsync(resolveData, (err, result) => {
//...
this.hooks.factorize.callAsync(resolveData, (err, module) => {
const factoryResult = {
module,
cacheable: resolveData.cacheable
};
callback(null, factoryResult);
});
});
}
create会初始化属性并调用钩子this.hooks.factorize.callAsync一共有两个插件。第一个是ExternalModuleFactoryPlugin
,会对external配置的模块进行处理。第二个插件的名字叫NormalModuleFactory
,他是之前实例化NormalModuleFactory时注册进来的
class NormalModuleFactory {
constructor () {
//...
//Compiler.js实例化NormalModuleFactory的时候注册
this.hooks.factorize.tapAsync(
{
name: "NormalModuleFactory",
stage: 100
},
(resolveData, callback) => {
//调用resolve钩子
this.hooks.resolve.callAsync(resolveData, (err, result) => {
//...
});
}
);
//...
this.hooks.resolve.tapAsync(...)
}
//...
}
在实例化NormalModuleFactory的时候会注册两个插件,一个在factorize
阶段,一个在resolve
阶段。factorize
钩子会调用resolve
钩子。resolve
下注册的hook代码如下
NormalModuleFactory.hooks.resolve
this.hooks.resolve.tapAsync(
{
name: "NormalModuleFactory",
stage: 100
},
(data, callback) => {
const {
contextInfo,
context,
dependencies,
dependencyType,
request,
assertions,
resolveOptions,
} = data;
//获取loader的解析器
const loaderResolver = this.getResolver("loader");
/** @type {ResourceData | undefined} */
let matchResourceData = undefined;
/** @type {string} */
let unresolvedResource;
/** @type {ParsedLoaderRequest[]} */
let elements;
let noPreAutoLoaders = false;
let noAutoLoaders = false;
let noPrePostAutoLoaders = false;
//Scheme表示为URL方案,data,file等
const contextScheme = getScheme(context);
/** @type {string | undefined} */
let scheme = getScheme(request);
if (!scheme) {
//...
}
//...
}
);
首先会对context和request调用getScheme。context是执行目录,request就是我们的入口文件/index.js
。getScheme会解析对应的字符串是否是某些URL方案。比如可能是file:///user/webpack/index.js
或者data:text/javascript;base64...
,如果是的话会丢到相应的dataUrlPlugin
或fileUrlPlugin处理
Get scheme if specifier is an absolute URL specifier
- e.g. Absolute specifiers like 'file:///user/webpack/index.js'
- https://tools.ietf.org/html/r...
继续resolve hook之后的代码
//this.hooks.resolve.tapAsync
if (!scheme) {
/** @type {string} */
let requestWithoutMatchResource = request;
//...
scheme = getScheme(requestWithoutMatchResource);
if (!scheme && !contextScheme) {
//判断是否有inline-loader
const firstChar = requestWithoutMatchResource.charCodeAt(0);
const secondChar = requestWithoutMatchResource.charCodeAt(1);
noPreAutoLoaders = firstChar === 45 && secondChar === 33; // startsWith "-!"
noAutoLoaders = noPreAutoLoaders || firstChar === 33; // startsWith "!"
noPrePostAutoLoaders = firstChar === 33 && secondChar === 33; // startsWith "!!";
const rawElements = requestWithoutMatchResource
.slice(
noPreAutoLoaders || noPrePostAutoLoaders
? 2
: noAutoLoaders
? 1
: 0
)
.split(/!+/);
unresolvedResource = rawElements.pop();
elements = rawElements.map(el => {
const { path, query } = cachedParseResourceWithoutFragment(el);
return {
loader: path,
options: query ? query.slice(1) : undefined
};
});
scheme = getScheme(unresolvedResource);
} else {
unresolvedResource = requestWithoutMatchResource;
elements = EMPTY_ELEMENTS;
}
} else {
unresolvedResource = request;
elements = EMPTY_ELEMENTS;
}
如果没有scheme,会判断是否含有inline-loader,如果有会被拆分开来。比如!style-loader!css-loader?modules!./styles.css
会拆成如下对象,然后丢进resolveRequestArray
进行相关处理
[
{
loader: 'style-loader', options: undefined
},
{
loader: 'css-loader', options: 'modules'
}
]
接着会调用defaultResolve(context)
生成模块解析器。
//this.hooks.resolve
if (scheme) {
//...
}
else if (contextScheme) {
//...
}
// resource without scheme and without path
defaultResolve(context);
const defaultResolve = context => {
if (/^($|\?)/.test(unresolvedResource)) {
//...
}
// resource without scheme and with path
else {
//创建解析器
const normalResolver = this.getResolver(
"normal",
//dependencyType: "esm"
dependencyType
? cachedSetProperty(
resolveOptions || EMPTY_RESOLVE_OPTIONS,
"dependencyType",
dependencyType
)
: resolveOptions
);
this.resolveResource(
contextInfo,
context,
unresolvedResource,
normalResolver,
resolveContext,
(err, resolvedResource, resolvedResourceResolveData) => {
//...
continueCallback();
});
}
};
this.getResolver
会调用ResolverFactory生成解析器,解析器用于解析文件绝对路径等信息。
ResolverFactory内部的功能扩展于enhanced-resolve。enhanced-resolve是一个高度可配置的resolve库,比如我们经常配置resolve.alias用别名代替某些路径,配置resolve.extensions用于扩展名等。
根据getResolver传进来的参数会实例化enhanced-resolve并调用resolverFactory的钩子合并options
//WebpackOptionsApply.js
//getResolver('normal', 'esm')会生成Resolver,然后触发hooks进行合并用户配置的options.resolve
compiler.resolverFactory.hooks.resolveOptions
.for("normal")
.tap("WebpackOptionsApply", resolveOptions => {
resolveOptions = cleverMerge(options.resolve, resolveOptions);
resolveOptions.fileSystem = compiler.inputFileSystem;
return resolveOptions;
});
compiler.resolverFactory.hooks.resolveOptions
.for("context")
.tap("WebpackOptionsApply", resolveOptions => {
//...
});
compiler.resolverFactory.hooks.resolveOptions
.for("loader")
.tap("WebpackOptionsApply", resolveOptions => {
//...
});
接着调用this.resolveResource
,它会调用刚刚生成的resolver解析模块
resolveResource(
contextInfo,
context,
unresolvedResource,
resolver,
resolveContext,
callback
) {
resolver.resolve(
contextInfo,
context,
unresolvedResource,
resolveContext,
(err, resolvedResource, resolvedResourceResolveData) => {
if (err) {
//...
}
callback(err, resolvedResource, resolvedResourceResolveData);
}
);
}
resolver.resolve会得到resolvedResource和resolvedResourceResolveData。
resolvedResource为c:\\Users\\Administrator\\Desktop\\webpack\\index.js
,是执行webpack的入口文件的绝对路径。resolvedResourceResolveData会包含一些解析相关信息。
接着回到this.resolveResource回调执行continueCallback
函数
const continueCallback = needCalls(2, err => {
//...
const settings = {};
const useLoadersPost = [];
const useLoaders = [];
const useLoadersPre = [];
// handle .webpack[] suffix
let resource;
let match;
if (
//...
) {
//...
} else {
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,
//url方案
scheme,
assertions,
//模块的mimetype
mimetype: matchResourceData
? ""
: resourceData.data.mimetype || "",
//依赖类型
dependency: dependencyType,
//模块的描述文件,列如packjson.js
descriptionData: matchResourceData
? undefined
: resourceData.data.descriptionFileData,
//模块发起者
issuer: contextInfo.issuer,
compiler: contextInfo.compiler,
issuerLayer: contextInfo.issuerLayer || ""
});
//loader类型
for (const r of result) {
if (r.type === "use") {
if (!noAutoLoaders && !noPrePostAutoLoaders) {
useLoaders.push(r.value);
}
} else if (r.type === "use-post") {
if (!noPrePostAutoLoaders) {
useLoadersPost.push(r.value);
}
} else if (r.type === "use-pre") {
//...
} else if (
//...
) {
//...
}
}
}
let postLoaders, normalLoaders, preLoaders;
//...
this.resolveRequestArray(
//...
useLoadersPost,
(err, result) => {
//...
}
);
this.resolveRequestArray(
contextInfo,
this.context,
useLoaders,
loaderResolver,
resolveContext,
(err, result) => {
normalLoaders = result;
continueCallback(err);
}
);
this.resolveRequestArray(
//...
(err, result) => {
//...
}
);
//
defaultResolve = () {
//...
}
});
首先会执行this.ruleSet.exec
,之前的初始化篇讲过它是用来根据资源输出对应匹配的loader。解析出来的结果有三种类型,use
,use-post
和use-pre
,然后放置在对应的useLoadersPost
, useLoaders
和useLoadersPre
。
在webpack1的时候有相应的配置,可以在loader处理之前或之后执行其他 预/后 处理loader。但是在v2后就被移除了。
之后会对每个loaders Array进行resolveRequestArray处理,因为没有preLoader和postLoader,所以我们只用关注中间的resolveRequestArray
resolveRequestArray(
contextInfo,
context,
array,
resolver,
resolveContext,
callback
) {
if (array.length === 0) return callback(null, array);
//异步库
asyncLib.map(
array,
(item, callback) => {
//调用解析器的resolve方法
resolver.resolve(
contextInfo,
context,
item.loader,
resolveContext,
(err, result) => {
//...
const parsedResult = this._parseResourceWithoutFragment(result);
const resolved = {
loader: parsedResult.path,
options:
item.options === undefined
? parsedResult.query
? parsedResult.query.slice(1)
: undefined
: item.options,
ident: item.options === undefined ? undefined : item.ident
};
return callback(null, resolved);
}
);
},
callback
);
}
async是一个异步库,map方法会循环array,然后把每个item给resolver.resolve执行,最后调用callback。resolver是之前调用的 this.getResolver("loader")
创建的LoaderResolver,resolver解析loader相应的绝对路径,比如传进去的为babel-loader
, 出来的就是node_modules包里入口地址C:\\Users\\Administrator\\Desktop\\webpack\\node_modules\\.pnpm\\babel-loader@8.2.3_ed870ac3ba52c4ec230ba2bc3dbb311c\\node_modules\\babel-loader\\lib\\index.js
。然后将结果回调给continueCallback
const continueCallback = needCalls(3, err => {
if (err) {
return callback(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;
if (layer !== undefined && !layers) {
return callback(
new Error(
"'Rule.layer' is only allowed when 'experiments.layers' is enabled"
)
);
}
try {
Object.assign(data.createData, {
layer: layer === undefined ? contextInfo.issuerLayer || null : layer, //资源发起者
request: stringifyLoadersAndResource(allLoaders, resourceData.resource), //loader绝对路径和模块资源绝对路径
userRequest, //用户请求绝对路径
rawRequest: request, //未经处理的请求路径
loaders: allLoaders, //资源所需要的所有loader
resource: resourceData.resource, //资源绝对路径
context: resourceData.context || getContext(resourceData.resource),
matchResource: matchResourceData ? matchResourceData.resource : undefined,
resourceResolveData: resourceData.data, //enhance-resolve返回的数据
settings,
type, //资源类型
parser: this.getParser(type, settings.parser), //根据类型和设置返回的Parser
parserOptions: settings.parser,
generator: this.getGenerator(type, settings.generator), //根据类型和设置返回的generator
generatorOptions: settings.generator,
resolveOptions
});
} catch (e) {
return callback(e);
}
callback();
});
continueCallback会整合所有之前resolve的数据(资源信息,loader信息)到createData
createData还会初始化对应的parser和generator,对于不同的type会有不同的parser和generator,用于webpack处理不同的资源和输出不同代码
Parser
JavascriptParser
- javascript/auto
- javascript/esm
- javascript/dynamic
AssetParser
- asset
- asset/inline
- asset/resource
- asset/source
JsonParser
WebAssemblyParser
CssParser
Generator
- JavascriptGenerator
- JsonGenerator
- WebAssemblyGenerator
- CssGenerator
webpack5也允许对Parser和Generator进行配置以获得某些功能或者更改输出配置
NormalModuleFactory.hooks.factorize
至此,resolve hooks的工作都做完了,将会回到factorize hooks。
// this.hooks.factorize.tapAsync
this.hooks.resolve.callAsync(resolveData, (err, result) => {
//resolveData: createData
//...
this.hooks.afterResolve.callAsync(resolveData, (err, result) => {
if (err) return callback(err);
//enhanse-resolve解析的数据
const createData = resolveData.createData;
this.hooks.createModule.callAsync(
createData,
resolveData,
(err, createdModule) => {
//...
//实例化NormalModule
createdModule = new NormalModule(
/** @type {NormalModuleCreateData} */ (createData)
);
}
//调用插件判断是否配置sideEffects
createdModule = this.hooks.module.call(
createdModule,
createData,
resolveData
);
return callback(null, createdModule);
}
);
});
});
之后便会根据createData实例化NormalModule,这样NormalModule就保存了当前模块的解析信息。一个模块会对应一个NormalModule,之后模块相关的操作都会在NormalModule进行。
this.hooks.module.call会有两个插件 ,一个判断packjson的有无sideEffects 配置,一个判断Rules有无sideEffects 配置。对模块设置sideEffects能够更友好的让webpack进行tree shaking
// SideEffectsFlagPlugin
//package.json存在sideEffects就进行相关设置
(module, data) => {
//resolve数据
const resolveData = data.resourceResolveData;
if (
resolveData &&
resolveData.descriptionFileData &&
resolveData.relativePath
) {
//packjson.sideEffects
const sideEffects = resolveData.descriptionFileData.sideEffects;
if (sideEffects !== undefined) {
if (module.factoryMeta === undefined) {
module.factoryMeta = {};
}
const hasSideEffects =
SideEffectsFlagPlugin.moduleHasSideEffects(
resolveData.relativePath,
sideEffects,
cache
);
module.factoryMeta.sideEffectFree = !hasSideEffects;
}
}
return module;
}
//Rules里包含sideEffects就进行设置
(module, data) => {
if (typeof data.settings.sideEffects === "boolean") {
if (module.factoryMeta === undefined) {
module.factoryMeta = {};
}
module.factoryMeta.sideEffectFree = !data.settings.sideEffects;
}
return module;
}
到这里,整个模块相关信息就解析完了, 将会开始回收栈,把结果返回给this.hooks.factorize,回到factory.create,再回到_startProcessing里的_processor
_startProcessing(entry) {
this.hooks.beforeStart.callAsync(entry.item, err => {
//...
try {
this._processor(entry.item, (e, r) => {
inCallback = true;
//回到_processor回调执行_handleResult
this._handleResult(entry, e, r);
});
} catch (err) {
//...
}
});
}
_handleResult(entry, err, result) {
this.hooks.result.callAsync(entry.item, err, result, hookError => {
const error = hookError
? makeWebpackError(hookError, `AsyncQueue(${this._name}).hooks.result`)
: err;
const callback = entry.callback;
const callbacks = entry.callbacks;
entry.state = DONE_STATE;
entry.callback = undefined;
entry.callbacks = undefined;
entry.result = result;
entry.error = error;
//...
//执行callback
callback(error, result);
inHandleResult--;
});
}
_handleResult会将一些属性赋值给AsyncQueueEntry对象并执行callback,还记得最初是由this.factorizeModule调用的吗
handleModuleCreation () {
//...
this.factorizeModule(
{
currentProfile,
factory,
dependencies,
factoryResult: true,
originModule,
contextInfo,
context
},
(err, factoryResult) => {
//...
const newModule = factoryResult.module;
//...
this.addModule(newModule, (err, module) => {
//...
}
}
);
它将会用resolveData创建的NormalModule丢进this.addModule处理,
addModule AsyncQueue
this.addModule和this.factorizeModule一样也是AsyncQueue。
addModule(module, callback) {
this.addModuleQueue.add(module, callback);
}
_addModule(module, callback) {
const identifier = module.identifier();
//...
this._modulesCache.get(identifier, null, (err, cacheModule) => {
//...
this._modules.set(identifier, module);
this.modules.add(module);
if (this._backCompat)
ModuleGraph.setModuleGraphForModule(module, this.moduleGraph);
if (currentProfile !== undefined) {
currentProfile.markIntegrationEnd();
}
callback(null, module);
});
}
//ModuleGraph.js
const moduleGraphForModuleMap = new WeakMap();
static setModuleGraphForModule(module, moduleGraph) {
moduleGraphForModuleMap.set(module, moduleGraph);
}
首先会获取模块的identifier,它会拼凑模块的type,request和layer。然后将module和对应的moduleGraph加入到map里。然后执行的this.addModule的回调
this.addModule(newModule, (err, module) => {
if (...) {
//...
} else {
//...
for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];
moduleGraph.setResolvedModule(
connectOrigin ? originModule : null,
dependency,
module
);
}
}
//...
});
首先会循环dependencies并将当前module和依赖丢进setResolvedModule,originModule是父模块,比如index.js
从module_c.js
引入了个变量,module_c
的originModule就是index
。
ModuleGraph
setResolvedModule会执行一些ModuleGraph操作
ModuleGraph.js
里会包含ModuleGraph、ModuleGraphConnection、ModuleGraphModule,它们会收集模块间的依赖关系,并为后续处理提供信息
ModuleGraph
- _dependencyMap
- _moduleMap
ModuleGraph会被实例化,然后赋值到Compilation对象里,之后只要调用Compilation.ModuleGraph就能进行相关操作。
ModuleGraph有两个重要的Map,_dependencyMap
和_moduleMap
。
_dependencyMap接收Dependency为键,ModuleGraphConnection为值,Dependency比如为EntryDependency,ImportDependency等。
_moduleMap接收Module为键,ModuleGraphModule为值 , Module就是我们resloveData创建的NormalModule。
对于Map数据结构,之后就可以通过Module和Dependency对象直接方便找到对应的相关ModuleGraphConnection或ModuleGraphModule信息
ModuleGraphConnection
- originModule;
- dependency;
- module
- weak
ModuleGraphConnection保存引用信息,当前模块,父模块,依赖等。weak表示为弱依赖,用于处理一些SSR场景
ModuleGraphModule
- incomingConnections
- outgoingConnections
- issuer
- exports
incomingConnections保存模块的被引用集合,outgoingConnections保存模块的引用集合,集合对象都是ModuleGraphConnection。
ModuleGraphModule还保存着其他的信息,比如ExportsInfo,之后会用于保存模块的导出信息,并在优化的时候分析是否被使用来实现tree shaking
回到addModule
this.addModule(newModule, (err, module) => {
if (...) {
//...
} else {
//...
for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];
moduleGraph.setResolvedModule(
connectOrigin ? originModule : null,
dependency,
module
);
}
}
//...
});
它会循环每个依赖并调用setResolvedModule
//ModuleGraph.js
setResolvedModule(originModule, dependency, module) {
//实例化ModuleGraphConnection
const connection = new ModuleGraphConnection(
originModule,
dependency,
module,
undefined,
dependency.weak,
dependency.getCondition(this)
);
//获取模块对应ModuleGraphModule的incomingConnections
const connections = this._getModuleGraphModule(module).incomingConnections;
connections.add(connection);
//保存在outgoingConnections或_dependencyMap
if (originModule) {
const mgm = this._getModuleGraphModule(originModule);
if (mgm._unassignedConnections === undefined) {
mgm._unassignedConnections = [];
}
mgm._unassignedConnections.push(connection);
if (mgm.outgoingConnections === undefined) {
mgm.outgoingConnections = new SortableSet();
}
mgm.outgoingConnections.add(connection);
} else {
this._dependencyMap.set(dependency, connection);
}
}
setResolvedModule会通过当前module新建ModuleGraphConnection实例,并保存在module对应的ModuleGraphModule的incomingConnections里。
之后如果有父模块就保存在outgoingConnections里,否则在_dependencyMap里保存connection。
最后会执行this.addmodule里的_handleModuleBuildAndDependencies,开始处理模块内容。
this.addModule(newModule, (err, module) => {
//...
this._handleModuleBuildAndDependencies(
originModule,
module,
recursive,
callback
);
});
_handleModuleBuildAndDependencies(originModule, module, recursive, callback) {
//...
this.buildModule(module, err => {
//...
callback()
});
}
//buildModule的processor
_buildModule(module, callback) {
//...
module.needBuild(
{
compilation: this,
fileSystemInfo: this.fileSystemInfo,
valueCacheVersions: this.valueCacheVersions
},
(err, needBuild) => {
//...
//调用module.build构建模块
module.build(
this.options,
this,
this.resolverFactory.get("normal", module.resolveOptions),
this.inputFileSystem,
err => {
//...
}
);
}
);
}
buildModule AsyncQueue
this.buildModule同样也是一个AsyncQueue,它最终会调用Module.build开始构建模块
//normalModule.js
build(options, compilation, resolver, fs, callback) {
//初始化属性
this._forceBuild = false;
this._source = null;
if (this._sourceSizes !== undefined) this._sourceSizes.clear();
this._sourceTypes = undefined;
this._ast = null;
this.error = null;
this.clearWarningsAndErrors();
this.clearDependenciesAndBlocks();
this.buildMeta = {};
this.buildInfo = {
cacheable: false,
parsed: true,
buildDependencies: undefined,
valueDependencies: undefined,
hash: undefined,
assets: undefined,
assetsInfo: undefined
};
const startTime = compilation.compiler.fsStartTime || Date.now();
const hooks = NormalModule.getCompilationHooks(compilation);
return this._doBuild(options, compilation, resolver, fs, hooks, err => {
//...
});
}
module.build会初始化构建信息,源码,AST,buildInfo 等,然后调用_doBuild
_doBuild(options, compilation, resolver, fs, hooks, callback) {
//获取loader上下文
const loaderContext = this._createLoaderContext(
resolver,
options,
compilation,
fs,
hooks
);
//...
//调用loader-runner执行loaders
runLoaders(
{
//模块路径
resource: this.resource,
//loaders
loaders: this.loaders,
//loader上下文
context: loaderContext,
//处理资源的函数
processResource: (loaderContext, resourcePath, callback) => {
const resource = loaderContext.resource;
const scheme = getScheme(resource);
//根据不同的方案读文件
hooks.readResource
.for(scheme)
.callAsync(loaderContext, (err, result) => {
if (err) return callback(err);
if (typeof result !== "string" && !result) {
return callback(new UnhandledSchemeError(scheme, resource));
}
return callback(null, result);
});
}
},
(err, result) => {
//...
//将loader添加进构建信息
for (const loader of this.loaders) {
this.buildInfo.buildDependencies.add(loader.loader);
}
this.buildInfo.cacheable = this.buildInfo.cacheable && result.cacheable;
processResult(err, result.result);
}
);
}
_doBuild首先会创建loaderContext,用于runLoaders的参数,runLoaders来自loader-runner,这是webpack用于运行Loaders的库。
processResource用于该怎么读资源给Loader,因为我们的入口文件是"/indx.js",所以readResource hook会拿出FileUriPlugin
,并进行fs.readFile,将文件Buffer取出。
runLoaders会将result返回,它包含resourceBuffer和result,如果有相应的loader,result将会转换成js可操作的字符串。
下图是使用babel-loader生成的结果
最后会将结果传给processResult进行处理
const processResult = (err, result) => {
//...
const source = result[0];
const sourceMap = result.length >= 1 ? result[1] : null;
const extraInfo = result.length >= 2 ? result[2] : null;
//如果不是Buffer或者字符串就报错
if (!Buffer.isBuffer(source) && typeof source !== "string") {
const currentLoader = this.getCurrentLoader(loaderContext, 0);
const err = new Error(
`Final loader (${
currentLoader
? compilation.runtimeTemplate.requestShortener.shorten(
currentLoader.loader
)
: "unknown"
}) didn't return a Buffer or String`
);
const error = new ModuleBuildError(err);
return callback(error);
}
//生成sourceMap
this._source = this.createSource(
options.context,
//如果是二进制就转换成Buffer,否则字符串化
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();
};
processResult会要求loader必须返回Buffer或者String,然后会调用createSrouce对result处理,createSource会调用webpack-sources库,能够生成有或者无sourceMap的源码。
最后初始化AST,调用callback,callback会回到_doBuild。
return this._doBuild(options, compilation, resolver, fs, hooks, err => {
// if we have an error mark module as failed and exit
if (err) {
//...
}
//...
let result;
try {
const source = this._source.source();
//调用parser转换成AST
result = this.parser.parse(this._ast || source, {
source,
current: this,
module: this,
compilation: compilation,
options: options
});
}
//...
});
_doBuild会把结果进行解析,还记得吗,这里的parser是在resolve阶段生成的,因为当前模块是js资源所以生成的是JavascriptParser。
JavascriptParser
parse(source, state) {
//ast
let ast;
//注释
let comments;
const semicolons = new Set();
if (source === null) {
throw new Error("source must not be null");
}
if (Buffer.isBuffer(source)) {
source = source.toString("utf-8");
}
if (typeof source === "object") {
ast = /** @type {ProgramNode} */ (source);
comments = source.comments;
} else {
comments = [];
//调用acorn生成ast
ast = JavascriptParser._parse(source, {
sourceType: this.sourceType,
onComment: comments,
onInsertedSemicolon: pos => semicolons.add(pos)
});
}
const oldScope = this.scope;
const oldState = this.state;
const oldComments = this.comments;
const oldSemicolons = this.semicolons;
const oldStatementPath = this.statementPath;
const oldPrevStatement = this.prevStatement;
//初始化作用域
this.scope = {
topLevelScope: true,
inTry: false,
inShorthand: false,
isStrict: false,
isAsmJs: false,
definitions: new StackedMap()
};
/** @type {ParserState} */
this.state = state;
this.comments = comments;
this.semicolons = semicolons;
this.statementPath = [];
this.prevStatement = undefined;
if (this.hooks.program.call(ast, comments) === undefined) {
//判断js的模式
this.detectMode(ast.body);
//遍历topLevelScope的变量并声明
this.preWalkStatements(ast.body);
this.prevStatement = undefined;
this.blockPreWalkStatements(ast.body);
this.walkStatements(ast.body);
}
this.hooks.finish.call(ast, comments);
this.scope = oldScope;
/** @type {ParserState} */
this.state = oldState;
this.comments = oldComments;
this.semicolons = oldSemicolons;
this.statementPath = oldStatementPath;
this.prevStatement = oldPrevStatement;
return state;
}
JavascriptParser._parse函数用于解析js,在函数里会用acorn作为parser,然后转换成AST(抽象语法树),对于AST(抽象语法书)的树结构可以去astexplorer探索。
this.detectMode用于去检测是否含有 "use strict"
或"use asm"
,"use strict"
代表当前为严格模式,"use asm"
表示是使用asm.js编译出来的,帮助浏览器优化性能。
preWalkStatements
preWalkStatements用于迭代声明变量的范围
preWalkStatements(statements) {
for (let index = 0, len = statements.length; index < len; index++) {
const statement = statements[index];
this.preWalkStatement(statement);
}
}
preWalkStatement(statement) {
this.statementPath.push(statement);
if (this.hooks.preStatement.call(statement)) {
this.prevStatement = this.statementPath.pop();
return;
}
switch (statement.type) {
case "BlockStatement":
this.preWalkBlockStatement(statement);
break;
case "DoWhileStatement":
this.preWalkDoWhileStatement(statement);
break;
case "ForInStatement":
this.preWalkForInStatement(statement);
break;
case "ForOfStatement":
this.preWalkForOfStatement(statement);
break;
case "IfStatement":
this.preWalkIfStatement(statement);
break;
case "TryStatement":
this.preWalkTryStatement(statement);
break;
case "VariableDeclaration":
this.preWalkVariableDeclaration(statement);
break;
//...
}
this.prevStatement = this.statementPath.pop();
}
preWalkStatement会遍历AST body,如果遇到最外层的变量声明,就定义变量到this.scope.definitions。definitions是个map结构,name为key,this.scope为值。
比如遇到var a = 2
, 就符合VariableDeclaration
的case,然后将"a"保存到definitions map里,对应的this.scope的topLevelScope为true。当然如果在if,for等block是用let声明的,那么会跳过,后续再定义scope,因为它们不属于topLevelScope。
blockPreWalkStatements
接着就是调用blockPreWalkStatements
blockPreWalkStatements(statements) {
for (let index = 0, len = statements.length; index < len; index++) {
const statement = statements[index];
this.blockPreWalkStatement(statement);
}
}
blockPreWalkStatement(statement) {
this.statementPath.push(statement);
if (this.hooks.blockPreStatement.call(statement)) {
this.prevStatement = this.statementPath.pop();
return;
}
switch (statement.type) {
case "ImportDeclaration":
this.blockPreWalkImportDeclaration(statement);
break;
case "ExportAllDeclaration":
this.blockPreWalkExportAllDeclaration(statement);
break;
case "ExportDefaultDeclaration":
this.blockPreWalkExportDefaultDeclaration(statement);
break;
case "ExportNamedDeclaration":
this.blockPreWalkExportNamedDeclaration(statement);
break;
case "VariableDeclaration":
this.blockPreWalkVariableDeclaration(statement);
break;
case "ClassDeclaration":
this.blockPreWalkClassDeclaration(statement);
break;
}
this.prevStatement = this.statementPath.pop();
}
在blockPreWalkStatements里,如果遇到的声明是ImportDeclaration的时候会调用blockPreWalkImportDeclaration。比如import some form "some"
blockPreWalkImportDeclaration(statement) {
//获取module name
const source = statement.source.value;
//添加HarmonyImportSideEffectDependency依赖
this.hooks.import.call(statement, source);
//根据不同的import type 定义 VariableInfo
switch (specifier.type) {
case "ImportDefaultSpecifier":
if (
!this.hooks.importSpecifier.call(statement, source, "default", name)
) {
this.defineVariable(name);
}
break;
case "ImportSpecifier":
if (
!this.hooks.importSpecifier.call(statement,source,specifier.imported.name,name)
) {
this.defineVariable(name);
}
break;
case "ImportNamespaceSpecifier":
if (!this.hooks.importSpecifier.call(statement, source, null, name)) {
this.defineVariable(name);
}
break;
default:
this.defineVariable(name);
}
//...
}
//impor钩子注入的回调
parser.hooks.import.tap(
"HarmonyImportDependencyParserPlugin",
(statement, source) => {
parser.state.lastHarmonyImportOrder =
(parser.state.lastHarmonyImportOrder || 0) + 1;
//创建ConstDependency
const clearDep = new ConstDependency(
parser.isAsiPosition(statement.range[0]) ? ";" : "",
statement.range
);
clearDep.loc = statement.loc;
parser.state.module.addPresentationalDependency(clearDep);
parser.unsetAsiPosition(statement.range[1]);
const assertions = getAssertions(statement);
const sideEffectDep = new HarmonyImportSideEffectDependency(
source,
parser.state.lastHarmonyImportOrder,
assertions
);
//设置代码位置
sideEffectDep.loc = statement.loc;
parser.state.module.addDependency(sideEffectDep);
return true;
}
);
blockPreWalkImportDeclaration先会获取module名字,如import { some } form "loadsh"
, source.value就为loadsh。然后会调用parser的import hook。
HarmonyImportDependencyParserPlugin做的事情就是创建HarmonyImportSideEffectDependency并收集到模块的Dependencies数组里。
HarmonyImportSideEffectDependency是用于生成最后的import代码。比如
import { c_var } from "./module_c"
//生成如下代码
/* harmony import */ var _module_c__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./module_c */ "./module_c.js");
之后会根据不同的Import specifier给importSpecifier hooks传不同的参数,该钩子的插件会设置import变量的scope,跟平常变量不同的是,它会创建VariableInfo对象set到this.scope.definitions。VariableInfo除了包含作用域,还包含模块名字等信息。
如果遇到的是export,也会执行上述操作,不同的地方在于会创建不同的Dependency。
walkStatements
walkStatement(statement) {
this.statementPath.push(statement);
if (this.hooks.statement.call(statement) !== undefined) {
this.prevStatement = this.statementPath.pop();
return;
}
switch (statement.type) {
case "BlockStatement":
this.walkBlockStatement(statement);
break;
case "ClassDeclaration":
this.walkClassDeclaration(statement);
break;
case "ExportDefaultDeclaration":
this.walkExportDefaultDeclaration(statement);
break;
case "ExportNamedDeclaration":
this.walkExportNamedDeclaration(statement);
break;
case "ExpressionStatement":
this.walkExpressionStatement(statement);
break;
case "ForStatement":
this.walkForStatement(statement);
break;
case "FunctionDeclaration":
this.walkFunctionDeclaration(statement);
break;
case "SwitchStatement":
this.walkSwitchStatement(statement);
break;
case "VariableDeclaration":
this.walkVariableDeclaration(statement);
break;
//...
}
this.prevStatement = this.statementPath.pop();
}
walkStatements除了会遍历块内变量并定义对应scope,还会做其他事情。
webpack很聪明,虽然有import,但如果没有使用的话,也不会去请求依赖,除非有变量引用了它。
当遇到VariableDeclaration
,会从scope拿出这个变量引用信息,如果取出来的是VariableInfo
,说明是import的依赖,就会调用相关插件创建HarmonyImportSpecifierDependency
并添加到module.dependencies
如果遇到FunctionDeclaration
,并且为ImportExpression
。说明是个dynamic-imports,然后会调用ImportParserPlugin
插件进行处理。
为了支持magic comments,ImportParserPlugin
会解析出comments,并对其进行相关设置。之后会创建AsyncDependenciesBlock
,平常的import会添加到module.dependencies,但是动态导入是添加到module.block,而且处理优先级也在最后。
到这里,buildModule的事情就基本完成了,将会回到buildModule回调。
this.buildModule(module, err => {
//...
this.processModuleDependencies(module, err => {
if (err) {
return callback(err);
}
callback(null, module);
});
});
processModuleDependencies AsyncQueue
在buildModule里会调用processModuleDependencies
,它是个AsyncQueue,processor为_processModuleDependencies
_processModuleDependencies(module, callback) {
/** @type {Array<{factory: ModuleFactory, dependencies: Dependency[], originModule: Module|null}>} */
const sortedDependencies = [];
/** @type {DependenciesBlock} */
let currentBlock;
/** @type {Map<ModuleFactory, Map<string, Dependency[]>>} */
let dependencies;
//...
try {
/** @type {DependenciesBlock[]} */
const queue = [module];
do {
const block = queue.pop();
if (block.dependencies) {
currentBlock = block;
let i = 0;
for (const dep of block.dependencies) processDependency(dep, i++);
}
if (block.blocks) {
for (const b of block.blocks) queue.push(b);
}
} while (queue.length !== 0);
} catch (e) {
return callback(e);
}
if (--inProgressSorting === 0) onDependenciesSorted();
}
_processModuleDependencies会循环模块的dependencies和block。前面说过dependencies里是正常的import,block里是Dynamic import,它们都会经过processDependency进行处理。
因为在parser的时候添加HarmonyImportSpecifierDependency
依赖是根据代码引用位置,在dependencies里是乱序的。所以processDependency的作用就是根据import前后位置进行排序,然后将相同模块的几个依赖放在一起。
排序完后调用onDependenciesSorted处理所有外部依赖
const onDependenciesSorted = err => {
if (err) return callback(err);
// early exit without changing parallelism back and forth
if (sortedDependencies.length === 0 && inProgressTransitive === 1) {
return callback();
}
// This is nested so we need to allow one additional task
this.processDependenciesQueue.increaseParallelism();
for (const item of sortedDependencies) {
inProgressTransitive++;
this.handleModuleCreation(item, err => {
// In V8, the Error objects keep a reference to the functions on the stack. These warnings &
// errors are created inside closures that keep a reference to the Compilation, so errors are
// leaking the Compilation object.
if (err && this.bail) {
if (inProgressTransitive <= 0) return;
inProgressTransitive = -1;
// eslint-disable-next-line no-self-assign
err.stack = err.stack;
onTransitiveTasksFinished(err);
return;
}
if (--inProgressTransitive === 0) onTransitiveTasksFinished();
});
}
if (--inProgressTransitive === 0) onTransitiveTasksFinished();
};
onDependenciesSorted会循环依赖,并执行handleModuleCreation,这个函数不就是最开始处理入口模块的吗?是的,从开头的handleModuleCreation到这就是webpack递归处理所有引入模块的过程。
当所有依赖都处理完后就会从processDependenciesQueue往后回收栈,回到buildQueue,factorizeModule,回到最初调用handleModuleCreation的地方
addModuleTree({ context, dependency, contextInfo }, callback) {
//...
this.handleModuleCreation(
(//...)
,
(err, result) => {
if (err && this.bail) {
callback(err);
this.buildQueue.stop();
this.rebuildQueue.stop();
this.processDependenciesQueue.stop();
this.factorizeQueue.stop();
} else if (!err && result) {
callback(null, result);
} else {
callback();
}
}
);
}
handleModuleCreation会停止所有任务队列并回调到make hook
finishMake
//compiler.js
compile(callback) {
//...
err => {
if (err) return callback(err);
//...
logger.time("make hook");
this.hooks.make.callAsync(compilation, err => {
logger.timeEnd("make hook");
if (err) return callback(err);
logger.time("finish make hook");
this.hooks.finishMake.callAsync(compilation, err => {
logger.timeEnd("finish make hook");
if (err) return callback(err);
process.nextTick(() => {
logger.time("finish compilation");
compilation.finish(err => {
logger.timeEnd("finish compilation");
if (err) return callback(err);
logger.time("seal compilation");
compilation.seal(err => {
//...
});
});
});
});
});
});
make hooks之后是finishMake hooks,在这里面会调用compilation.finish。
compilation.finish里大部分代码都在输出日志,但里面有个finishModules hooks里有个重要的插件flagDependencyExportsPlugin
。它会将所有模块的export信息添加到NormalModule对应的ModuleGraphModule里,之后在优化阶段的时候用于分析是否被使用,是否需要tree shaking。
到这里make的所有环节就结束了
总结
总的来说make环节主要就是在处理模块,webpack将处理模块分为几个步骤。
factorize会调用resolver去解析模块和loader的路径相关信息,并生成对应的paser和generator。
addModule会设置模块对应的ModuleGraph,ModuleGraph包含模块,外部依赖的引用信息,导出信息等。
buildModule会通过Loader-runner执行loaders,将资源转换成js能操作的目标,然后将源码paser成AST,并遍历AST Body,对不同的声明进行处理,在此期间会收集外部依赖,导出信息等。
processModuleDependencies会处理所有收集的依赖,并将依赖回到factorize,从而实现递归处理所有的模块
- 原文: https://github.com/Hazlank/blog/issues/17 ,欢迎star