將個人研究成果,畫一個簡要的流程圖,若是您有興趣看完,回頭再看看這個流程圖。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,同你們一塊兒學習。