Webpack 源碼研究

Webpack運行流程圖

將個人研究成果,畫一個簡要的流程圖,若是您有興趣看完,回頭再看看這個流程圖。webpack

image

圖片裏的方塊中文字的序號,就是運行的順序。web

研究Webpack源碼的好處

  1. 理解Webpack的運行流程
  2. 理解源碼是理解Webpack插件的基礎
  3. 學習Webpack源碼中的技巧和思想

編寫一個插件的思考

寫一個插件很簡單,以下: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,都是流程中的一部分。咱們查看流程,須要瞭解這些必須的插件。

webpack函數的職責

總結:讀取配置,實例compiler,實例化和掛載插件。

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,成功完成一次輸出。

compiler對象的職責

總結:啓動編譯和管理輸出。實例化compilation對象並利用make鉤子的插件,讓其開始工做。模塊的編譯工做是compilation對象作的。

梳理一下compiler對象的調用棧:run->compile->onCompiled

  • run函數中觸發的鉤子:beforeRun,run
  • compile函數中觸發的鉤子:beforeCompile,compile,thisCompilation,compilation,make,afterCompile
  • onCompiled函數中觸發的鉤子: should-emit,emit,done。

compilation

瞭解了compiler對象,接下來看compilation對象。

compilation的研究相對耗時一點,由於webpack函數和compiler關聯的東西不多,webpack只關聯了compiler,compiler只關聯了compilation,因此比較容易梳理清楚。

但想弄懂compilation,須要弄清楚compilation,moduleFactory,module,三種對象之間的關係,邏輯上也更復雜一點,接下來一塊兒看看吧。

此圖是在compilation構建模塊鏈階段,主要使用的對象和它們的職責示意圖。

compilation編譯模塊的入口

要從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,咱們後面在構建模塊中用到。

Webpack處理異步都是使用callback

再看回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。

addEntry方法作的事

  1. 調用addEntry鉤子。
  2. 調用在_addModuleChain方法,調用完畢後執行addEntry的callback,通知make鉤子的插件編譯工做完成。

_addModuleChain方法作的事

  1. 從名字能夠看出,這個方法是添加模塊鏈的。就是它執行完,模塊都構建好了,而且造成了鏈。
  2. 根據dep找到moduleFactory,咱們找到的是normalModuleFactory。
  3. 調用moduleFactory.create方法。在返回值中拿到module,這裏的module是個NormalModule類型的對象。
  4. 獲得moudle對象後,調用buildModule方法。此方法內調用buildModule鉤子,而後調用moudle自身的build方法。build方法就是調用loader,構建模塊,獲取依賴的邏輯。
  5. 獲得編譯後的moudle,調用afterBuild方法。在afterBuild判斷模塊依賴了哪些模塊,遞歸的用模塊工廠建立它們,重複3,4,5流程。直到全部關聯的模塊構建完畢,咱們就拿到模塊鏈了。

NormalModule中build

咱們來細看一下上面4步的build方法,內部調用了doBuild,doBuild中調用了runLoaders,從方法名能夠看出,此處就是模塊構建中使用loader的地方了。

模塊構建結束

也就是addEntry執行結束、SingleEntryPlugin執行結束、make鉤子調用結束。該執行make鉤子的回調函數了。

Chunk構建和打包的優化

make鉤子回調中調用了compilation的seal方法。開始了Chunk的構建和打包的優化過程。能夠說看到這裏,咱們對Webpack的執行流程,了得的八九不離十了。

seal方法作的事。

  1. 調用seal鉤子,seal方法中鉤子可謂大展威力,seal方法只是調用各類鉤子,真正的構建和優化工做,都是插件作的。
  2. 循環調用optimizeDependenciesBasic、optimizeDependencies、optimizeDependenciesAdvanced鉤子。
  3. 調用afterOptimizeDependencies鉤子。
  4. 調用beforeChunks鉤子。從名字也能夠看出上面作的依賴方面的優化,此處開始構建Chunk了。
  5. 調用afterChunks鉤子。
  6. 調用optimize鉤子。
  7. 調用optimizeModulesBasic、optimizeModules、optimizeModulesAdvanced、afterOptimizeModules鉤子。此處優化module。,參數是this.modules。
  8. 調用optimizeChunksBasic、optimizeChunks、optimizeChunksAdvanced、afterOptimizeChunks。此處優化Chunk,參數是this.chunkGroups。
  9. 一系列鉤子調用再也不贅述,有須要的時候,咱們再研究。

總之seal方法完成了Chunk的構建和依賴、Chunk、module等各方面的優化。

回到compiler

seal方法執行完畢,生成好了Chunks對象,compilation的工做告一段落,控制權又還給compiler,此時compiler的compile方法就執行完畢了。該執行compile的回調函數onCompiled了。上面咱們也貼出來onCompiled的簡要代碼。

onCompiled方法作的事

onCompiled完成了最後的輸出階段。將咱們生成的Chunk輸出到磁盤上。調用了done鉤子以後,一次構建就此完成。

compilation對象的職責

總結:構建模塊和Chunk,並利用插件優化構建過程

改業務代碼的再編譯過程

思考一下,當咱們改動業務代碼時,webpack-cli會直接再次調用compiler.run()。從新編譯咱們的代碼。因此有了這張圖片:

image

wepack函數不會被調用了吧,由於沒必要在重複初始化。內存中已經有compiler對象了,直接run它就從新編譯了。

參考文章

從Webpack源碼探究打包流程,萌新也能看懂~

webpack原理

我是邊看他們的文章,邊研究源碼學習的,想研究源碼的同窗,也可使用這種方式,我認爲光看文章仍是很難弄清楚源碼的,仍是得調試、實踐。

結束語

本章只是簡單梳理了Webpack的基本流程,期待後續繼續研究Webpack的各種插件,結合插件更好的熟悉Webpack,同你們一塊兒學習。

相關文章
相關標籤/搜索