最近在一直在为面试作准备,搜了不少大佬记录的面试经验和面试内容,对本身不太熟悉和已经记忆模糊的知识点内容进行复习巩固,争取可以有一个好的状态。这篇文章以个人经验讲述了我是如何从源码的角度了解到 webpack 插件机制,也简单描述了 webpack 编译构建的机制。css
使用 vscode 调试功能,运行项目打包程序,一步一步走 webpack(version: 3.10.0)执行代码。node
先来看看 webpack 函数源码:webpack
function webpack(options, callback) {
const webpackOptionsValidationErrors = validateSchema(webpackOptionsSchema, options);
if(webpackOptionsValidationErrors.length) {
throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
}
let compiler;
if(Array.isArray(options)) {
compiler = new MultiCompiler(options.map(options => webpack(options)));
} else if(typeof options === "object") {
// TODO webpack 4: process returns options
new WebpackOptionsDefaulter().process(options);
compiler = new Compiler();
compiler.context = options.context;
compiler.options = options;
new NodeEnvironmentPlugin().apply(compiler);
if(options.plugins && Array.isArray(options.plugins)) {
compiler.apply.apply(compiler, options.plugins);
}
compiler.applyPlugins("environment");
compiler.applyPlugins("after-environment");
compiler.options = new WebpackOptionsApply().process(options, compiler);
} else {
throw new Error("Invalid argument: options");
}
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);
}
return compiler;
}
复制代码
const webpackOptionsValidationErrors = validateSchema(webpackOptionsSchema, options);
if(webpackOptionsValidationErrors.length) {
throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
}
复制代码
validateSchema
是一个依赖 ajv(JSON 模式验证器) 插件的对 webpack 配置进行验证的函数,须要提供一份 webpack 配置的 JSON 格式的验证描述 webpackOptionsSchema
和咱们项目的配置信息 options
,先用 ajv 的 compile
方法编译 webpackOptionsSchema
获得验证器,再用验证器验证 options
并返回验证结果。相似于 React 中使用的 prop-types
验证父组件传给子组件的属性和 Vue 中的 props 自定义验证。git
options
肯定 compiler 对象这里须要先着重介绍下 compiler 对象,它对咱们了解 webpack 的构建机制和接下来的讲解相当重要,须要理解它究竟是什么,有什么做用。先借用官网的一些介绍:github
compiler 对象表明了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性创建,并在全部可操做的设置中被配置,包括原始配置,loader 和插件。当在 webpack 环境中应用一个插件时,插件将收到一个编译器对象的引用。可使用它来访问 webpack 的主环境。web
compiler 对象在 webpack 构建过程当中表明着整个 webpack 环境,包含上下文、项目配置信息、执行、监听、统计等等一系列的信息,提供给 loader 和插件使用。它继承于 Tapable(Tapable 是 webpack 的一个底层库,相似于 NodeJS 的 EventEmitter 类),使用事件的发布 compiler.applyPlugins('eventName')
订阅compiler.plugin('eventName', callback)
模式注册 new WebpackPlugin().apply(compiler)
全部插件,插件必须提供 apply 方法给 webpack 完成注册流程,插件在 apply 方法内作一些初始化操做并监听 webpack 构建过程当中的生命周期事件,等待构建时生命周期事件的发布。面试
全部插件都会在构建方法 compiler.run(callback)
以前注册,当 webpack 构建到某个阶段就会发布一个生命周期事件,此时全部订阅了当前发布的生命周期事件的插件会按照注册顺序一个一个执行订阅时提供的回调函数,回调函数的参数是与发布的生命周期事件相对应的参数,好比经常使用的 compilation 生命周期事件回调函数参数就包含 compilation 对象(此对象也是 webpack 构建机制的重要成员),entry-option 生命周期事件回调函数参数是 context(项目上下文路径)和 entry(项目配置的入口对象)。另外,插件若是须要异步执行编译,则还会提供一个回调函数做为监听回调函数的参数,异步编译完成必须调用回调函数。json
简单点说,webpack 的构建包含不少个阶段,每一个阶段都会发布对应的生命周期事件,插件须要提供 apply
方法注册并在此方法内监听指定的生命周期事件,事件发布后会顺序执行监听的回调函数并提供相对应的参数。数组
OK,了解完 compiler 对象后继续看代码。app
if(Array.isArray(options)) {
compiler = new MultiCompiler(options.map(options => webpack(options)));
} else if(typeof options === "object") {
// ...
compiler = new Compiler();
// ...
}
复制代码
这里有一层判断决定 compiler 对象是 MultiCompiler 类仍是 Compiler 类的实例,两者的关系实际上是一层包装关系,从代码能够看出,MultiCompiler 类的参数是多个 Compiler 实例成员组成的数组,再进入到 MultiCompiler.js 查看源码会发现,在 MultiCompiler 的构造函数中,实例有个 compilers 属性指向这个由多个 Compiler 实例成员组成的数组,以及遍历 compilers 数组给每一个 compiler 成员注册(监听) done
和 invalid
两个生命周期事件。
对于 outputPath、inputFileSystem、outputFileSystem 这三个属性,会使用取值函数(getter)和存值函数(setter)进行拦截(outputPath 只有取值函数),查看 Compiler 源码 Compiler 类的构造函数,会发现这三个属性都是 Compiler 实例上的属性,再看 MultiCompiler 中这三个属性的存取值函数会发现,都是在遍历 MultiCompiler 实例的 compilers 属性对每个 compiler 成员作相应的存取值操做(其中 inputFileSystem 和 outputFileSystem 的取值函数是抛出错误),也就是说对 MultiCompiler 实例的 inputFileSystem 和 outputFileSystem 属性赋值其实就是对全部 Compiler 实例的 inputFileSystem 和 outputFileSystem 属性赋值。MultiCompiler 类还覆写了 Compiler 类中的 watch、run、purgeInputFileSystem 三个方法,无一例外也都是遍历 compilers 让每一个 compiler 成员执行与之相对应的方法。
通常状况下,咱们项目配置的 options 是一个 object
,至于什么状况下会使用到数组,我我的见解是一个大的项目中包含多个小的子项目,须要可以单独打包小的子项目,也须要可以一次打包整个大的项目,这时候 Array.isArray(options) === true
才应用到 MultiCompiler。
new WebpackOptionsDefaulter().process(options);
复制代码
咱们以 typeof options === "object"
常规项目为例,代码往下执行,首先建立了 WebpackOptionsDefaulter 实例,而后立刻执行 process 方法并传入 options 做为参数。查看 WebpackOptionsDefaulter 源码 会发现,它自己只有一个构造函数,构造函数内大规模使用 this.set()
方法初始化 webpack options 默认配置,这个 set 方法是来自它所继承的 OptionsDefaulter 类,执行的 process 方法也是出自 OptionsDefaulter 类,能够说 WebpackOptionsDefaulter 类只是一层外壳,设置全部的 webpack 默认配置信息,借由 procss 方法将 webpack 默认的配置信息与项目配置信息融合,提供出接下来须要使用的 options。
compiler = new Compiler();
compiler.context = options.context;
compiler.options = options;
复制代码
初始化 webpack options 后,建立 compiler 实例并设置 context(项目上下文路径)和 options(项目配置)。
new NodeEnvironmentPlugin().apply(compiler);
复制代码
上文对 compiler 对象的介绍已经清楚的解释了上面一行代码的行为,就是注册一个 NodeEnvironmentPlugin 插件,查看源码发现很是简单,以下:
class NodeEnvironmentPlugin {
apply(compiler) {
compiler.inputFileSystem = new CachedInputFileSystem(new NodeJsInputFileSystem(), 60000);
const inputFileSystem = compiler.inputFileSystem;
compiler.outputFileSystem = new NodeOutputFileSystem();
compiler.watchFileSystem = new NodeWatchFileSystem(compiler.inputFileSystem);
compiler.plugin("before-run", (compiler, callback) => {
if(compiler.inputFileSystem === inputFileSystem)
inputFileSystem.purge();
callback();
});
}
}
复制代码
apply 方法执行插件初始化操做修改了 compiler 的 inputFileSystem、outputFileSystem、watchFileSystem 这三个属性的值,而后监听 before-run 事件,顾名思义,这个事件是在 compiler.run(callback)
函数执行会被发布的。查看回调函数内部代码,执行了 callback()
方法,马上能想到这是一个异步编译回调,以前有一层 inputFileSystem 引用的判断,若是 before-run 事件发布以前 compiler 的 inputFileSystem 被修改从新赋值,则不作任何操做直接执行 callback;若是没有被修改从新赋值,则运行 purge 方法。大意就是若是有其它插件(指项目配置的插件)提供了 inputFileSystem 对象,就用其它插件的,若是没有,那就由我来接管了。
if(options.plugins && Array.isArray(options.plugins)) {
compiler.apply.apply(compiler, options.plugins);
}
复制代码
首先判断是否有 plugins 属性而且是否为数组,判断语句内的语法稍微有一点绕,咋一眼看上去有些懵,但稍微想一下应该就能明白,就是执行 compiler 的 apply 方法而且绑定 this 为 compiler 对象,再传入 options.plugins
(项目配置的全部插件)做为参数。这里使用 apply 方法的目的其实不是为了绑定 this,由于自己 compiler.apply()
方法执行上下文中的 this 指向的就是 compiler,这里主要目的是为了把 options.plugins
解构为一个一个的参数。
Tapable.prototype.apply = function apply() {
for(var i = 0; i < arguments.length; i++) {
arguments[i].apply(this);
}
};
复制代码
由于 Compiler 类继承自 Tapable 类,Compiler 实例上的 apply 方法调用的是 Tapable.prototype.apply
,经过上面代码能够清楚的看到,之因此要解构 options.plugins
是由于要遍历 arguments 对象,让 arguments 成员(插件实例)调用自身的 apply 方法(不是 Function.prototype.apply 方法)执行注册流程,传入 this 也就是 compiler 做为参数。
至此,插件注册的流程已经很是清晰明了,对于开始动手写一个 webpack 插件应该没有什么畏惧啦。
compiler.applyPlugins("environment");
compiler.applyPlugins("after-environment");
复制代码
若是已经理解 compiler 对象那上面两行代码是能够略过的,就是发布 compiler.applyPlugins
两个生命周期事件:environment 和 after-environment,若是项目配置的插件中有监听这两个事件的插件则会执行监听回调函数,只提供了默认的 compiler 做为参数,没有任何其余参数。
compiler.options = new WebpackOptionsApply().process(options, compiler);
复制代码
这里是对 compiler.options
从新赋值操做,那意思就很明白了,又要对 options 参数作一系列的操做,为何说又?由于以前执行 new WebpackOptionsDefaulter().process(options);
已经对 options 作过一次初始化操做,融合了 webpack 的默认配置与项目配置的结果,那此次操做 options 又是为了什么呢?查看 WebpackOptionsApply 源码 发现 WebpackOptionsApply 继承自 OptionsApply,再查看 OptionsApply 源码 发现 OptionsApply 无关紧要...,不太清楚为何要写这么一个空类,或许是为 webpack v4 作准备,这儿咱们就先无论,意义不大。再回到 WebpackOptionsApply 类的 process 方法,哗啦啦的一串,将近 300 行,这里我就不贴代码了,我大概描述一下 process 方法内作了哪些事情。
把一些 options 上的属性赋值给 compiler 对象
根据 options.target
的值注册相应的 webpack 内部插件
options.target
配置的意思是告诉 webpack 构建应用于什么环境的代码,它的默认值是 web
,另外还有 webworker
、node
、async-node
、node-webkit
、atom
、electron
、electron-main
、electron-renderer
。
根据 options 的配置肯定是否要注册一些内部插件
好比若是配置了 externals 属性须要则注册 ExternalsPlugin 插件
肯定 compiler.resolvers
三个属性 normal
、context
、loader
的值
发布三个生命周期事件:entry-option
、after-plugins
、after-resolvers
返回 options
大体就是在完善 compiler 对象,根据当前项目配置应用一些相对应的内部插件,从这里能够看出,webpack 内部也大量运用插件机制来实现编译构建,插件机制让 webpack 变得灵活而强大。
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);
}
return compiler;
复制代码
接下来,判断是否有 callback 参数,若是没有则直接返回 compiler 对象,注意!此时 webpack 并无执行构建程序,也不会执行,由于构建程序 compiler.run(callback)
方法只有当有 callback 参数时才会执行,而且 callback 必须为函数。若是开启 watch 模式,则 webpack 会监听文件变化,当文件发生变更则会触发从新编译,像 webpack-dev-server 和 webpack-dev-middleware 里 watch 模式是默认开启的,方便进行开发。
最后执行 compiler.run(callback);
表示开始构建,至此,webpack 构建前的初始化操做已经所有完成,接下来要探索的就是 run 方法是如何执行 webpack 构建的。
本文篇幅已经很长,run 方法内的构建过程不是三言两语可以描述清楚,这里也不打算细说,大概罗列几个关键事件节点:
compile: 开始编译
make: 从入口点分析模块及其依赖的模块并建立这些模块对象
build-module: 构建模块
after-compile: 完成构建
after-compile 完成构建
emit: 把各个chunk输出到结果文件
after-emit: 完成输出
另外 webpack 构建还有一个关键对象 compilation,上文介绍 compiler 时有提到,他们俩是理解和扩展 webpack 引擎的关键,是 webpack 插件必不可缺的组成部分。
compilation
对象表明了一次单一的版本构建和生成资源。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,一次新的编译将被建立,从而生成一组新的编译资源。一个编译对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。编译对象也提供了不少关键点回调供插件作自定义处理时选择使用。
compiler 是 webpack 环境的表明,compilation 则是 webpack 构建内容的表明,它包含了每一个构建环节及输出环节所对应的方法,存放着全部 module、chunk、asset 以及用来生成最后打包文件的 template 的信息。
最后,附上一张淘宝 FED 团队在《细说 webpack 之流程篇》 一文中的 webpack 总体流程图: