想象一下,为了拥有美好的生活,你必须穿越一片危险的丛林。你能够安全地留在原地,过着普通的生活;你也能够冒险穿越丛林,过着绝妙的生活。你将如何对待这一选择?
《原则》瑞·达利欧前端
观感度:🌟🌟🌟🌟🌟node
口味:部队火锅webpack
烹饪时间:15mingit
随着多终端设备的迅速普及,Web前端开发的复杂性和应用场景日益扩大,Webpack在前端构建演变的工程化浪潮中担当起了针对不一样应用场景打包的大任。 现在,Webpack可谓是JavaScript社区最伟大的项目之一。github
本文力争从源码层面窥探Webpack的实现原理。文中出示了核心的代码块并注释了相应的path,若是你也想揭开Webpack神秘的面纱,那就打开一份源码跟随本文一块儿享受一次秃头的快乐。web
文章过长,记得点赞收藏~ 单押X2
算法
Webpack的本质是什么呢?可能有的同窗已经知道了,npm
Webpack本质上一种基于事件流的编程范例,其实就是一系列的插件运行。
编程
Webpack主要使用Compiler
和Compilation
两个类来控制Webpack的整个生命周期。他们都继承了Tapabel而且经过Tapabel来注册了生命周期中的每个流程须要触发的事件。数组
Tapabel是一个相似于 Node.js 的 EventEmitter 的库,主要是控制钩子函数的发布与订阅,是Webpack插件系统的大管家。
Tapable库为插件提供了不少 Hook以便挂载。
const {
SyncHook, // 同步钩子
SyncBailHook, // 同步熔断钩子
SyncWaterfallHook, // 同步流水钩子
SyncLoopHook, // 同步循环钩子
AsyncParalleHook, // 异步并发钩子
AsyncParallelBailHook, // 异步并发熔断钩子
AsyncSeriesHook, // 异步串行钩子
AsyncSeriesBailHook, // 异步串行熔断钩子
AsyncSeriesWaterfallHook // 异步串行流水钩子
} = require("tapable");
复制代码
Tabpack 提供了同步&异步
绑定钩子的方法,方法以下所示:
Async | Sync |
---|---|
绑定:tapAsync/tapPromise/tap | 绑定:tap |
执行:callAsync/promise | 执行:call |
const demohook = new SyncHook(["arg1", "arg2", "arg3"]);
// 绑定事件到webpack事件流
demohook.tap("hook1",(arg1, arg2, arg3) => console.log(arg1, arg2, arg3)) // 1 2 3
// 执行绑定的事件
demohook.call(1,2,3)
复制代码
追本溯源,第一步咱们要找到Webpack的入口文件。
当经过命令行启动Webpack后,npm会让命令行工具进入node_modules.bin 目录。
而后查找是否存在 webpack.sh 或者 webpack.cmd 文件,若是存在,就执行它们,不存在就会抛出错误。
实际的入口文件是:node_modules/webpack/bin/webpack.js
,让咱们来看一下里面的核心函数。
// node_modules/webpack/bin/webpack.js
// 正常执行返回
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);
// 根据安装数量进行处理
if (installedClis.length === 0) {...} else if
(installedClis.length === 1) {...} else {...}
复制代码
启动后,Webpack最终会找到 webpack-cli /webpack-command
的 npm 包,而且 执行 CLI。
搞清楚了Webpack启动的入口文件后,接下来让咱们把目光转移到webpack-cli,看看它作了哪些事儿。
webpack-cli
会处理不须要通过编译的命令。
// node_modules/webpack-cli/bin/cli.js
const {NON_COMPILATION_ARGS} = require("./utils/constants");
const NON_COMPILATION_CMD = process.argv.find(arg => {
if (arg === "serve") {
global.process.argv = global.process.argv.filter(a => a !== "serve");
process.argv = global.process.argv;
}
return NON_COMPILATION_ARGS.find(a => a === arg);
});
if (NON_COMPILATION_CMD) {
return require("./utils/prompt-command")(NON_COMPILATION_CMD,...process.argv);
}
复制代码
webpack-cli
提供的不须要编译的命令以下。
// node_modules/webpack-cli/bin/untils/constants.js
const NON_COMPILATION_ARGS = [
"init", // 建立一份webpack配置文件
"migrate", // 进行webpack版本迁移
"add", // 往webpack配置文件中增长属性
"remove", // 往webpack配置文件中删除属性
"serve", // 运行webpack-serve
"generate-loader", // 生成webpack loader代码
"generate-plugin", // 生成webpack plugin代码
"info" // 返回与本地环境相关的一些信息
];
复制代码
webpack-cli 使用命令行工具包yargs。
// node_modules/webpack-cli/bin/config/config-yargs.js
const {
CONFIG_GROUP,
BASIC_GROUP,
MODULE_GROUP,
OUTPUT_GROUP,
ADVANCED_GROUP,
RESOLVE_GROUP,
OPTIMIZE_GROUP,
DISPLAY_GROUP
} = GROUPS;
复制代码
webpack-cli对配置文件和命令行参数进行转换最终生成配置选项参数 options,最终会根据配置参数实例化webpack对象,而后执行构建流程。
除此以外,让咱们回到node_modules/webpack/lib/webpack.js
里来看一下Webpack还作了哪些准备工做。
// node_modules/webpack/lib/webpack.js
const webpack = (options, callback) => {
...
options = new WebpackOptionsDefaulter().process(options);
compiler = new Compiler(options.context);
new NodeEnvironmentPlugin().apply(compiler);
...
compiler.options = new WebpackOptionsApply().process(options, compiler);
...
webpack.WebpackOptionsDefaulter = WebpackOptionsDefaulter;
webpack.WebpackOptionsApply = WebpackOptionsApply;
...
webpack.NodeEnvironmentPlugin = NodeEnvironmentPlugin;
}
复制代码
WebpackOptionsDefaulter的功能是设置一些默认的Options(代码比较多不贴了,你们自行查看node_modules/webpack/lib/WebpackOptionsDefaulter.js
)。
// node_modules/webpack/lib/node/NodeEnvironmentPlugin.js
class NodeEnvironmentPlugin {
apply(compiler) {
...
compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin", compiler => {
if (compiler.inputFileSystem === inputFileSystem) inputFileSystem.purge();
});
}
}
复制代码
从上面的代码咱们能够知道,NodeEnvironmentPlugin
插件监听了beforeRun钩子,它的做用是清除缓存。
WebpackOptionsApply
会将全部的配置options参数转换成webpack内部插件。
使用默认插件列表
// node_modules/webpack/lib/WebpackOptionsApply.js
new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);
复制代码
实际上,插件最后都会变成compiler
对象上的实例。
接下来让咱们进入EntryOptionPlugin
插件,看看它作了哪些事儿。
// node_modules/webpack/lib/EntryOptionPlugin.js
module.exports = class EntryOptionPlugin {
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;
});
}
};
复制代码
若是是数组,则转换成多个entry来处理,若是是对象则转换成一个个entry来处理。
如上述代码所示。
compiler实例化是在node_modules/webpack/lib/webpack.js
里完成的。经过EntryOptionPlugin
插件进行参数校验。经过WebpackOptionsDefaulter
将传入的参数和默认参数进行合并成为新的options,建立compiler,以及相关plugin,最后经过 WebpackOptionsApply
将全部的配置options参数转换成Webpack内部插件。
不要急,还没完事。
再次来到咱们的node_modules/webpack/lib/webpack.js
中
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后会根据options的watch判断是否启动了watch
,若是启动watch了就调用compiler.watch
来监控构建文件,不然启动compiler.run
来构建文件。
首先会实例化NormalModuleFactory
和ContextModuleFactory
。而后进入到run
方法。
// node_modules/webpack/lib/Compiler.js
run(callback) {
...
// beforeRun 如上文NodeEnvironmentPlugin插件清除缓存
this.hooks.beforeRun.callAsync(this, err => {
if (err) return finalCallback(err);
// 执行run Hook开始编译
this.hooks.run.callAsync(this, err => {
if (err) return finalCallback(err);
this.readRecords(err => {
if (err) return finalCallback(err);
// 执行compile
this.compile(onCompiled);
});
});
});
}
复制代码
在执行this.hooks.compile
以前会执行this.hooks.beforeCompile
,来对编译以前须要处理的插件进行执行。紧接着this.hooks.compile
执行后会实例化Compilation
对象。
// node_modules/webpack/lib/compiler.js
compile(callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
if (err) return callback(err);
// 进入compile阶段
this.hooks.compile.call(params);
const compilation = this.newCompilation(params);
// 进入make阶段
this.hooks.make.callAsync(compilation, err => {
if (err) return callback(err);
compilation.finish(err => {
if (err) return callback(err);
// 进入seal阶段
compilation.seal(err => {
if (err) return callback(err);
this.hooks.afterCompile.callAsync(compilation, err => {
if (err) return callback(err);
return callback(null, compilation);
})
})
})
})
})
}
复制代码
一个新的Compilation
建立完毕,将从Entry开始读取文件,根据文件类型和配置的Loader对文件进行编译,编译完成后再找出该文件依赖的文件,递归的编译和解析。
咱们来看一下make钩子被监听的地方。
如代码中注释所示,addEntry是make构建阶段真正开始的标志
。
// node_modules/webpack/lib/SingleEntryPlugin.js
compiler.hooks.make.tapAsync(
"SingleEntryPlugin",
(compilation, callback) => {
const { entry, name, context } = this;
cosnt dep = SingleEntryPlugin.createDependency(entry, name);
// make构建阶段开始标志
compilation.addEntry(context, dep, name, callback);
}
)
复制代码
addEntry实际上调用了_addModuleChain
方法,_addModuleChain方法将模块添加到依赖列表中去,同时进行模块构建。构建时会执行以下函数。
// node_modules/webpack/lib/Compilation.js
// addEntry -> addModuleChain
_addModuleChain(context, dependency, onModule, callback) {
...
this.buildModule(module, false, null, null, err => {
...
})
...
}
复制代码
若是模块构建完成,会触发finishModules
。
// node_modules/webpack/lib/Compilation.js
finish(callback) {
const modules = this.modules;
this.hooks.finishModules.callAsync(modules, err => {
if (err) return callback(err);
for (let index = 0; index < modules.length; index++) {
const module = modules[index];
this.reportDependencyErrorsAndWarnings(module, [module]);
}
callback();
})
}
复制代码
Module包括NormalModule(普通模块)
、ContextModule(./src/a ./src/b)
、ExternalModule(module.exports=jQuery)
、DelegatedModule(manifest)
以及MultiModule(entry:['a', 'b'])
。
本文以NormalModule(普通模块)为例子,看一下构建(Compilation
)的过程。
// node_modules/webpack/lib/NormalModule.js
const { getContext, runLoaders } = require("loader-runner");
doBuild(){
...
runLoaders(
...
)
...
}
...
try {
const result = this.parser.parse()
}
复制代码
doBuild
会去加载资源,doBuild中会传入资源路径和插件资源去调用loader-runner
插件的runLoaders方法去加载和执行loader。
// node_modules/webpack/lib/Parser.js
const acorn = require("acorn");
复制代码
使用acorn
解析转换后的内容,输出对应的抽象语法树(AST)。
// node_modules/webpack/lib/Compilation.js
this.hooks.buildModule.call(module);
...
if (error) {
this.hooks.failedModule.call(module, error);
return callback(error);
}
this.hooks.succeedModule.call(module);
return callback();
复制代码
成功就触发succeedModule
,失败就触发failedModule
。
最终将上述阶段生成的产物存放到Compilation.js的this.modules = [];
上。
完成后就到了seal阶段。
这里补充介绍一下Chunk生成的算法。
1.webpack首先会将entry中对应的module都生成一个新的chunk。
2.遍历module的依赖列表,将依赖的module也加入到chunk中。
3.若是一个依赖module是动态引入的模块,会根据这个module建立一个新的chunk,继续遍历依赖。
4.重复上面的过程,直至获得全部的chunk。
全部模块及其依赖的模块都经过Loader转换完成,根据依赖关系开始生成Chunk。
seal
阶段也作了大量的的优化工做,进行了hash的建立以及对内容进行生成(createModuleAssets
)。
// node_modules/webpack/lib/Compilation.js
this.createHash();
this.modifyHash();
this.createModuleAssets();
复制代码
// node_modules/webpack/lib/Compilation.js
createModuleAssets(){
for (let i = 0; i < this.modules.length; i++) {
const module = this.modules[i];
if (module.buildInfo.assets) {
for (const assetName of Object.keys(module.buildInfo.assets)) {
const fileName = this.getPath(assetName);
this.assets[fileName] = module.buildInfo.assets[assetName];
this.hooks.moduleAsset.call(module, fileName);
}
}
}
}
复制代码
seal阶段经历了不少的优化,好比tree shaking
就是在这个阶段执行。最终生成的代码会存放在Compilation
的assets
属性上。
将输出的内容输出到磁盘,建立目录生成文件,文件生成阶段结束。
// node_modules/webpack/lib/compiler.js
this.hooks.emit.callAsync(compilation, err => {
if (err) return callback(err);
outputPath = compilation.getPath(this.outputPath);
this.outputFileSystem.mkdirp(outputPath, emitFiles);
})
复制代码
为了可以更深刻的理解Webpack的总体流程,咱们能够动手来实现一个简易的Webpack。
简易的Webpack源码地址 仓库地址,欢迎Star~
Webpack在启动阶段对配置参数和命令行参数以及默认参数进行了合并,并进行了插件的初始化工做。完成初始化的工做后调用Compiler的run
开启Webpack编译构建过程,构建主要流程包括compile
、make
、build
、seal
、emit
等阶段。
固然,Webpack源码还包括不少具体的实现细节,经过一篇文章是总结不完的,你们感兴趣的能够进一步学习。
1.看到这里了就点个赞支持下吧,你的赞是我创做的动力。
2.关注公众号前端食堂
,你的前端食堂,记得按时吃饭!
3.后台回复福利,便可得到海量学习资料。