本文内容基于webpack 5.74.0
版本进行分析
webpack5核心流程
专栏共有5篇,使用流程图的形式分析了webpack5的构建原理
:
- 「Webpack5源码」make阶段(流程图)分析
- 「Webpack5源码」enhanced-resolve路径解析库源码分析
- 「Webpack5源码」seal阶段(流程图)分析(一)
- 「Webpack5源码」seal阶段分析(二)-SplitChunksPlugin源码
- 「Webpack5源码」seal阶段分析(三)-生成代码&runtime
前言
- 由于
webpack5
整体代码过于复杂,为了减少复杂度,本文所有分析将只基于js
文件类型进行分析,不会对其它类型(css
、image
)进行分析,所举的例子也都是基于js
类型 - 为了增加可读性,会对源码进行删减、调整顺序、改变的操作,文中所有源码均可视作为伪代码
- 文章默认读者已经掌握
tapable
、loader
、plugin
等基础知识,对文章中出现asyncQueue
、tapable
、loader
、plugin
相关代码都会直接展示,不会增加过多说明 - 由于
webpack5
整体代码过于复杂,因此本文会采取抽离出核心代码的模式进行分析讲解
核心代码是笔者认为核心代码的部分,肯定会造成部分内容(读者也觉得是核心代码)缺失,如果发现缺失部分,请参考其它文章或者私信/评论区告知我
文章内容
从npm run build
命令开始,将webpack
编译入口到make
阶段的所有流程抽离出核心代码形成流程图,然后针对核心代码进行具体的分析,主要分为:
- 从
npm run build
命令开始,分析webpack
入口文件的源码执行流程,分析是从npm run build
入口开始是如何执行到make
阶段 - 分析
make
阶段的factorizeModule()
的执行流程 - 分析
make
阶段的buildMode()
的执行流程 - 分析
make
阶段的processModuleDependencies()
的执行流程
1. 初始化
1.1 npm run build
1.1.1 流程图
1.1.2 流程图源码分析
当我们执行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.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()流程图
下图中的compiler.run()
和compiler.compile()
相关内容会在1.3
中进行分析
1.2.2 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.3 compiler.run()
在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.3 小结
- 我们从上面的分析中知道了整体的初始化流程以及如何触发下一阶段make阶段
- 在上面初始化流程,我们没有分析初始化ruleSet的主要代码逻辑,这一块是
make
阶段的resolve
环节所涉及到的逻辑,将放在下面段落讲解 - 我们在初始化阶段中要注意:
applyWebpackOptions
会形成一些默认的options参数,后面会有很多地方涉及到options的不同导致的逻辑不同,在后续的开发中,如果发现配置参数不是在webpack.config.js
中书写,应该要想到这个方法是否自动帮我们添加了一些参数 - 我们在初始化阶段中要注意:
new WebpackOptionsApply().process()
会注册非常非常多的内置插件,这些插件在后续流程中有非常大的作用,每当无法知道某一个流程的插件在哪里注册时,应该想要这个方法是否提前注册了一些内置插件
2. make阶段-整体流程图
3. make阶段-流程图源码分析
从上面流程图可以知道,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.7.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.7.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;
//....
}
}
5.4 编译入口->make->seal流程图总结
参考
- 精通 Webpack 核心原理专栏
- webpack@4.46.0 源码分析 专栏
- webpack loader 从上手到理解系列:style-loader
- webpack5 源码详解 - 先导
- webpack5 源码详解 - 编译模块
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。