将个人研究成果,画一个简要的流程图,若是您有兴趣看完,回头再看看这个流程图。webpack
图片里的方块中文字的序号,就是运行的顺序。web
写一个插件很简单,以下:segmentfault
class TestPlugin {
apply(compiler) {
console.log('compiler');
compiler.hooks.compilation.tap('TestPlugin', function (compilation) {
console.log('compilation', compilation);
})
}
}
// 导出 Plugin
module.exports = TestPlugin;
复制代码
经过咱们以往对tapable的了解,知道能够经过钩子来监听Tapable类相应的事件,咱们作相应的处理就好了。api
tapable是了解Webpack源码的前置条件,能够阅读《Webpack tapable 使用研究》和《Webpack tapable源码研究》学习。promise
写插件关键的问题不是注册钩子,而是compiler和compilation是啥,钩子给咱们暴露了这两个对象,让咱们任意操做它们。但这两个对象有什么变量,方法,它们的运行机制,咱们尚不清楚,搞懂了这些,才能写出插件。bash
总结,想搞懂插件,必须搞懂源码,了解Webpack运行流程。app
把Webpack比做一个魔术师的箱子,咱们放进去(输入)的是什么?拿出来(输出)的又是什么?异步
输入的是配置文件+源代码,输出的是bundle文件。函数
Webpack的入口是一个webpack的函数,咱们来一下,我将不主要的所有省略,只留下主流程和主要代码:post
webpack = (options) => {
// 1,第一步就是整合options
// options就是配置,有咱们配置文件中的配置,加上Webpack默认的配置。这些配置指导webpack后续如何运行,好比从哪里开始读取源代码文件,bundle文件输出到哪里,如何进行代码分割等等等。
...
// 2, 第二步实例化compiler对象
compiler = new Compiler(options.context);
...
// 3,实例化全部的(内置的和咱们配置的)Webpack插件。调用它们的apply方法。
// 4, 返回compiler对象
return compiler;
}
复制代码
// 使用,这里模仿webpack-cli中的代码,至关于在命令行里输入webpack。
const options = require("./webpack.config.js");
const compiler = webpack(options);
compiler.run();
复制代码
webpack函数的主要逻辑大体如此。核心是生成compiler,返回它。而后外部获得compiler实例后,run它。
webpack函数内部,分四步。这里说说第三步,实例化插件。插件被不被实例化,配置是能够控制的。请看下面的代码:
// WebpackOptionsApply.js
if (options.optimization.removeAvailableModules) {
const RemoveParentModulesPlugin = require("./optimize/RemoveParentModulesPlugin");
new RemoveParentModulesPlugin().apply(compiler);
}
复制代码
若是咱们的配置中optimization.removeAvailableModules不是true,那就不会实例化RemoveParentModulesPlugin插件。
可是,有一个插件,是必需要实例化的,就是NodeEnvironmentPlugin,源码在 compiler = new Compiler(options.context);后就直接写着new NodeEnvironmentPlugin().apply(compiler);
咱们来看一下这个插件的代码:
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.hooks.beforeRun.tap("NodeEnvironmentPlugin", compiler => {
if (compiler.inputFileSystem === inputFileSystem) inputFileSystem.purge();
});
}
}
module.exports = NodeEnvironmentPlugin;
复制代码
不用关注这里面每行的意思,咱们了解它的功能是:封装了文件存取的api。
显然这个插件赋予了compiler对象文件存取的能力,这是Webpack必需要有的能力啊,不存取文件无法打包了。因此NodeEnvironmentPlugin是必需要用的插件。
总结就是插件分两种,一种是锦上添花的,譬如各类优化插件。一种是Webpack流程中必需要用的,好比这个NodeEnvironmentPlugin,还有后面要提到的EntryOptionPlugin,都是流程中的一部分。咱们查看流程,须要了解这些必须的插件。
总结:读取配置,实例compiler,实例化和挂载插件。
了解了webpack函数的逻辑,接下来就看compiler中的逻辑了。咱们知道它被实例化后,被调用了run方法。直接看run方法:
// Compiler.js
class Compiler extends Tapable {
constructor(context) {
this.hooks = {
...
beforeRun,
run
}
}
...
run(callback) {
...
this.hooks.beforeRun.callAsync(this, err => {
this.hooks.run.callAsync(this, err => {
...
this.compile(onCompiled);
});
});
}
}
复制代码
run方法的主要逻辑就是,先调用beforeRun钩子,这个钩子咱们知道,上面的NodeEnvironmentPlugin注册了此钩子。为compiler添加了文件存取功能。
接下里是调用run钩子。run钩子调用完,会调用compile方法。
// Compiler.js
class Compiler extends Tapable {
...
createCompilation() {
return new Compilation(this);
}
newCompilation(params) {
const compilation = this.createCompilation();
...
this.hooks.thisCompilation.call(compilation, params);
this.hooks.compilation.call(compilation, params);
return compilation;
}
compile(callback) {
this.hooks.beforeCompile.callAsync(params, err => {
this.hooks.compile.call(params);
const compilation = this.newCompilation(params);
this.hooks.make.callAsync(compilation, err => {
compilation.finish(err => {
compilation.seal(err => {
this.hooks.afterCompile.callAsync(compilation, err => {
return callback(null, compilation);
});
});
});
});
});
}
}
复制代码
能够看到compile函数的核心逻辑就是调用beforeCompile钩子,而后调用compile钩子,而后实例化compilation对象,在实例化的过程当中,调用了thisCompilation和compilation两个钩子。而后执行make钩子。make执行结束后,模块的转换工做结束了,要开始seal封装了。seal结束后,调用afterCompile钩子,从这个afterCompile的语义能够分析出,到这就编译结束了。
咱们再看回run方法,在编译结束以后,回调用onCompiled的一个回调函数,这个回调函数掌管的是输出的事,代码以下:
const onCompiled = (err, compilation) => {
if (this.hooks.shouldEmit.call(compilation) === false) {
...
this.hooks.done.callAsync(stats, err => {
...
});
return;
}
this.emitAssets(compilation, err => {
this.hooks.done.callAsync(stats, err => {
...
});
return;
});
};
复制代码
先调用shouldEmit钩子,编译不成功直接调用done钩子,表示结束。编译成功则调用emitAssets方法,emitAssets内部调用emit钩子,执行文件输出,最后调用done,成功完成一次输出。
总结:启动编译和管理输出。实例化compilation对象并利用make钩子的插件,让其开始工做。模块的编译工做是compilation对象作的。
梳理一下compiler对象的调用栈:run->compile->onCompiled
了解了compiler对象,接下来看compilation对象。
compilation的研究相对耗时一点,由于webpack函数和compiler关联的东西不多,webpack只关联了compiler,compiler只关联了compilation,因此比较容易梳理清楚。
但想弄懂compilation,须要弄清楚compilation,moduleFactory,module,三种对象之间的关系,逻辑上也更复杂一点,接下来一块儿看看吧。
要从compiler的make钩子看起,从上面的compile的方法内看到,实例化compilation对象后,并无对它作什么操做,而是直接调用了make钩子,在钩子挂载的入口相关的插件中,操做了compilation,咱们来看一下:
class SingleEntryPlugin {
constructor(context, entry, name) {
this.context = context;
this.entry = entry;
this.name = name;
}
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);
}
);
}
static createDependency(entry, name) {
const dep = new SingleEntryDependency(entry);
dep.loc = { name };
return dep;
}
}
复制代码
这里使用SingleEntryPlugin做为例子,配置单入口时会使用此插件,插件往make钩子会挂载了回调函数。它不但挂载了make钩子,还挂载了compilation钩子,这个钩子先于make钩子调用,为compilation对象的dependencyFactories中添加了值,这值是一个key-value对,key是SingleEntryDependency,值是normalModuleFactory,normalModuleFactory就是一种modulefactory,咱们后面在构建模块中用到。
再看回make钩子的回调,回调有两个形参数,compilation和callback。这里要注意的一点是,对于异步问题,我平常都喜欢写promise和await,但Webpack中,机会全是callback,习惯看这种表达,才容易看懂源码,如:
// 对于异步,咱们会写
const { err,module } = await moduleFactory.create(params);
// 但源码中都是这样的,在回调中拿到调用的返回值
moduleFactory.create(params,(err,module)=> { ... })
复制代码
从make钩子的插件开始,插件能够操做compilation,调用了compilation的addEntry。编译工做就从入口文件开始了。参数dep就是为了在compilation.dependencyFactories找到normalModuleFactory。
咱们来细看一下上面4步的build方法,内部调用了doBuild,doBuild中调用了runLoaders,从方法名能够看出,此处就是模块构建中使用loader的地方了。
也就是addEntry执行结束、SingleEntryPlugin执行结束、make钩子调用结束。该执行make钩子的回调函数了。
make钩子回调中调用了compilation的seal方法。开始了Chunk的构建和打包的优化过程。能够说看到这里,咱们对Webpack的执行流程,了得的八九不离十了。
总之seal方法完成了Chunk的构建和依赖、Chunk、module等各方面的优化。
seal方法执行完毕,生成好了Chunks对象,compilation的工做告一段落,控制权又还给compiler,此时compiler的compile方法就执行完毕了。该执行compile的回调函数onCompiled了。上面咱们也贴出来onCompiled的简要代码。
onCompiled完成了最后的输出阶段。将咱们生成的Chunk输出到磁盘上。调用了done钩子以后,一次构建就此完成。
总结:构建模块和Chunk,并利用插件优化构建过程
思考一下,当咱们改动业务代码时,webpack-cli会直接再次调用compiler.run()。从新编译咱们的代码。因此有了这张图片:
wepack函数不会被调用了吧,由于没必要在重复初始化。内存中已经有compiler对象了,直接run它就从新编译了。
我是边看他们的文章,边研究源码学习的,想研究源码的同窗,也可使用这种方式,我认为光看文章仍是很难弄清楚源码的,仍是得调试、实践。
本章只是简单梳理了Webpack的基本流程,期待后续继续研究Webpack的各种插件,结合插件更好的熟悉Webpack,同你们一块儿学习。