1.webpack命令入口解析
当开始断点调试后,webpack就会开始执行node_modules/webpack/bin/webpack.js文件,文件如下,其中做了一些注释:
#!/usr/bin/env node
// 正常执行返回
process.exitCode = 0;
// 运行某个命令
const runCommand = (command, args) => {
...
};
// 判断某个包是否安装
const isInstalled = packageName => {
...
};
// webpack可用的CLI:webpacl-cli和webpack-command
const CLIs = [
...
];
// 判断是否两个CLI是否安装了
const installedClis = CLIs.filter(cli => cli.installed);
// 根据CLI安装数量进行处理
if (installedClis.length === 0) {
...
} else if (installedClis.length === 1) {
const path = require("path");
const pkgPath = require.resolve(`${installedClis[0].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[installedClis[0].binName]
));
} else {
...
}
如果正常安装了一个CLI,会走到if语句的第二个分支中,进而执行了安装好的CLI的package.json中的bin属性中的相应的CLI命令,在webpack-cli中是:
"bin": {
"webpack-cli": "bin/cli.js"
}
下面来到了node_modules/webpack-cli/bin/cli.js文件中,其中主要执行了一个立即执行函数,函数精简后的主干逻辑为:
这里通过引入的node_modules/webpack/lib/webpack.js生成了一个compiler实例,然后执行compiler.run(),run是一个重要的入口后文会详细分析。 我们来看一下这个compiler对象在webpack函数中的生成过程,有些注释没看懂没关系,下面有讲解:
const webpack = (options, callback) => {
//将webpackOptionsSchema和options传入验证配置有效性
const webpackOptionsValidationErrors = validateSchema(
webpackOptionsSchema,
options
);
if (webpackOptionsValidationErrors.length) {
throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
}
let compiler;
if (Array.isArray(options)) {
compiler = new MultiCompiler(
Array.from(options).map(options => webpack(options))
);
} else if (typeof options === "object") {
//在options中补充默认配置
options = new WebpackOptionsDefaulter().process(options);
//demo文件夹目录传入Compiler生成compiler实例
compiler = new Compiler(options.context);
compiler.options = options;
//把NodeEnvironmentPlugin实例通过apply方法加入到compiler对象中
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging
}).apply(compiler);
//对options对象中的plugins迭代执行apply安装插件
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
//执行environment钩子
compiler.hooks.environment.call();
//执行afterEnvironment钩子
compiler.hooks.afterEnvironment.call();
compiler.options = new
//对options.target/optimization/performance相关的插件进行apply()
WebpackOptionsApply().process(options, compiler);
} else {
throw new Error("Invalid argument: options");
}
//如果传入callback,在这里直接执行compiler.run
if (callback) {
if (typeof callback !== "function") {
throw new Error("Invalid argument: callback");
}
if (
options.watch === true ||
(Array.isArray(options) && options.some(o => o.watch))
) {
const watchOptions = Array.isArray(options)
? options.map(o => o.watchOptions || {})
: options.watchOptions || {};
return compiler.watch(watchOptions, callback);
}
compiler.run(callback);
}
//返回compiler实例
return compiler;
};
注释中我们看到了NodeEnvironmentPlugin实例通过apply方法加入到compiler对象中,我们再继续看下NodeEnvironmentPlugin类的apply方法,其中比较关键的一句:
compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin", compiler => {
if (compiler.inputFileSystem === inputFileSystem) inputFileSystem.purge();
});
是通过compiler.hooks.beforeRun.tap方法把回调函数挂载到beforeRun钩子上,webpack函数后面又通过钩子对象的call方法来执行了之前挂载的插件。相关原理可以阅读webpack4.0源码分析之Tapable,此文详细地分析了tapable的机制。
tap是在apply函数中实现的。其实,webpack中的插件都是通过apply的这种方式,来添加到compiler对象中的。而对options对象中的plugins数组也是迭代执行apply来安装插件到compiler中的。
这里我们还要注意的一点是在WebpackOptionsApply().process()中执行了:
new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);
再来看下EntryOptionPlugin的源码:
//node_modules/webpack/lib/EntryOptionPlugin.js
const itemToPlugin = (context, item, name) => {
if (Array.isArray(item)) {
return new MultiEntryPlugin(context, item, name);
}
return new SingleEntryPlugin(context, item, name);
};
module.exports = class EntryOptionPlugin {
/**
* @param {Compiler} compiler the compiler instance one is tapping into
* @returns {void}
*/
apply(compiler) {
compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
if (typeof entry === "string" || Array.isArray(entry)) {
itemToPlugin(context, entry, "main").apply(compiler);
} else if (typeof entry === "object") {
for (const name of Object.keys(entry)) {
itemToPlugin(context, entry[name], name).apply(compiler);
}
} else if (typeof entry === "function") {
new DynamicEntryPlugin(context, entry).apply(compiler);
}
return true;
});
}
};
通过apply方法安装插件,挂载到EntryOptionPlugin钩子中,回调函数中判断entry分别为字符串或数组、对象、函数的三种情况,本文简单分析,默认entry为一个字符串。可以分析出,最后挂载了SingleEntryPlugin插件。我们再来看下SingleEntryPlugin源码:
//node_modules/webpack/lib/SingleEntryPlugin.js
apply(compiler) {
compiler.hooks.compilation.tap(
"SingleEntryPlugin",
(compilation, { normalModuleFactory }) => {
compilation.dependencyFactories.set(
SingleEntryDependency,
normalModuleFactory
);
}
);
compiler.hooks.make.tapAsync(
"SingleEntryPlugin",
(compilation, callback) => {
const { entry, name, context } = this;
const dep = SingleEntryPlugin.createDependency(entry, name);
compilation.addEntry(context, dep, name, callback);
}
);
}
其中挂载了两处钩子,分别是compilation和make。我们主要看make这一处,回调函数里面执行了compilation.addEntry()方法,这点一会用到。
2.webpack生成compiler实例
上文中我们看到了插件通过apply方法把自己挂载到了compiler的钩子上。
Compiler类继承自Tapable,并且在构造函数中初始化了很多tapable的钩子函数,run函数中通过call(不是JavaScript原生的call)也调用了一开始定义的钩子函数。我们再看一下compiler的构造函数,其中摘出了Compiler类的关键逻辑:
//node_modules/webpack/lib/Compiler.js
class Compiler extends Tapable {
constructor(context) {
super();
//compiler对象的一系列钩子
this.hooks = {
...
beforeRun: new AsyncSeriesHook(["compiler"]),
run: new AsyncSeriesHook(["compiler"]),
emit: new AsyncSeriesHook(["compilation"]),
assetEmitted: new AsyncSeriesHook(["file", "content"]),
afterEmit: new AsyncSeriesHook(["compilation"]),
thisCompilation: new SyncHook(["compilation", "params"]),
compilation: new SyncHook(["compilation", "params"]),
normalModuleFactory: new SyncHook(["normalModuleFactory"]),
contextModuleFactory: new SyncHook(["contextModulefactory"]),
beforeCompile: new AsyncSeriesHook(["params"]),
compile: new SyncHook(["params"]),
make: new AsyncParallelHook(["compilation"]),
afterCompile: new AsyncSeriesHook(["compilation"]),
...
};
//compiler对象的一系列属性
...
}
run(callback) {
const startTime = Date.now();
this.running = true;
this.hooks.beforeRun.callAsync(this, err => {
if (err) return finalCallback(err);
this.hooks.run.callAsync(this, err => {
if (err) return finalCallback(err);
this.readRecords(err => {
if (err) return finalCallback(err);
this.compile(onCompiled);
});
});
});
}
compile(callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
if (err) return callback(err);
this.hooks.compile.call(params);
const compilation = this.newCompilation(params);
this.hooks.make.callAsync(compilation, err => {
if (err) return callback(err);
compilation.finish(err => {
if (err) return callback(err);
compilation.seal(err => {
if (err) return callback(err);
this.hooks.afterCompile.callAsync(compilation, err => {
if (err) return callback(err);
return callback(null, compilation);
});
});
});
});
});
}
}
webpack-cli中执行了这里的run(),在run()中执行了beforeRun和run这两个钩子,之后run钩子的回调中,执行了compile方法。
3.执行make钩子
在webpack返回了compiler实例以后,webpack-cli就执行了compiler.run(),run()触发compile(),而在compile()中又发生了什么,上面的代码中可以看到compile()中执行的钩子如下:
beforeCompile->compile->make->afterCompile
这几个就是整个打包流程中最主要的钩子节点。而其中最主要的打包流程就是在make之中,因为上文分析过,为打包流程添加入口其实就是在make这个钩子通过SingleEntryPlugin插件来实现的。
而在make之前,执行了newCompilation()返回了一个compilation实例:
//node_modules/webpack/lib/Compiler.js
createCompilation() {
return new Compilation(this);
}
newCompilation(params) {
const compilation
this.createCompilation();
compilation.fileTimestamps = this.fileTimestamps;
compilation.contextTimestamps = this.contextTimestamps;
compilation.name = this.name;
compilation.records = this.records;
compilation.compilationDependencies = params.compilationDependencies;
this.hooks.thisCompilation.call(compilation, params);
this.hooks.compilation.call(compilation, params);
return compilation;
}
在这里再说明一下compiler和compilation之间的关系,compilation是在compiler.compile中实例化的一个对象。在webpack打包的过程中只会生成一个compile类,但是每次打包的操作都需要一个compilation来具体完成。字面意义上理解,compiler是一个打包者,而compilation是这个打包者发出的一个动作。
下面来到了make钩子,上文分析过make会触发compilation.addEntry():
//node_modules/webpack/lib/Compilation.js
addEntry(context, entry, name, callback) {
...
this._addModuleChain(
...
);
}
其中主要调用了_addModuleChain():
//node_modules/webpack/lib/Compilation.js
_addModuleChain(context, dependency, onModule, callback) {
...
this.semaphore.acquire(() => {
moduleFactory.create(
{
contextInfo: {
issuer: "",
compiler: this.compiler.name
},
context: context,
dependencies: [dependency]
},
(err, module) => {
...
const afterBuild = () => {
if (addModuleResult.dependencies) {
this.processModuleDependencies(module, err => {
if (err) return callback(err);
callback(null, module);
});
} else {
return callback(null, module);
}
};
...
this.buildModule(module, false, null, null, err => {
if (err) {
this.semaphore.release();
return errorAndCallback(err);
}
if (currentProfile) {
const afterBuilding = Date.now();
currentProfile.building = afterBuilding - afterFactory;
}
this.semaphore.release();
afterBuild();
});
}
);
});
}
_addModuleChain()将模块添加到依赖列表中,同时进行模块构建。构建时执行buildModule(),回调是afterBuild()。再看下buildModule():
//node_modules/webpack/lib/Compilation.js
buildModule(module, optional, origin, dependencies, thisCallback) {
...
module.build(
this.options,
this,
this.resolverFactory.get("normal", module.resolveOptions),
this.inputFileSystem,
error => {
...
}
);
}
buildModule()中执行了module.build,这里就涉及到了webpack中模块的种类,在源码目录中我们可以看到有:MultiModule、NormalModule、ExternalModule、DelegatedModule。笔者其实也还没有深入分析这几种模块都是什么情况下引入的,我们先拿最简单的NormalModule分析在build中都做了什么:
// node_modules/webpack/lib/Parser.js
const acorn = require("acorn");
//node_modules/webpack/lib/NormalModule.js
const { getContext, runLoaders } = require("loader-runner");
...
build(options, compilation, resolver, fs, callback) {
return this.doBuild(options, compilation, resolver, fs, err => {
...
try {
//分析AST语义树
const result = this.parser.parse(
this._ast || this._source.source(),
{
current: this,
module: this,
compilation: compilation,
options: options
},
(err, result) => {
if (err) {
handleParseError(err);
} else {
handleParseResult(result);
}
}
);
if (result !== undefined) {
// parse is sync
handleParseResult(result);
}
} catch (e) {
handleParseError(e);
}
});
}
doBuild(options, compilation, resolver, fs, callback) {
const loaderContext = this.createLoaderContext(
resolver,
options,
compilation,
fs
);
runLoaders(
{
resource: this.resource,
loaders: this.loaders,
context: loaderContext,
readResource: fs.readFile.bind(fs)
},
(err, result) => {
...
this._ast =
typeof extraInfo === "object" &&
extraInfo !== null &&
extraInfo.webpackAST !== undefined
? extraInfo.webpackAST
: null;
return callback();
}
);
}
在normalModule.build方法中会先调用自身doBuild()。doBuild()通过loader-runner库中引入的runLoaders方法选用合适的loader去加载resource,目的是为了将这份resource转换为JS模块。而在doBuild的回调函数中又会看到this.parser.parse,主要是通过相应的loader加载完模块以后,来分析模块的依赖关系。用到了一个第三方库acorn提供的parse方法对JS源代码进行语法解析。
至此就可以执行buildModule()的回调afterBuild(),摘了一个网上的图,总结的比较好:
4.封装和输出
至此,webpack收集完整了该模块的信息和依赖项,接下来就是如何进一步打包封装模块了。compilation.seal的步骤比较多,先封闭模块,生成资源,这些资源保存在compilation.assets, compilation.chunks。然后调用compilation.createChunkAssets方法把所有依赖项通过对应的模板 render出一个拼接好的字符串:
//node_modules/webpack/lib/Compilation.js
createChunkAssets() {
...
for (let i = 0; i < this.chunks.length; i++) {
const chunk = this.chunks[i];
chunk.files = [];
let source;
let file;
let filenameTemplate;
try {
const template = chunk.hasRuntime()
? this.mainTemplate
: this.chunkTemplate;
// manifest是数组结构,每个manifest元素都提供了 `render` 方法,提供后续的源码字符串生成服务。至于render方法何时初始化的,在`./lib/MainTemplate.js`中
const manifest = template.getRenderManifest({
chunk,
hash: this.hash,
fullHash: this.fullHash,
outputOptions,
moduleTemplates: this.moduleTemplates,
dependencyTemplates: this.dependencyTemplates
});
for (const fileManifest of manifest) {
...
this.emitAsset(file, source, assetInfo);
chunk.files.push(file);
this.hooks.chunkAsset.call(chunk, file);
alreadyWrittenFiles.set(file, {
hash: usedHash,
source,
chunk
});
}
}
}
}
在seal执行后,关于模块所有信息以及打包后源码信息都存在内存中,是时候将它们输出为文件了。然后,compile()会执行onCompiled()回调:
//node_modules/webpack/lib/Compiler.js
const onCompiled = (err, compilation) => {
...
process.nextTick(() => {
...
this.emitAssets(compilation, err => {
...
this.emitRecords(err => {
this.hooks.done.callAsync(stats, err => {
...
});
});
});
});
};
this.cache.endIdle(err => {
if (err) return finalCallback(err);
this.hooks.beforeRun.callAsync(this, err => {
this.hooks.run.callAsync(this, err => {
this.readRecords(err => {
this.compile(onCompiled);
});
});
});
});
在onCompiled()中,会执行compiler.emitAssets(),在compiler.emitAssets中会先调用this.hooks.emit生命周期,之后根据webpack config文件的output配置的path属性,将文件输出到指定的文件夹。至此,你就可以在./debug/dist中查看到调试代码打包后的文件了。
参考:
《Webpack源码解读:理清编译主流程》juejin.cn/post/684490…
《「搞点硬货」从源码窥探Webpack4.x原理》zhuanlan.zhihu.com/p/102424286
《webpack4源码分析》juejin.cn/post/684490…
《从Webpack源码探究打包流程,萌新也能看懂~》juejin.cn/post/684490…
《webpack-源码分析》echizen.github.io/tech/2019/0…
《Webpack揭秘——走向高阶前端的必经之路》imweb.io/topic/5baca…