本文内容基于
webpack 5.74.0
版本进行剖析因为
webpack5
整体代码过于简单,为了缩小复杂度,本文所有剖析将只基于js
文件类型进行剖析,不会对其它类型(css
、image
)进行剖析,所举的例子也都是基于js
类型为了减少可读性,会对源码进行删减、调整程序、扭转的操作,文中所有源码均可视作为伪代码,并且文章默认读者曾经把握
tapable
、loader
、plugin
等基础知识,对文章中呈现asyncQueue
、tapable
、loader
、plugin
相干代码都会间接展现,不会减少过多阐明
文章内容
- 从
npm run build
命令开始,剖析webpack
入口文件的源码执行流程,剖析是如何执行到make
阶段和seal
阶段 - 剖析
make
阶段的factorizeModule()
的执行流程 - 剖析
make
阶段的buildMode()
的执行流程 - 剖析
make
阶段的processModuleDependencies()
的执行流程
1. 初始化
1.1 npm run build
1.1.1 源码剖析
当咱们执行 npm run build
的时候,理论就是执行bin/webpack.js
{
"scripts": {
"build-debugger": "node --inspect-brk ./node_modules/webpack/bin/webpack.js --config webpack.config.js --progress",
"build": "webpack"
}
}
在 bin/webpack.js
,最终会加载webpack-cli/package.json
的bin
字段,也就是./bin/cli.js
const cli = {
name: "webpack-cli",
package: "webpack-cli",
binName: "webpack-cli",
installed: isInstalled("webpack-cli"),
url: "https://github.com/webpack/webpack-cli"
};
const runCli = cli => {const path = require("path");
const pkgPath = require.resolve(`${cli.package}/package.json`);
// eslint-disable-next-line node/no-missing-require
const pkg = require(pkgPath);
// eslint-disable-next-line node/no-missing-require
require(path.resolve(path.dirname(pkgPath), pkg.bin[cli.binName]));
};
runCli(cli);
// webpack-cli/package.json
"bin": {"webpack-cli": "./bin/cli.js"}
在 webpack-cli/bin/cli.js
中,触发了 new WebpackCLI()
和run()
办法
// webpack-cli/bin/cli.js
const runCLI = require("../lib/bootstrap");
runCLI(process.argv);
// webpack-cli/lib/bootstrap.js
const WebpackCLI = require("./webpack-cli");
const runCLI = async (args) => {
// Create a new instance of the CLI object
const cli = new WebpackCLI();
try {await cli.run(args);
}
catch (error) {cli.logger.error(error);
process.exit(2);
}
};
cli=new WebpackCLI()
和cli.run()
办法就十分绕了
上面执行的流程能够概括为:
await this.program.parseAsync(args, parseOptions)
触发this.program.action(fn)
的fn
执行this.program.action(fn)
的fn
次要包含loadCommandByName()
依据名称创立命令以及再次this.program.parseAsync()
触发命令-
loadCommandByName()
:触发makeCommand()
执行- 一开始会先触发
options()
执行,也就是this.webpack
的初始化this.loadWebpack()
,从而触发require("webpack")
,从而找到了webpack/package.json
的main
字段,最终找到了webpack/lib/index.js
,而后触发了webpack/lib/webpack.js
的执行 - 执行完
options()
后,执行command.action(action)
- 一开始会先触发
- 再次
this.program.parseAsync()
触发命令:触发loadCommandByName()
注册的command.action(action)
,action()
外围就是触发this.runWebpack()
,最终触发的是下面loadCommandByName()->options()
拿到的this.webpack()
,this.webpack()
会触发整个编译流程的执行
this.webpack()
会触发整个编译流程的执行逻辑请看上面1.2 webpack.js
的剖析
const WEBPACK_PACKAGE = process.env.WEBPACK_PACKAGE || "webpack";
class WebpackCLI {
// ==============================makeCommand==========================
async loadWebpack(handleError = true) {
// WEBPACK_PACKAGE="webpack"
return this.tryRequireThenImport(WEBPACK_PACKAGE, handleError);
}
async tryRequireThenImport(module, handleError = true) {result = require(module);
}
async runWebpack(options, isWatchCommand) {compiler = await this.createCompiler(options, callback);
}
async createCompiler(options, callback) {let config = await this.loadConfig(options);
config = await this.buildConfig(config, options);
let compiler = this.webpack(config.options, ...);
return compiler;
}
// ==============================makeCommand==========================
async run() {const loadCommandByName = async (commandName, allowToInstall = false) => {
//...
await this.makeCommand(isBuildCommandUsed ? buildCommandOptions : watchCommandOptions, async () => {this.webpack = await this.loadWebpack();
return isWatchCommandUsed
? this.getBuiltInOptions().filter((option) => option.name !== "watch")
: this.getBuiltInOptions();}, async (entries, options) => {if (entries.length > 0) {options.entry = [...entries, ...(options.entry || [])];
}
await this.runWebpack(options, isWatchCommandUsed);
});
}
this.program.action(async (options, program) => {if (isKnownCommand(commandToRun)) {
// commandToRun = "build"
await loadCommandByName(commandToRun, true);
}
await this.program.parseAsync([commandToRun, ...commandOperands, ...unknown], {from: "user",});
});
await this.program.parseAsync(args, parseOptions);
}
async makeCommand(commandOptions, options, action) {
const command = this.program.command(commandOptions.name, {
noHelp: commandOptions.noHelp,
hidden: commandOptions.hidden,
isDefault: commandOptions.isDefault,
});
if (options) {options();
}
command.action(action);
return command;
}
}
// webpack/package.json
"main": "lib/index.js"
// webpack/lib/index.js
const fn = lazyFunction(() => require("./webpack"));
module.exports = mergeExports(fn, {get webpack() {return require("./webpack");
},
//...
});
1.1.2 流程图
下面 1.1 npm run build
的剖析能够稀释为上面这张流程图:
1.2 webpack.js
在 webpack/lib/webpack.js
中,咱们会应用 create()
->createCompiler()
进行 compiler
对象的初始化,而后触发compiler.run()
//node_modules/webpack/lib/webpack.js
const webpack = (options, callback) => {const { compiler, watch, watchOptions} = create(options);
compiler.run();
return compiler;
}
const create = () => {
const webpackOptions = options;
compiler = createCompiler(webpackOptions);
//...
return {compiler, watch, watchOptions};
}
那
createCompiler()
具体执行了什么逻辑呢?
1.2.1 createCompiler()
如上面代码所示,次要执行了 5 个步骤:
- 进行
webpack
配置数据的整顿:比方entry
如果没有在webpack.config.js
申明,则会主动填补entry:{main:{}}
- 初始化
Compiler
对象 - 解决
webpack.config.js
的plugins
注册 - 初始化默认参数配置,比方
getResolveDefaults
(前面resolver.resolve
会用到的参数) - 注册内置插件
const createCompiler = rawOptions => {
// 1. 整顿 webpack.config.js 的参数
const options = getNormalizedWebpackOptions(rawOptions);
// 2. 初始化 Compiler 对象
const compiler = new Compiler(options.context, options);
// 3. 解决 webpack.config.js 的 plugins 注册
if (Array.isArray(options.plugins)) {for (const plugin of options.plugins) {if (typeof plugin === "function") {plugin.call(compiler, compiler);
} else {plugin.apply(compiler);
}
}
}
// 4. 初始化默认参数配置,比方 getResolveDefaults(前面 resolver.resolve 会用到的参数)
applyWebpackOptionsDefaults(options);
// 5. 注册内置插件
new WebpackOptionsApply().process(options, compiler);
return compiler;
};
其中第 5 步 new WebpackOptionsApply().process(options, compiler)
会注册十分十分多的内置插件,包含多种 type
的resolveOptions
拼接的相干插件,如上面代码所示
前面
make
阶段的resolver.resolve()
会用到resolveOptions
class WebpackOptionsApply extends OptionsApply {process(options, compiler) {
//...
new EntryOptionPlugin().apply(compiler);
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 => {resolveOptions = cleverMerge(options.resolve, resolveOptions);
resolveOptions.fileSystem = compiler.inputFileSystem;
resolveOptions.resolveToContext = true;
return resolveOptions;
});
compiler.resolverFactory.hooks.resolveOptions
.for("loader")
.tap("WebpackOptionsApply", resolveOptions => {resolveOptions = cleverMerge(options.resolveLoader, resolveOptions);
resolveOptions.fileSystem = compiler.inputFileSystem;
return resolveOptions;
});
}
}
其中最应该关注的是new EntryOptionPlugin().apply(compiler)
,它是入口相干的一个插件
class WebpackOptionsApply extends OptionsApply {process(options, compiler) {new EntryOptionPlugin().apply(compiler);
}
}
class EntryOptionPlugin {apply(compiler) {compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {EntryOptionPlugin.applyEntryOption(compiler, context, entry);
return true;
});
}
}
EntryOptionPlugin
插件会对 entry
入口文件类型进行判断,从而触发注册对应的 EntryPlugin
插件
// EntryOptionPlugin.applyEntryOption
static applyEntryOption(compiler, context, entry) {if (typeof entry === "function") {const DynamicEntryPlugin = require("./DynamicEntryPlugin");
new DynamicEntryPlugin(context, entry).apply(compiler);
} else {const EntryPlugin = require("./EntryPlugin");
for (const name of Object.keys(entry)) {const desc = entry[name];
const options = EntryOptionPlugin.entryDescriptionToOptions(
compiler,
name,
desc
);
for (const entry of desc.import) {new EntryPlugin(context, entry, options).apply(compiler);
}
}
}
}
而 EntryPlugin
插件中注册了两个 hooks
,一个是获取对应的NormalModuleFactory
,一个是监听compiler.hooks.make
而后进行 compilation.addEntry()
流程
apply(compiler) {
compiler.hooks.compilation.tap(
"EntryPlugin",
(compilation, { normalModuleFactory}) => {
compilation.dependencyFactories.set(
EntryDependency,
normalModuleFactory
);
}
);
const {entry, options, context} = this;
const dep = EntryPlugin.createDependency(entry, options);
compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
compilation.addEntry(context, dep, options, err => {callback(err);
});
});
}
1.2.2 compiler.run()
在 1.2.1 createCompiler()
之后,咱们就能够失去了 compiler
对象,而后应用 compiler.run()
开始 make
阶段和 seal
阶段的执行
//node_modules/webpack/lib/webpack.js
const webpack = (options, callback) => {const { compiler, watch, watchOptions} = create(options);
compiler.run();
return compiler;
}
// node_modules/webpack/lib/Compiler.js
class Compiler {run(callback) {const run = () => {this.compile(onCompiled);
}
run();}
compile(callback) {const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {const compilation = this.newCompilation(params);
this.hooks.make.callAsync(compilation, err => {
compilation.seal(err => {
this.hooks.afterCompile.callAsync(compilation, err => {return callback(null, compilation);
});
});
});
});
}
}
1.2.3 整体流程图
下面的一系列剖析能够稀释为上面这张流程图:
下图中的
compiler.compile()
相干内容会在下一大节进行剖析
1.3 小结
- 咱们从下面的剖析中晓得了整体的初始化流程以及如何触发下一阶段 make 阶段
- 在下面初始化流程,咱们没有剖析初始化 ruleSet 的次要代码逻辑,这一块是
make
阶段的resolve
环节所波及到的逻辑,将放在上面段落解说 - 咱们在初始化阶段中要留神:
applyWebpackOptions
会造成一些默认的 options 参数,前面会有很多中央波及到 options 的不同导致的逻辑不同,在后续的开发中,如果发现配置参数不是在webpack.config.js
中书写,应该要想到这个办法是否主动帮咱们增加了一些参数 - 咱们在初始化阶段中要留神:
new WebpackOptionsApply().process()
会注册十分十分多的内置插件,这些插件在后续流程中有十分大的作用,每当无奈晓得某一个流程的插件在哪里注册时,应该想要这个办法是否提前注册了一些内置插件
2. make 阶段 - 整体流程图
3. make 阶段 - 流程图源码剖析
因为 webpack5 细节过于繁冗,各种 callback 调用,因而不会进行各个子流程办法调用具体的流程推导过程,间接以下面总结的流程图为根底,对其中的外围步骤进行剖析
从下面流程图能够晓得,make
阶段有三种次要流程:resolve
、build
、processModuleDependencies
,上面的剖析将这三种外围步骤进行剖析
3.1 resolve- 获取 NormalModuleFactory
addModuleTree() {
const Dep = dependency.constructor;
const moduleFactory = this.dependencyFactories.get(Dep);
//... 一系列办法跳转而后触发 this._factorizeModule({factory: moduleFactory})
}
而这个 moduleFactory 是怎么获取到的呢?咱们什么时候进行
this.dependencyFactories.set
操作?
在初始化流程中,咱们能够晓得,咱们在 EntryPlugin
注册了 compiler.hooks.compilation
事件的监听,在这个事件监听中,咱们能够获取 EntryDependency
对应的 normalModuleFactory
,因而咱们只有晓得compiler.hooks.compilation
事件什么时候触发,就能找到 normalModuleFactory
构建的中央
compiler.hooks.compilation.tap(
"EntryPlugin",
(compilation, { normalModuleFactory}) => {
compilation.dependencyFactories.set(
EntryDependency,
normalModuleFactory
);
}
);
在更后面的流程 compile()
中,咱们会进行const params = this.newCompilationParams()
,这个时候咱们会顺便初始化NormalModuleFactory
newCompilationParams()
能够参考下面1.2.3 整体流程图
newCompilationParams() {
const params = {normalModuleFactory: this.createNormalModuleFactory(),
contextModuleFactory: this.createContextModuleFactory()};
return params;
}
createNormalModuleFactory() {
const normalModuleFactory = new NormalModuleFactory({resolverFactory: this.resolverFactory,});
this.hooks.normalModuleFactory.call(normalModuleFactory);
return normalModuleFactory;
}
初始化实现 params
,咱们会间接进行const compilation = this.newCompilation(params)
的创立,这个时候会触发 compiler.hooks.compilation
事件,从而触发下面 EntryPlugin.js
提及的 compilation.dependencyFactories.set(xxDependency, xxxFactory)
操作
// node_modules/webpack/lib/Compiler.js
newCompilation(params) {const compilation = this.createCompilation(params);
this.hooks.compilation.call(compilation, params);
return compilation;
}
// node_modules/webpack/lib/EntryPlugin.js
compiler.hooks.compilation.tap(
"EntryPlugin",
(compilation, { normalModuleFactory}) => {
compilation.dependencyFactories.set(
EntryDependency,
normalModuleFactory
);
}
);
3.2 resolve-NormalModuleFactory.create()
如上面代码所示,在 3.1 获取 NormalModuleFactory
后,咱们会触发factory.create()
->this.hooks.factorize.callAsync()
//node_modules/webpack/lib/Compilation.js
addModuleTree() {
const Dep = dependency.constructor;
const moduleFactory = this.dependencyFactories.get(Dep);
//... 一系列办法跳转而后触发 this._factorizeModule({factory: moduleFactory})
}
_factorizeModule({factory}) {factory.create();
}
//node_modules/webpack/lib/NormalModuleFactory.js
create(data, callback) {this.hooks.factorize.callAsync()
}
3.3 resolve-getResolver()&resolver.resolve()
3.3.1 整体流程图
this.hooks.factorize.callAsync
通过一系列的跳转之后,会触发 NormalModuleFactory constructor()
注册的事件
class NormalModuleFactory extends ModuleFactory {constructor(){
this.hooks.factorize.tapAsync(
{
name: "NormalModuleFactory",
stage: 100
},
(resolveData, callback) => {this.hooks.resolve.callAsync(resolveData, (err, result) => {//....});
}
);
}
}
而后触发this.hooks.resolve
,这个事件执行逻辑较为简单,先应用一个流程图展现下外围流程:
为了缩小复杂度,临时只思考 normalLoaders,不思考 preLoaders 和 postLoaders,因而上面的流程图也只展现 userLoaders 的相干逻辑,而没有 useLoadersPost 和 useLoadersPre
下面流程能够总结为:
- 解析
inline-loader
:应用 startsWith"-!"
、"!"
、"!!"
判断是否存在 inline 状况,如果存在,则解析为elements
和unresolvedResource
,其中elements
代表解析后的loaders
,unresolvedResource
示意文件的申请链接 resolveRequestArray
:对loaders
进行门路resolve
,获取getResolver("loader")
,调用resolver.resolve()
进行门路解决
resolveRequestArray
是为了解决文件链接中内联模式的loader
,比方下面流程图的"babel-loader!./index-item.line"
,如果整个我的项目都不应用内联模式的loader
,那么resolveRequestArray
传入的参数就是一个空数组!
-
defaultResolve
:- 对文件申请门路
request
进行门路resolve
,获取getResolver("normal")
,调用resolver.resolve()
进行门路解决 - 应用
this.ruleSet.exec
进行筛选出适配文件申请门路request
的loaders
,webpack.config.js
配置了多条rules
规定,然而有一些文件只须要其中的 1、2 个rules
规定,比方.js
文件须要babel-loader
的rule
,而不须要css-loader
的rule
- 下面步骤筛选进去的
loaders
再应用resolveRequestArray
进行门路的整顿,如上面的图片所示
- 对文件申请门路
-
elements
和unresolvedResource
实现后,则进行最终数据的整合,将loader
、parser
、generator
整合到data.createData
中elements
和unresolvedResource
合并为最终的loaders
数组数据getParser()
初始化getGenerator()
初始化
this.hooks.resolve
整体流程如下面所示,上面咱们将依据几个方向进行具体具体地剖析:
getResolver()
: 依据不同 type 获取对应的 resolver 对象resolver.resolve()
: 执行 resolver 对象的 resolve()办法ruleSet.exec()
: 为一般文件筛选出适配的 loader 列表,比方.scss
文件须要sass-loader
、css-loader
等多个 loader 的解决
注:因为
webpack5
的代码逻辑切实太过繁冗,因而文章中有几个中央会采取先概述再分点详细分析的模式
3.3.2 getResolver()
两种类型 resolver
,一种是解决loader
的resolver
,一种是解决一般文件申请的resolver
// loader 类型的 loaderResolver
const loaderResolver = this.getResolver("loader");
// 一般文件类型的 loaderResolver
const normalResolver = this.getResolver(
"normal",
dependencyType
? cachedSetProperty(
resolveOptions || EMPTY_RESOLVE_OPTIONS,
"dependencyType",
dependencyType
)
: resolveOptions
);
getResolver()
简化后的外围代码为:
getResolver(type, resolveOptions) {return this.resolverFactory.get(type, resolveOptions);
}
// node_modules/webpack/lib/NormalModuleFactory.js
get(type, resolveOptions = EMPTY_RESOLVE_OPTIONS) {
//...cache 解决
const newResolver = this._create(type, resolveOptions);
return newResolver;
}
// node_modules/webpack/lib/ResolverFactory.js
_create(type, resolveOptionsWithDepType) {
// 第 1 步:依据 type 获取不同的 options 配置
const resolveOptions = convertToResolveOptions(this.hooks.resolveOptions.for(type).call(resolveOptionsWithDepType)
);
// 第 2 步:依据不同的配置创立不同的 resolver
const resolver = Factory.createResolver(resolveOptions);
return resolver;
}
第 1 步:依据不同 type 获取对应的 options
通过 webpack 官网文档 - 模块解析和 webpack 官网文档 - 解析,咱们能够晓得,resolve 一共分为两种配置,一种是文件类型的门路解析配置,一种是 webpack 的 loader 包的解析配置
文件门路解析能够分为三种:绝对路径、相对路径和模块门路
咱们能够从下面的剖析晓得,第 1 步会依据 type 获取不同的 options 配置,在 webpack.config.js 中,咱们能够配置 resolve 参数,如果没有配置,webpack 也有默认的配置 resolve 参数
配置参数是在哪里配置的呢?又是如何辨别不同 type 的呢?
// 第 1 步:依据 type 获取不同的 options 配置
const resolveOptions = convertToResolveOptions(this.hooks.resolveOptions.for(type).call(resolveOptionsWithDepType)
);
在最下面的初始化流程中,一开始咱们创立 Compiler
对象的时候,会触发 applyWebpackOptionsDefaults()
,在上面的代码块中,咱们能够看到,进行options.resolve
和options.resolveLoader
不同类型的初始化,对应的就是文件类型的门路解析配置,以及 webpack 的 loader 包的解析配置
// node_modules/webpack/lib/webpack.js
const createCompiler = rawOptions => {const options = getNormalizedWebpackOptions(rawOptions);
applyWebpackOptionsDefaults(options);
new WebpackOptionsApply().process(options, compiler);
return compiler;
};
// node_modules/webpack/lib/config/defaults.js
const applyWebpackOptionsDefaults = options => {
options.resolve = cleverMerge(
getResolveDefaults({
cache,
context: options.context,
targetProperties,
mode: options.mode
}),
options.resolve
);
options.resolveLoader = cleverMerge(getResolveLoaderDefaults({ cache}),
options.resolveLoader
);
}
初始化 resolveOptions
后,咱们会触发 new WebpackOptionsApply().process(options, compiler)
进行不同类型:normal
、context
、loader
类型的 resolver
的构建,this.hooks.resolveOptions.for(type).call(resolveOptionsWithDepType)
中的 type
就是 normal
、context
和loader
// node_modules/webpack/lib/webpack.js
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 => {resolveOptions = cleverMerge(options.resolve, resolveOptions);
resolveOptions.fileSystem = compiler.inputFileSystem;
resolveOptions.resolveToContext = true;
return resolveOptions;
});
compiler.resolverFactory.hooks.resolveOptions
.for("loader")
.tap("WebpackOptionsApply", resolveOptions => {resolveOptions = cleverMerge(options.resolveLoader, resolveOptions);
resolveOptions.fileSystem = compiler.inputFileSystem;
return resolveOptions;
});
为了缩小复杂度,临时只剖析
normal
和loader
类型,不剖析context
类型的resolver
type | normal | loader |
---|---|---|
resolveOptions |
其中 normal
的参数获取是合并了 webpack.config.js
和默认 options
的后果
入口文件是EntryDependency
,它具备默认的category
="esm"
class EntryDependency extends ModuleDependency {
/**
* @param {string} request request path for entry
*/
constructor(request) {super(request);
}
get type() {return "entry";}
get category() {return "esm";}
}
而在初始化 normal
类型的 Resolver
时,会触发 hooks.resolveOptions
进行 webpack.config.js
和一些默认参数的初始化
// node_modules/webpack/lib/ResolverFactory.js
_create(type, resolveOptionsWithDepType) {/** @type {ResolveOptionsWithDependencyType} */
const originalResolveOptions = {...resolveOptionsWithDepType};
const resolveOptionsTemp = this.hooks.resolveOptions.for(type).call(resolveOptionsWithDepType);
const resolveOptions = convertToResolveOptions(resolveOptionsTemp);
}
// node_modules/webpack/lib/WebpackOptionsApply.js
compiler.resolverFactory.hooks.resolveOptions
.for("normal")
.tap("WebpackOptionsApply", resolveOptions => {resolveOptions = cleverMerge(options.resolve, resolveOptions);
resolveOptions.fileSystem = compiler.inputFileSystem;
return resolveOptions;
});
this.hooks.resolveOptions.for(type).call(resolveOptionsWithDepType)
获取到的数据如下图所示
而后触发 convertToResolveOptions()
办法,经验几个办法的调用执行后,最终触发 resolveByProperty()
,如下图所示,会依据下面EntryDependency
失去的 "esm"
进行参数的合并,最终失去残缺的配置参数
第 2 步:依据不同的 options 初始化 Resolver 对象
// 第 2 步:依据不同的配置创立不同的 resolver
const resolver = Factory.createResolver(resolveOptions);
创立过程中会注册十分十分多的 plugin
,期待后续的resolver.resolve()
调用来解析门路
// node_modules/enhanced-resolve/lib/ResolverFactory.js
createResolver = function (options) {const normalizedOptions = createOptions(options);
// pipeline //
resolver.ensureHook("resolve");
//... 省略 ensureHook("xxxx")
// raw-resolve
if (alias.length > 0) {plugins.push(new AliasPlugin("raw-resolve", alias, "internal-resolve"));
}
//... 省略很多很多 plugin 的注册
for (const plugin of plugins) {if (typeof plugin === "function") {plugin.call(resolver, resolver);
} else {plugin.apply(resolver);
}
}
return resolver;
}
3.3.3 resolver.resolve()
enhanced-resolve 封装库:多个插件之间的解决,应用 doResolve()进行串联,波及多种文件系统插件的门路查找,第一个插件找不到,就应用第二个插件,直到找到进行这种管道的查找
从上面代码块能够晓得,resolver.resolve
理论就是调用doResolve()
// node_modules/enhanced-resolve/lib/Resolver.js
resolve(context, path, request, resolveContext, callback) {return this.doResolve(this.hooks.resolve, ...args);
}
// node_modules/enhanced-resolve/lib/Resolver.js
doResolve(hook, request, message, resolveContext, callback) {const stackEntry = Resolver.createStackEntry(hook, request);
if (resolveContext.stack) {newStack = new Set(resolveContext.stack);
newStack.add(stackEntry);
} else {newStack = new Set([stackEntry]);
}
return hook.callAsync(request, innerContext, (err, result) => {if (err) return callback(err);
if (result) return callback(null, result);
callback();});
}
一开始调用 doResolve()
时,咱们传入的第一个参数 hook
=this.hooks.resolve
,咱们从下面代码块能够晓得,会间接触发hook.callAsync
,也就是this.hooks.resolve.callAsync()
,而在初始化创立resolver
时,如上面代码块所示,咱们应用 "resolve"
作为 ParsePlugin
参数传入
ResolverFactory.createResolver = function (options) {const normalizedOptions = createOptions(options);
// resolve
for (const { source, resolveOptions} of [{ source: "resolve", resolveOptions: { fullySpecified} },
{source: "internal-resolve", resolveOptions: { fullySpecified: false} }
]) {if (unsafeCache) {//...} else {plugins.push(new ParsePlugin(source, resolveOptions, "parsed-resolve"));
}
}
// parsed-resolve
plugins.push(
new DescriptionFilePlugin(
"parsed-resolve",
descriptionFiles,
false,
"described-resolve"
)
);
}
而在 ParsePlugin
的源代码,如上面的代码块能够晓得,最终
resolver.resolve(this.hooks.resolve)
触发resolver.doResolve("resolve")
而后执行hooks["resolve"].callAsync()
- 触发订阅监听的
ParsePlugin.apply
(ParsePlugin
订阅了resolve
),触发resolver.doResolve(target="parsed-resolve")
而后执行hooks["parsed-resolve"].callAsync()
- 触发订阅监听的
DescriptionFilePlugin.apply
(DescriptionFilePlugin
订阅了parsed-resolve
) - ……
- 这样一直反复上来,就能够创立出一个插件接着一个插件的串行事件处理
为了不便记忆,咱们能够简略了解为
ResolverFactory.createResolver
时注册的插件,第一个参数就是订阅的名称,最初一个参数是下一个订阅的触发名称,比方下面代码块的parsed-resolve
,订阅parsed-resolve
-> 解决 -> 触发下一个订阅described-resolve
class ParsePlugin {constructor(source, requestOptions, target) {
this.source = source;
this.requestOptions = requestOptions;
this.target = target;
}
apply(resolver) {
// this.source = "resolve"
// this.target = "parsed-resolve"
const target = resolver.ensureHook(this.target);
resolver
.getHook(this.source)
.tapAsync("ParsePlugin", (request, resolveContext, callback) => {resolver.doResolve(target, obj, null, resolveContext, callback);
});
}
}
看待不同类型的申请,比方文件类型、目录类型、模块类型在 resolver.resolve()
有不同的执行流程,因为篇幅过长,感兴趣能够查看下一篇文章「Webpack5 源码」enhanced-resolve 门路解析库源码剖析,实质上就是替换别称,寻找文件所在目录门路,拼凑成最终的绝对路径
3.3.4 ruleSetCompiler.exec()
整体流程图
咱们在下面 3.3.1 整体流程图
能够晓得,当解决好依赖文件 (一般文件门路和 loader 门路) 后,会触发 this.ruleSet.exec()
为一般文件进行 loader 的筛选
那么
this.ruleSet
是在哪里初始化的呢?
this.ruleSet
的初始化是在 this.hooks.make.callAsync()
之前就执行的,如上面流程图所示
从下面流程图能够晓得,this.ruleSet
的初始化为:
- 初始化
RuleSetCompiler
-
ruleSetCompiler.compile
compileRules()
- 返回对象数据
{exec: function(){}}
初始化 RuleSetCompiler
初始化 RuleSetCompiler
时初始化了大量的 plugin
,每一个plugin
都进行了 ruleSetCompiler.hooks.rule.tap()
的监听,在前面的 compileRule()
中拼凑 compiledRule
时触发this.hooks.rule.call()
const ruleSetCompiler = new RuleSetCompiler([new BasicMatcherRulePlugin("test", "resource"),
new BasicMatcherRulePlugin("scheme"),
new BasicMatcherRulePlugin("mimetype"),
new BasicMatcherRulePlugin("dependency"),
new BasicMatcherRulePlugin("include", "resource"),
new BasicMatcherRulePlugin("exclude", "resource", true),
new BasicMatcherRulePlugin("resource"),
new BasicMatcherRulePlugin("resourceQuery"),
new BasicMatcherRulePlugin("resourceFragment"),
new BasicMatcherRulePlugin("realResource"),
new BasicMatcherRulePlugin("issuer"),
new BasicMatcherRulePlugin("compiler"),
new BasicMatcherRulePlugin("issuerLayer"),
new ObjectMatcherRulePlugin("assert", "assertions"),
new ObjectMatcherRulePlugin("descriptionData"),
new BasicEffectRulePlugin("type"),
new BasicEffectRulePlugin("sideEffects"),
new BasicEffectRulePlugin("parser"),
new BasicEffectRulePlugin("resolve"),
new BasicEffectRulePlugin("generator"),
new BasicEffectRulePlugin("layer"),
new UseEffectRulePlugin()]);
初始化 ruleSet: ruleSetCompiler.compile(rules)
ruleSetCompiler.compile()
次要由 this.compileRules()
和execRule()
两个办法组成
this.ruleSet = ruleSetCompiler.compile([{rules: Array<{}>}]);
// node_modules/webpack/lib/rules/RuleSetCompiler.js
compile(ruleSet) {const refs = new Map();
const rules = this.compileRules("ruleSet", ruleSet, refs);
const execRule = (data, rule, effects) => {};
return {
references: refs,
exec: data => {const effects = [];
for (const rule of rules) {execRule(data, rule, effects);
}
return effects;
}
};
}
ruleSetCompiler.compile()
传入的参数rules
如下所示,是两个数组汇合,第一个是默认的配置参数,第二个是咱们在webpack.config.js
中设置的loaders
的参数配置
compileRules()办法解析
从上面的代码能够看出,这是为了拼接 exec()
办法中的 rules 对象
,具备conditions
、effects
、rules
、oneOf
四个属性
compileRules(path, rules, refs) {return rules.map((rule, i) =>
this.compileRule(`${path}[${i}]`, rule, refs)
);
}
compileRule(path, rule, refs) {
const compiledRule = {conditions: [],
effects: [],
rules: undefined,
oneOf: undefined
};
// 拼接 compiledRule.conditions 数据
// 拼接 compiledRule.effects 数据
this.hooks.rule.call(path, rule, unhandledProperties, compiledRule, refs);
if (unhandledProperties.has("rules")) {
// 拼接 compiledRule.rules 数据
compiledRule.rules = this.compileRules(`${path}.rules`, rules, refs);
}
if (unhandledProperties.has("oneOf")) {
// 拼接 compiledRule.oneOf 数据
compiledRule.oneOf = this.compileRules(`${path}.oneOf`, oneOf, refs);
}
return compiledRule;
}
能够看出,compileRules()办法次要分为两块内容:
compiledRule
对象 4 个属性的构建this.hooks.rule.call()
的调用
compiledRule 对象属性剖析
const compiledRule = {conditions: [],
effects: [],
rules: undefined,
oneOf: undefined
};
compileRules.conditions
// webpack.config.js
const path = require('path');
module.exports = {
//...
module: {
rules: [
{
test: /\.css$/,
include: [
// will include any paths relative to the current directory starting with `app/styles`
// e.g. `app/styles.css`, `app/styles/styles.css`, `app/stylesheet.css`
path.resolve(__dirname, 'app/styles'),
// add an extra slash to only include the content of the directory `vendor/styles/`
path.join(__dirname, 'vendor/styles/'),
],
},
],
},
};
条件能够是这些之一:
- 字符串:匹配输出必须以提供的字符串开始,目录绝对路径或文件绝对路径。
- 正则表达式:test 输出值。
- 函数:调用输出的函数,必须返回一个真值 (truthy value) 以匹配。
- 条件数组:至多一个匹配条件。
- 对象:匹配所有属性。每个属性都有一个定义行为。
也就是 test、include、exclude、resourceQuery 等条件的筛选,放在 conditions 中,具体能够参考 webpack 官网文档
compileRules.effects
{
type: "use",
value: {ident: "ruleSet[1].rules[0]"
loader: "babel-loader"
options: {presets: [ '@babel/preset-env', {...}]
}
}
}
也就是 loader、options 等条件的筛选,指定要应用哪个 loader 以及对应的配置参数以及对应的门路放在 effects 中
compileRules.rules
寄存子规定,从下面的剖析咱们能够晓得,一开始传入的 ruleSet
是两个数组汇合,咱们须要解析进去,而后将数组外面的每一个 item 都解析成一个rules
,也就是
const compiledRule = {conditions: [],
effects: [],
rules: [
{conditions: [],
effects: [],
rules: undefined,
oneOf: undefined
}
],
oneOf: undefined
};
compileRules.oneOf
规定数组,当规定匹配时,只应用第一个匹配规定。
module.exports = {
//...
module: {
rules: [
{
test: /.css$/,
oneOf: [
{
resourceQuery: /inline/, // foo.css?inline
use: 'url-loader',
},
{
resourceQuery: /external/, // foo.css?external
use: 'file-loader',
},
],
},
],
},
};
this.hooks.rule.call()
this.hooks.rule.call(path, rule, unhandledProperties, compiledRule, refs);
触发 UseEffectRulePlugin
、BasicEffectRulePlugin
、BasicMatcherRulePlugin
、ObjectMatcherRulePlugin
进行传入的 compiledRule
的数据增加
实质就是依据注册的插件,往
compiledRule
汇合数据增加数据,以便于前面执行exec()
this.ruleSet.exec
在经验了下面 RuleSetCompiler
的初始化、ruleSetCompiler.compile(rules)
获取 this.ruleSet
后,最终会执行this.ruleSet.exec()
exec: data => {/** @type {Effect[]} */
const effects = [];
for (const rule of rules) {execRule(data, rule, effects);
}
return effects;
}
const execRule = (data, rule, effects) => {for (const condition of rule.conditions) {//...}
for (const effect of rule.effects) {if (typeof effect === "function") {const returnedEffects = effect(data);
for (const effect of returnedEffects) {effects.push(effect);
}
} else {effects.push(effect);
}
}
if (rule.rules) {for (const childRule of rule.rules) {execRule(data, childRule, effects);
}
}
if (rule.oneOf) {for (const childRule of rule.oneOf) {if (execRule(data, childRule, effects)) {break;}
}
}
return true;
};
依据下面初始化时的 rules
数据汇合,传入数据 data,而后进行每一个 rule
的筛选,实质就是传入一个门路,而后依据门路检测出须要应用什么 loader
如下图所示,咱们传入一个文件对象,包含了门路等数据,而后一直遍历 rules
,将合乎题意的effect
退出到数组中,比方上面这个effect:{value:"javascript/auto"}
3.4 resolve-getParser()
默认设置 settings.type = “javascript/auto”,因而 createParser 的 type 都是 ”javascript/auto”
实质是获取 JavascriptParser
对象
后续流程再仔细分析这里的
getParser()
有何用处,目前只有晓得是JavascriptParser
对象即可
getParser(type, parserOptions = EMPTY_PARSER_OPTIONS) {
// ... 省略缓存逻辑
parser = this.createParser(type, parserOptions);
return parser;
}
createParser() {
parserOptions = mergeGlobalOptions(
this._globalParserOptions,
type,
parserOptions
);
const parser = this.hooks.createParser.for(type).call(parserOptions);
return parser;
}
// createCompiler()->new WebpackOptionsApply().process(options, compiler)
new JavascriptModulesPlugin().apply(compiler);
// node_modules/webpack/lib/javascript/JavascriptModulesPlugin.js
normalModuleFactory.hooks.createParser
.for("javascript/auto")
.tap("JavascriptModulesPlugin", options => {return new JavascriptParser("auto");
});
3.5 resolve-getGenerator()
跟下面
getParser()
逻辑一摸一样,省略反复构造代码
实质是获取 JavascriptGenerator
对象
后续流程再仔细分析这里的
getGenerator()
有何用处,目前只有晓得是JavascriptGenerator
对象即可
// node_modules/webpack/lib/javascript/JavascriptModulesPlugin.js
normalModuleFactory.hooks.createGenerator
.for("javascript/auto")
.tap("JavascriptModulesPlugin", () => {return new JavascriptGenerator();
});
3.6 build 整体流程图
3.7 build 流程图源码剖析 - 外围步骤整体概述
从下面的流程图能够晓得,通过 resolve
流程,咱们获取到了所有申请的绝对路径,拿到了 factoryResult
数据,而后兜兜转转通过很多弯达到了 buildModule()
,最终触发NormalModule._doBuild()
办法
this.addModule(newModule, (err, module) => {
// ... 解决 moduleGraph
this._handleModuleBuildAndDependencies(
originModule,
module,
recursive,
callback
);
}
const _handleModuleBuildAndDependencies = ()=> {
// AsyncQueue,理论就是调用_buildModule
// 减少可读性,callback 改为 await/async
this.buildModule(module, err => {});
}
const _buildModule = (module, callback) => {
// 减少可读性,callback 改为 await/async
module.build();}
// NormalModule.js
build(options, compilation, resolver, fs, callback) {
return this._doBuild(options, compilation, resolver, fs, hooks, err => {
let result;
// js 应用 JavascriptParser._parse 进行解析,外部应用了 require("acorn")进行 parser.parse(code, parserOptions)
// ast = JavascriptParser._parse(source, {
// sourceType: this.sourceType,
// onComment: comments,
// onInsertedSemicolon: pos => semicolons.add(pos)
// });
result = this.parser.parse(this._ast || source, {
source,
current: this,
module: this,
compilation: compilation,
options: options
});
handleParseResult(result);
});
}
3.8.1 _doBuild()具体内容
- 调用
loader-runner
进行loaderContext
的解析 - 解决
sourceMap
逻辑和初始化ast
对象进行下一步调用
_doBuild(options, compilation, resolver, fs, hooks, callback) {
// 拼接上下文参数
const loaderContext = this._createLoaderContext(
resolver,
options,
compilation,
fs,
hooks
);
// const {getContext, runLoaders} = require("loader-runner");
runLoaders(
{
resource: this.resource, // 模块门路
loaders: this.loaders, // loaders 汇合
context: loaderContext, // 下面拼凑的上下文
processResource: (loaderContext, resourcePath, callback) => {// 依据文件类型进行不同 Plugin 的解决,比方入口文件.js,触发了 FileUriPlugin 的 apply(),进行文件的读取
// hooks.readResource
// .for(undefined)
// .tapAsync("FileUriPlugin", (loaderContext, callback) => {// const { resourcePath} = loaderContext;
// loaderContext.addDependency(resourcePath);
// loaderContext.fs.readFile(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) => {
// 将 loaders 放入 buildInfo 中
for (const loader of this.loaders) {this.buildInfo.buildDependencies.add(loader.loader);
}
this.buildInfo.cacheable = this.buildInfo.cacheable && result.cacheable;
processResult(err, result.result);
}
);
}
const processResult = (err, result) => {
// 解决 sourceMap 逻辑
this._source = this.createSource(
options.context,
this.binary ? asBuffer(source) : asString(source),
sourceMap,
compilation.compiler.root
);
// 初始化 AST
this._ast =
typeof extraInfo === "object" &&
extraInfo !== null &&
extraInfo.webpackAST !== undefined
? extraInfo.webpackAST
: null;
// _doBuild 的 callback()
return callback();}
3.8.2 Parse.parse-AST 相干逻辑
_doBuild()执行结束后,会执行
this.parser.parse(this._ast||source)
,而后执行handleParseResult()
调用 resolve
阶段生成的 parse
,最终解析生成AST
语法树
build(options, compilation, resolver, fs, callback) {
return this._doBuild(options, compilation, resolver, fs, hooks, err => {
let result;
// js 应用 JavascriptParser._parse 进行解析,外部应用了 require("acorn")进行 parser.parse(code, parserOptions)
// ast = JavascriptParser._parse(source, {
// sourceType: this.sourceType,
// onComment: comments,
// onInsertedSemicolon: pos => semicolons.add(pos)
// });
result = this.parser.parse(this._ast || source, {
source,
current: this,
module: this,
compilation: compilation,
options: options
});
handleParseResult(result);
});
}
build 流程到此整体流程的剖析曾经完结,之后会进行依赖的递归调用 handleModuleCreate()的解决
因为 build 流程这一大块还是存在很多简单的小模块内容,上面几个大节将着重剖析这些简单的小模块
3.8 build 流程小模块 - 概述
在整个 build
流程中,咱们上面将针对:
_doBuild()
->runLoaders()
noParse
this.parser.parse
理论就是JavascriptParser._parse
三个小点进行具体地剖析
build(options, compilation, resolver, fs, callback) {
return this._doBuild(options, compilation, resolver, fs, hooks, err => {
let result;
const noParseRule = options.module && options.module.noParse;
if (this.shouldPreventParsing(noParseRule, this.request)) {
// We assume that we need module and exports
this.buildInfo.parsed = false;
this._initBuildHash(compilation);
return handleBuildDone();}
// js 应用 JavascriptParser._parse 进行解析,外部应用了 require("acorn")进行 parser.parse(code, parserOptions)
// ast = JavascriptParser._parse(source, {
// sourceType: this.sourceType,
// onComment: comments,
// onInsertedSemicolon: pos => semicolons.add(pos)
// });
result = this.parser.parse(this._ast || source, {
source,
current: this,
module: this,
compilation: compilation,
options: options
});
handleParseResult(result);
});
}
_doBuild(options, compilation, resolver, fs, hooks, callback) {// const { getContext, runLoaders} = require("loader-runner");
runLoaders(
{...},
(err, result) => {
// 将 loaders 放入 buildInfo 中
for (const loader of this.loaders) {this.buildInfo.buildDependencies.add(loader.loader);
}
this.buildInfo.cacheable = this.buildInfo.cacheable && result.cacheable;
processResult(err, result.result);
}
);
}
const processResult = (err, result) => {
// 解决 sourceMap 逻辑
this._source = this.createSource(
options.context,
this.binary ? asBuffer(source) : asString(source),
sourceMap,
compilation.compiler.root
);
// 初始化 AST
this._ast =
typeof extraInfo === "object" &&
extraInfo !== null &&
extraInfo.webpackAST !== undefined
? extraInfo.webpackAST
: null;
// _doBuild 的 callback()
return callback();}
3.9 build 流程小模块 -runLoaders 流程剖析
runLoaders 是
loader-runner
库提供的一个办法,本次剖析的loader-runner
库版本为4.3.0
通过 runLoaders 执行所有 loaders,获取原始_source
function runLoaders(options, callback) {
// 1. 为每一个 loader 创立一个状态 Object 数据,具备多个属性
loaders = loaders.map(createLoaderObject);
// 2. 给 loaderContext 增加其它属性和办法
loaderContext.context = contextDirectory;
loaderContext.loaderIndex = 0;
loaderContext.loaders = loaders;
// ......
// 3. 进行 loaders 的 pitch 循环
iteratePitchingLoaders(processOptions, loaderContext)
}
iteratePitchingLoaders()
源码逻辑比拟清晰简略,能够应用一个流程图展现
style-loader 源码解析
示例具体代码
// entry1.js
import "./index.less"
console.info("这是 entry1");
// index.less
#box1{
width: 100px;
height: 100px;
background: url('./1.jpg') no-repeat 100% 100%;
}
#box2{
width: 200px;
height: 200px;
background: url('./2.jpg') no-repeat 100% 100%;
}
#box3{
width: 300px;
height: 300px;
background: url('./3.jpg') no-repeat 100% 100%;
}
style-loader 源码概述
精简后的 style-loader
的index.js
如下所示,代码流程并不简单,只有弄清楚 injectType
这个参数的作用,实质就是通过 injectType
类型判断,而后造成不同的代码,进行 return 返回
var _path = _interopRequireDefault(require("path"));
var _utils = require("./utils");
var _options = _interopRequireDefault(require("./options.json"));
function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : {default: obj};
}
const loaderAPI = () => {};
loaderAPI.pitch = function loader(request) {const options = this.getOptions(_options.default);
const injectType = options.injectType || "styleTag";
const esModule = typeof options.esModule !== "undefined" ? options.esModule : true;
const runtimeOptions = {};
const insertType = typeof options.insert === "function" ? "function" : options.insert && _path.default.isAbsolute(options.insert) ? "module-path" : "selector";
const styleTagTransformType = typeof options.styleTagTransform === "function" ? "function" : options.styleTagTransform && _path.default.isAbsolute(options.styleTagTransform) ? "module-path" : "default";
switch (injectType) {case "linkTag": { return ...}
case "lazyStyleTag":
case "lazyAutoStyleTag":
case "lazySingletonStyleTag": {return ...}
case "styleTag":
case "autoStyleTag":
case "singletonStyleTag":
default: {return ...}
}
};
var _default = loaderAPI;
exports.default = _default;
injectType 品种以及作用
参考
webpack
官网:https://webpack.js.org/loaders/style-loader
styleTag
: 引入的css
文件都会造成独自的<style></style>
标签插入到DOM
中singletonStyleTag
: 引入的css
文件会合并造成 1 个<style></style>
标签插入到DOM
中linkTag
: 会通过<link rel="stylesheet" href="path/to/file.css">
的模式插入到DOM
中lazyStyleTag
、lazyAutoStyleTag
、lazySingletonStyleTag
: 提早加载的款式,能够通过style.use()
手动触发加载
injectType=default 时的返回后果剖析
默认
injectType
="styleTag"
从源码中,咱们能够看到,当 injectType
="styleTag"
时,会返回一大串应用 utils.xxx
拼接的代码,咱们间接应用下面具体例子断点取出这一串代码
上面代码块就是下面截图所获取到的代码,最终 style-loader
的pitch()
会返回上面这一串代码的字符串
import API from "!../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js";
import domAPI from "!../node_modules/style-loader/dist/runtime/styleDomAPI.js";
import insertFn from "!../node_modules/style-loader/dist/runtime/insertBySelector.js";
import setAttributes from "!../node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js";
import insertStyleElement from "!../node_modules/style-loader/dist/runtime/insertStyleElement.js";
import styleTagTransformFn from "!../node_modules/style-loader/dist/runtime/styleTagTransform.js";
import content, * as namedExport
from "!!../node_modules/css-loader/dist/cjs.js!../node_modules/less-loader/dist/cjs.js!./index.less";
var options = {};
options.styleTagTransform = styleTagTransformFn;
options.setAttributes = setAttributes;
options.insert = insertFn.bind(null, "head");
options.domAPI = domAPI;
options.insertStyleElement = insertStyleElement;
var update = API(content, options);
export * from "!!../node_modules/css-loader/dist/cjs.js!../node_modules/less-loader/dist/cjs.js!./index.less";
export default content && content.locals ? content.locals : undefined;
咱们从下面 iteratePitchingLoaders()
的剖析能够晓得,当一个 loader
的pitch()
办法由返回值时,会中断前面 loader
的执行
因而此时 style-loader pitch()
->css-loader pitch()
的执行会被中断,因为 style-loader
是解决 css
的第一个 loader
,因而style-loader
的pitch()
返回的字符串会交由 webpack
解决
style-loader 的 pitch()返回字符串造成 Module
从下面代码,咱们也能够看出,
tyle-loader
的pitch()
返回的实质就是一串可执行的代码,而不是一个数据处理后的后果
最终 webpack
会将 style-loader
的pitch()
返回的后果进行解决,最终打包造成一个 Module
(如main.js
所示),而所应用的工具办法因为合乎 node_modules
的分包规定,因而会被打包进 vendors-node_modules_css-loader_xxx
中
因为
style-loader
返回的是一系列能够执行的代码,所以咱们等同认为解决js
文件,上面将开展剖析
上面引入了两个文件,一个是index.less
,一个是test.js
// entry1.js
import {getC1} from "./test.js";
import "./index.less";
console.info("这是 entry1");
test.js
返回的数据可能是
import xxx from "xxx"
import xxxx from "xxxxx";
export {getC1};
而当初咱们引入 index.less
,从下面的剖析能够晓得,咱们应用runLoaders
解析失去跟 test.js
其实是相似的代码构造,既有import
,也有export
import API from "!../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js";
import domAPI from "!../node_modules/style-loader/dist/runtime/styleDomAPI.js";
import insertFn from "!../node_modules/style-loader/dist/runtime/insertBySelector.js";
import setAttributes from "!../node_modules/style-loader/dist/runtime/setAttributesWithoutAttributes.js";
import insertStyleElement from "!../node_modules/style-loader/dist/runtime/insertStyleElement.js";
import styleTagTransformFn from "!../node_modules/style-loader/dist/runtime/styleTagTransform.js";
import content, * as namedExport
from "!!../node_modules/css-loader/dist/cjs.js!../node_modules/less-loader/dist/cjs.js!./index.less";
var options = {};
options.styleTagTransform = styleTagTransformFn;
options.setAttributes = setAttributes;
options.insert = insertFn.bind(null, "head");
options.domAPI = domAPI;
options.insertStyleElement = insertStyleElement;
var update = API(content, options);
export * from "!!../node_modules/css-loader/dist/cjs.js!../node_modules/less-loader/dist/cjs.js!./index.less";
export default content && content.locals ? content.locals : undefined;
最终造成的打包文件中,也会造成同样构造的{"xxx.js":xxx, "xxx.less": xxx}
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {return cachedModule.exports;}
var module = __webpack_module_cache__[moduleId] = {exports: {}
};
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
var __webpack_modules__ = ({
/***/ "test.js":
/***/ (function (__unused_webpack_module, exports, __webpack_require__) {/***/}),
/***/ "./src/index.less":
/***/ (function (__unused_webpack_module, exports, __webpack_require__) {/***/})
/******/
});
然而跟一般的 js
文件解决有一点是不同的,就是外面进行了一个内联 loader
的指定,即
import content, * as namedExport
from "!!../node_modules/css-loader/dist/cjs.js!../node_modules/less-loader/dist/cjs.js!./index.less";
"!!"
是疏忽 webpack.config.js
的规定,间接应用 "!!"
配置的规定,也就是说遇到 xxx.less
文件,不再应用 webpack.config.js
配置的 style-loader
,而是应用css-loader
和less-loader
最终从 css-loader
和less-loader
转化失去的 content
内容会通过之前的 js
代码插入到DOM
import content, * as namedExport
from "!!../node_modules/css-loader/dist/cjs.js!../node_modules/less-loader/dist/cjs.js!./index.less";
var options = {};
options.styleTagTransform = styleTagTransformFn;
options.setAttributes = setAttributes;
options.insert = insertFn.bind(null, "head");
options.domAPI = domAPI;
options.insertStyleElement = insertStyleElement;
var update = API(content, options);
失去 css-loader 后果后,将 style 插入到 DOM
import content, * as namedExport
from "!!../node_modules/css-loader/dist/cjs.js!../node_modules/less-loader/dist/cjs.js!./index.less";
var update = API(content, options);
通过
import
获取CSS
款式数据后,API()
又是什么?
依据对打包文件的代码精简,实质 API
就是上面的 module.exports = function (list, options){}
办法,最终是遍历 list
,而后调用addElementStyle()
办法
module.exports = function (list, options) {
//...
var lastIdentifiers = modulesToDom(list, options);
return function update(newList) {//... 初始化不会调用外部的 update(),因而省略
}
}
function modulesToDom(list, options) {for (var i = 0; i < list.length; i++) {var item = list[i];
var obj = {css: item[1],
media: item[2]
//...
};
var updater = addElementStyle(obj, options);
//...
}
//...
}
而 addElementStyle()
办法通过一系列办法的调用,实质也是利用了 document.createElement("style")
,而后调用options.setAttributes
和options.insert
进行 style
的DOM
插入
options.setAttributes
和options.insert
不晓得又是什么哪个文件的工具函数 =_= 这里不再剖析
function addElementStyle(obj, options) {var api = options.domAPI(options);
api.update(obj);
var updater = function updater(newObj) {//...};
return updater;
}
function domAPI(options) {var styleElement = options.insertStyleElement(options);
return {update: function update(obj) {apply(styleElement, options, obj);
},
remove: function remove() {removeStyleElement(styleElement);
}
};
}
function insertStyleElement(options) {var element = document.createElement("style");
options.setAttributes(element, options.attributes);
options.insert(element, options.options);
return element;
}
3.10 build 流程小模块 -noParse
能够在 webpack.config.js
配置不须要解析的文件,而后在 this.parser.parse()
之前会应用 noParseRule
跟目前的申请进行比对,如果合乎则不进行下一步的解析逻辑
// check if this module should !not! be parsed.
// if so, exit here;
const noParseRule = options.module && options.module.noParse;
if (this.shouldPreventParsing(noParseRule, this.request)) {
// We assume that we need module and exports
this.buildInfo.parsed = false;
this._initBuildHash(compilation);
return handleBuildDone();}
3.11 build 流程小模块 -JavascriptParser._parse 源码剖析
通过 AST 内容,遍历特定 key,收集 Module 的依赖 Dependency,为后续
processModuleDependencies() 办法
解决做筹备
// node_modules/webpack/lib/NormalModule.js
result = this.parser.parse(this._ast || source, {
source,
current: this,
module: this,
compilation: compilation,
options: options
});
由下面 3.4 resolve-getParse()
的剖析,咱们能够晓得,this.parser
=JavascriptParser
,因而咱们须要剖析 JavascriptParser
类的 parse()
办法,如上面代码块所示
const {Parser: AcornParser} = require("acorn");
const parser = AcornParser.extend(importAssertions);
class JavascriptParser extends Parser {parse(source, state) {
ast = JavascriptParser._parse(source, {
sourceType: this.sourceType,
onComment: comments,
onInsertedSemicolon: pos => semicolons.add(pos)
});
this.detectMode(ast.body);
this.preWalkStatements(ast.body);
this.blockPreWalkStatements(ast.body);
this.walkStatements(ast.body);
return state;
}
static _parse(code, options) {ast = /** @type {AnyNode} */ (parser.parse(code, parserOptions));
return ast
}
}
从下面代码块能够晓得,次要分为两个局部:
- 应用
acorn
库将js
转化为AST
- 应用
preWalkStatements
、blockPreWalkStatements
、walkStatements
对转化后的AST.body
进行剖析
acorn
将js
转化为AST
的流程难度较高,本文不对这方面进行具体的剖析,感兴趣能够另外寻找文章学习
解析 ast.body
的流程,实质上是对多种条件进行列举解决,比方do-while
语句造成的AST type=DoWhileStatement
,for..in
语句造成的AST type=ForInStatement
,因为内容过大且繁冗,本文不会对所有的流程进行剖析,对解析 ast.body
所有的流程感兴趣的用户,能够参考这篇文章:模块构建之解析_source 获取 dependencies
ast.body 流程解析逆向推导
咱们从下面的流程图能够晓得,咱们在 buildMode
的下一个阶段会进行 processModuleDependencies()
的解决,而 processModuleDependencies()
办法中最外围的代码就是 module.dependencies
和module.blocks
,换句话说,咱们在 JavascriptParser._parse()
的流程中,应该最关注的就是怎么拿到 module
的dependencies
和blocks
咱们在下面的剖析能够晓得,进行 buildMode()
时,实际上曾经转化为 NormalModule
类的解决,因而咱们能够很快从 NormalModule.js
-> 继承Module.js
-> 继承DepencenciesBlock.js
中找到对应的 addBlock()
和addDependency()
,咱们间接进行debugger
而后咱们就能够轻易拿到每一次为以后 NormalModule
增加 dependencies
和blocks
时的代码流程,如上面图所示,咱们能够分明看到,具体的流程为:
JavascriptParser.parse()
blockPreWalkStatements()
blockPreWalkStatement()
blockPreWalkImportDeclaration()
addDependency()
然而这个语句到底是对应源代码哪一句呢?咱们须要晓得哪一句源代码,咱们能力更好了解
JavascriptParser.parse()
这个流程到底做了什么
ast.body 内容
这个时候咱们能够借助 AST 在线解析网站,咱们间接把调试代码放上去,咱们就能够失去十分清晰源码对应的 AST Body
,而且咱们也能够看到了下面图中所呈现的ImportDeclaration
字段
通过示例剖析 dependency 和 block 的增加流程
凭借下面 debugger 和 AST 在线解析网站,咱们能够残缺地晓得示例代码中哪一句造成了什么
AST
语句,以及前面依据这些语句进行了怎么的流程
上面通过几个流程图展现示例源码中依赖收集流程
// src/index.js
import {getC1} from "./item/index_item-parent1.js";
import "./index.scss";
var _ = require("lodash");
import {getContextItem} from "./contextItem";
var test = _.add(6, 4) + getC1(1, 3)
function getAsyncValue() {var temp = getContextItem();
import("./item/index_item-async.js").then((fnGetValue)=> {console.log("async", fnGetValue());
});
return temp;
}
setTimeout(()=> {console.log(getAsyncValue());
}, 1000);
import 语句和办法调用
import {getC1} from "./item/index_item-parent1.js";
// 下面的语句会触发 addDependency(HarmonyImportSideEffectDependency)
var test = _.add(6, 4) + getC1(1, 3)
// 下面的语句会触发 addDependency(HarmonyImportSpecifierDependency)
require 语句和办法调用
var _ = require("lodash");
// 下面的语句会触发 addDependency(CommonJsRequireDependency)
var test = _.add(6, 4);
// 下面的语句不会触发任何 addDependency()和 addBlock()
异步 import 语句和办法调用
function getAsyncValue() {import("./item/index_item-async.js")
.then((fn)=> {console.log("async", fn());
});
// 下面的语句会触发 addBlock(new AsyncDepenciesBlock(ImportDependency))
}
export 语句
import {getTemp} from "babel-loader!./index_item-inline";
export function getC1(a, b) {return getTemp() + 33 + a + b;
}
// 下面的语句会触发 addDependency(new HarmonyExportSpecifierDependency())
3.12 processModuleDependencies
从下面
3.11
的剖析,咱们晓得:import
造成的依赖是:HarmonyImportSideEffectDependency
require
造成的依赖是:CommonJsRequireDependency
import
的办法调用造成的依赖是:HarmonyImportSpecifierDependency
export
的办法调用造成的依赖是:HarmonyExportSpecifierDependency
从上面代码能够晓得,buildMode()
完结后,咱们会解决_processModuleDependencies()
:
- 遍历
module.dependencies
-> 调用processDependency()
解决依赖 - 异步的依赖
module.blocks
,则当作module
压入queue
中持续解决module.blocks[i]
的dependencies
和blocks
- 如果全副处理完毕,则调用
onDependenciesSorted()
_processModuleDependencies(module, callback) {
let inProgressSorting = 1;
const queue = [module];
do {const block = queue.pop();
// import 依赖
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);
// inProgressSorting: 正在进行排序,inProgressSorting= 0 阐明曾经排序实现,即实现下面的 processDependency()清空 queue
if (--inProgressSorting === 0) onDependenciesSorted();}
3.12.1 processDependency
- 建设
module
与dependency
之间的关联(this.moduleGraph=new ModuleGraph()
) - 筛选出
resourceIdent
为空的依赖 - 将依赖存入
sortedDependencies
中,为上面的onDependenciesSorted()
做筹备
const processDependency = (dep, index) => {
// 建设 module 与 dependency 之间的关联
this.moduleGraph.setParents(dep, currentBlock, module, index);
// ... 省略一系列的缓存逻辑
// 将 dependency 放入到 sortedDependencies 数组中
processDependencyForResolving(dep);
};
const processDependencyForResolving = dep => {const resourceIdent = dep.getResourceIdentifier();
if (resourceIdent !== undefined && resourceIdent !== null) {
const category = dep.category;
const constructor = dep.constructor;
const factory = this.dependencyFactories.get(constructor);
sortedDependencies.push({
factory: factoryCacheKey2,
dependencies: list,
context: dep.getContext(),
originModule: module
});
list.push(dep);
listCacheValue = list;
}
};
processDependencyForResolveing()
次要是进行 resourceIdent
的筛选以及同一个 request
的dependency
的合并
resourceIdent 筛选
const processDependencyForResolving = dep => {const resourceIdent = dep.getResourceIdentifier();
if (resourceIdent !== undefined && resourceIdent !== null) {//.....} else {debugger;}
};
间接在
processDependencyForResolving()
进行debugger
调试(如下面代码块所示)
咱们会发现,exports
所造成的 dependency
的resourceIdent
都为空,而后咱们看了下其中一种 exports
类型 HarmonyExportSpecifierDependency
的源码,咱们能够从上面代码块发现,getResourceIdentifier
间接返回了 null
,因而在processDependencyForResolving()
中会间接过滤掉 exports
所造成的dependency
class HarmonyExportSpecifierDependency extends NullDependency {}
class NullDependency extends Dependency {}
class Dependency {getResourceIdentifier() {return null;}
}
同一个 request 的 dependency 的合并
通过下面 processDependencyForResolveing()
的解决,最终造成以 request
为key
的依赖数据对象,比方上面代码块,以 request="./item/index_item-parent1.js"
的所有 dependency
都会合并到 sortedDependencies[0]
的dependencies
上
HarmonyImportSideEffectDependency
代表的是import {getC1} from "./item/index_item-parent1.js"
造成的依赖HarmonyImportSpecifierDependency
代表的是var test = getC1()
造成的依赖
sortedDependencies[0] = {
dependencies: [
{ // HarmonyImportSideEffectDependency
request: "./item/index_item-parent1.js",
userRequest: "./item/index_item-parent1.js"
},
{ // HarmonyImportSpecifierDependency
name: "getC1",
request: "./item/index_item-parent1.js",
userRequest: "./item/index_item-parent1.js"
}
],
originModule: {
userRequest: "/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js/src/index.js",
dependencies: [//...10 个依赖,包含下面那两个 Dependency]
}
}
3.12.2 onDependenciesSorted
- 遍历循环
sortedDependencies
数组,拿出依赖对象dependency
进行handleModuleCreation()
反复下面resolve()
->build()
造成NormalModule
数据的流程 - 递归调用
handleModuleCreation()
实现所有依赖对象dependency
的转化后,触发onTransitiveTasksFinished()
办法 - 完结最外层的
handleModuleCreation()
回调,完结 make 流程
const onDependenciesSorted = err => {// 解决所有的依赖,进行 handleModuleCreation 的调用,解决实现后,调用 callback(),回到最后的 handleModuleCreation()的回调
for (const item of sortedDependencies) {
inProgressTransitive++;
this.handleModuleCreation(item, err => {if (--inProgressTransitive === 0) onTransitiveTasksFinished();});
}
if (--inProgressTransitive === 0) onTransitiveTasksFinished();}
const onTransitiveTasksFinished = err => {if (err) return callback(err);
this.processDependenciesQueue.decreaseParallelism();
return callback();};
依赖对象 dependency 进行 handleModuleCreation()
咱们在下面的 sortedDependencies
数组拼接中能够晓得,咱们的 sortedDependencies[i]
的dependencies
实际上是一个数组汇合,那咱们持续调用 handleModuleCreation()
是如何解决这种数组汇合的呢?
// node_modules/webpack/lib/NormalModuleFactory.js
create(data, callback) {const dependencies = /** @type {ModuleDependency[]} */ (data.dependencies);
const dependency = dependencies[0];
const request = dependency.request;
const dependencyType =
(dependencies.length > 0 && dependencies[0].category) || "";
const resolveData = {
request,
dependencies,
dependencyType
};
// 利用 resolveData 进行一系列的 resolve()和 buildModule()操作...
}
咱们从下面的剖析晓得,handleModuleCreation()
一开始调用的 NormalModuleFactory.create()
的相干逻辑,如下面代码块所示,咱们会从 dependencies
中拿到第一个元素,dependencies[0]
,实际上对应的也是顶部的 import 语句
,如import {getC1} from "./item/index_item-parent1.js"
所造成的依赖 HarmonyImportSideEffectDependency
,咱们会将HarmonyImportSideEffectDependency
对应的 request
门路作为入口,进行整个 NormalModule
数据的创立
import {getC1} from "./item/index_item-parent1.js";
var test = _.add(6, 4) + getC1(1, 3);
var test1 = _.add(6, 4) + getC1(1, 3);
var test2 = getC1(4, 5);
因而无论咱们多少次调用 getC1()
这个办法(造成多个 HarmonyImportSpecifierDependency
),咱们只会取第一个HarmonyImportSideEffectDependency
作为依赖对象的 handleModuleCreation()
构建
4. 其它细节剖析
4.1 loader 优先级以及 inline 写法跳过优先级
参考 https://webpack.js.org/concepts/loaders/#inline
前缀为 "!"
将禁用所有已配置的normal loaders
import Styles from '!style-loader!css-loader?modules!./styles.css';
前缀为 "!!"
将禁用所有已配置的加载程序(preLoaders
、loaders
、postLoaders
)
import Styles from '!!style-loader!css-loader?modules!./styles.css';
前缀为 "-!"
将禁用所有已配置的 preLoaders
和loaders
,但不会禁用postLoaders
4.2 enhanced-resolve 不同类型的解决剖析
resolver.resolve()的具体流程
因为篇幅起因,放在下一篇文章「Webpack5 源码」enhanced-resolve 门路解析库源码剖析中剖析
5. 总结
5.1 resolve
5.1.1 enhanced-resolve 解决门路
解决module
、相对路径、绝对路径、应用别称等状况,将门路拼接为绝对路径
5.1.2 解析出目前门路 path 适宜的 loaders
依据 webpack.config.js
配置的规定,筛选目前门路利用的 loaders
,比方index.less
适配style-loader
+css-loader
+less-loader
5.2 build
5.2.1 NormalModule._doBuild
调用 runLoaders()
进行 loaders
的转化,比方 xxx.less
转化为 style
、ES6
转化 ES5
等等
5.2.2 this.parser.parse
拿到转化后统一标准的数据后,调用 require("acorn")
对这些数据进行 AST 剖析,失去对应的依赖关系,获取对应 module
的依赖dependencies
5.3 processModuleDependencies
5.3.1 转化依赖文件
依据 this.parser.parse
拿到的依赖关系,调用 handleModuleCreate()
进行下面流程的反复执行
5.3.2 建设依赖 dependency 与目前父 module 的关系
在调用 handleModuleCreate()
进行下面流程的反复执行时,originModule
是以后的 Module
,dependencies
是以后的 Module
的依赖
而后经验 handleModuleCreation()
->factorizeModule()
->addModule()
后,会应用 moduleGraph
进行依赖与目前父 module 的关系的绑定,如上面代码所示
从入口文件调用
handleModuleCreation()
时originModule
为空,只有dependency
触发handleModuleCreation()
时才会传入originModule
handleModuleCreation({
factory,
dependencies,
originModule,
contextInfo,
context,
recursive = true,
connectOrigin = recursive
}){const factoryResult = await this.factorizeModule();
await this.addModule();
for (let i = 0; i < dependencies.length; i++) {const dependency = dependencies[i];
moduleGraph.setResolvedModule(
connectOrigin ? originModule : null,
dependency,
unsafeCacheableModule
);
unsafeCacheDependencies.set(dependency, unsafeCacheableModule);
}
}
class ModuleGraphModule {setResolvedModule(originModule, dependency, module) {
const connection = new ModuleGraphConnection(
originModule,
dependency,
module,
undefined,
dependency.weak,
dependency.getCondition(this)
);
const connections = this._getModuleGraphModule(module).incomingConnections;
connections.add(connection);
if (originModule) {const mgm = this._getModuleGraphModule(originModule);
mgm.outgoingConnections.add(connection);
} else {this._dependencyMap.set(dependency, connection);
}
}
}
而这个关系的绑定在前面 seal
阶段解决依赖时会发挥作用,比方上面的 processBlock()
会从以后 moduleGraph
拿到 module
对应的outgoingConnections
const processBlock = block => {const blockModules = getBlockModules(block, chunkGroupInfo.runtime);
}
getBlockModules() {
//... 省略初始化 blockModules 和 blockModulesMap 的逻辑
extractBlockModules(module, moduleGraph, runtime, blockModulesMap);
blockModules = blockModulesMap.get(block);
return blockModules;
}
const extractBlockModules = (module, moduleGraph, runtime, blockModulesMap) => {for (const connection of moduleGraph.getOutgoingConnections(module)) {
const d = connection.dependency;
// We skip connections without dependency
if (!d) continue;
//....
}
}
参考
- 精通 Webpack 外围原理专栏
- webpack@4.46.0 源码剖析 专栏
- webpack loader 从上手到了解系列:style-loader
- webpack5 源码详解 – 先导
- webpack5 源码详解 – 编译模块