webpack系列之五module生成1

滴滴WebApp架構組發表於2019-04-25

作者:崔靜

引言

對於 webpack 來說每個檔案都是一個 module,這篇文章帶你來看 webpack 如何從配置中 entry 的定義開始,順藤摸瓜找到全部的檔案,並轉化為 module。

總覽

webpack 入口 entry,entry 引數是單入口字串、單入口陣列、多入口物件還是動態函式,無論是什麼都會呼叫 compilation.addEntry 方法,這個方法會執行 _addModuleChain,將入口檔案加入需要編譯的佇列中。然後佇列中的檔案被一個一個處理,檔案中的 import 引入了其他的檔案又會通過 addModuleDependencies 加入到編譯佇列中。最終當這個編譯佇列中的內容完成被處理完時,就完成了檔案到 module 的轉化。

總覽

上面是一個粗略的輪廓,接下來我們將細節一一補充進這個輪廓中。首先看編譯的總流程控制——編譯佇列的控制。

編譯佇列控制 —— Semaphore

_addModuleChain 和 addModuleDependencies 函式中都會呼叫 this.semaphore.acquire 這個函式的具體實現在 lib/util/Semaphore.js 檔案中。看一下具體的實現

class Semaphore {
	constructor(available) {
	   // available 為最大的併發數量
		this.available = available;
		this.waiters = [];
		this._continue = this._continue.bind(this);
	}

	acquire(callback) {
		if (this.available > 0) {
			this.available--;
			callback();
		} else {
			this.waiters.push(callback);
		}
	}

	release() {
		this.available++;
		if (this.waiters.length > 0) {
			process.nextTick(this._continue);
		}
	}

	_continue() {
		if (this.available > 0) {
			if (this.waiters.length > 0) {
				this.available--;
				const callback = this.waiters.pop();
				callback();
			}
		}
	}
}
複製程式碼

對外暴露的只有兩個個方法:

  1. acquire: 申請處理資源,如果有閒置資源(即併發數量)則立即執行處理,並且閒置的資源減1;否則存入等待佇列中。
  2. release: 釋放資源。在 acquire 中會呼叫 callback 方法,在這裡需要使用 release 釋放資源,將閒置資源加1。同時會檢查是否還有待處理內容,如果有則繼續處理

這個 Semaphore 類借鑑了在多執行緒環境中,對使用資源進行控制的 Semaphore(訊號量)的概念。其中併發個數通過 available 來定義,那麼預設值是多少呢?在 Compilation.js 中可以找到

this.semaphore = new Semaphore(options.parallelism || 100);
複製程式碼

預設的併發數是 100,注意這裡說的併發只是程式碼設計中的併發,不要和js的單執行緒特性搞混了。總的來看編譯流程如下圖

編譯佇列控制_new

從入口到 _addModuleChain

webpack 官網配置指南中 entry 可以有下面幾種形式:

  • string: 字串,例如
{
  entry: './demo.js'
}
複製程式碼
  • [string]: string 型別的陣列,例如
{
  entry: ['./demo1.js', './demo2.js']
}
複製程式碼
  • 物件,例如
{
  entry: {
    app: './demo.js'
  }
}
複製程式碼
  • 函式,動態返回入口,例如
{
  entry: () => './demo.js'
}
// 或者
{
  entry: () => new Promise((resolve) => resolve('./demo.js'))
}
複製程式碼

這些是哪裡處理的呢? webpack 的啟動檔案 webpack.js 中, 會先對 options 進行處理,有如下一句

compiler.options = new WebpackOptionsApply().process(options, compiler);
複製程式碼

process 的過程中會對 entry 的配置做處理

// WebpackOptionsApply.js 檔案中
new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);
複製程式碼

先看 EntryOptionsPlugin 做了什麼

const SingleEntryPlugin = require("./SingleEntryPlugin");
const MultiEntryPlugin = require("./MultiEntryPlugin");
const DynamicEntryPlugin = require("./DynamicEntryPlugin");

const itemToPlugin = (context, item, name) => {
	if (Array.isArray(item)) {
		return new MultiEntryPlugin(context, item, name);
	}
	return new SingleEntryPlugin(context, item, name);
};

module.exports = class EntryOptionPlugin {
	apply(compiler) {
		compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
		   // string 型別則為 new SingleEntryPlugin
		   // array 型別則為 new MultiEntryPlugin
			if (typeof entry === "string" || Array.isArray(entry)) {
				itemToPlugin(context, entry, "main").apply(compiler);
			} else if (typeof entry === "object") {
			    // 對於 object 型別,遍歷其中每一項
				for (const name of Object.keys(entry)) {
					itemToPlugin(context, entry[name], name).apply(compiler);
				}
			} else if (typeof entry === "function") {
			    // function 型別則為 DynamicEntryPlugin
				new DynamicEntryPlugin(context, entry).apply(compiler);
			}
			return true;
		});
	}
};
複製程式碼

EntryOptionsPlugin中註冊了 entryOption 的事件處理函式,根據 entry 值的不同型別(string/array/object中每一項/functioin)例項化和執行不同的 EntryPlugin:string 對應 SingleEntryPlugin; array 對應 MultiEntryPlugin;function 對應 DynamicEntryPlugin。而對於 object 型別來說遍歷其中的每一個 key,將每一個 key 當做一個入口,並根據型別 string/array 的不同選擇 SingleEntryPlugin 或 MultiEntryPlugin。下面我們主要分析:SingleEntryPlugin,MultiEntryPlugin,DynamicEntryPlugin

橫向對比一下這三個 Plugin,都做了兩件事:

  1. 註冊了 compilation 事件回撥(這個事件會在下面 make 事件之前會觸發),在 compilation 階段設定 dependencyFactories
compiler.hooks.compilation.tap('xxEntryPlugin', (compilation, { normalModuleFactory }) => {
  //...
  compilation.dependencyFactories.set(...)
})
複製程式碼
  1. 註冊了 make 事件回撥,在 make 階段的時候呼叫 addEntry 方法,然後進入 _addModuleChain 進入正式的編譯階段。
compiler.hooks.make.tapAsync('xxEntryPlugin',(compilation, callback) => {
  // ...
  compilation.addEntry(...)
})
複製程式碼

結合 webpack 的打包流程,我們從 Compiler.js 中的 compile 方法開始,看一下 compilation 事件和 make 事件回撥起了什麼作用

addEntry總流程

xxxEntryPlugin 在 compilation 事件中回撥用來設定compilation.dependencyFactories,保證在後面 _addModuleChain 回撥階段可以根據 dependency 獲取到對應的 moduleFactory

make 事件回撥中根據不同的 entry 配置,生成 dependency,然後呼叫addEntry,並將 dependency 傳入。

_addModuleChain 回撥中根據不同 dependency 型別,然後執行 multiModuleFactory.create 或者 normalModuleFacotry.create

上面的步驟中不停的提到 dependency,在接下來的文章中將會出現各種 dependency。可見,dependency 是 webpack 中一個很關鍵的東西,在 webpack/lib/dependencies 資料夾下,你會看到各種各樣的 dependency。dependency 和 module 的關係結構如下:

module: {
  denpendencies: [
    dependency: {
      //...
      module: // 依賴的 module,也可能為 null
    }
  ]
}
}
複製程式碼

webpack 中將入口檔案也當成入口的依賴來處理,所以上面 xxEntryPlugin 中生成的是 xxEntryDependency。module 中的 dependency 儲存了這個 module 對其他檔案的依賴資訊、自身 export 出去的內容等。後面的文章中,你會看到在生成 chunk 時會依靠 dependency 來得到依賴關係圖,生成最終檔案時會依賴 dependency 中方法和儲存的資訊將原始檔中的 import 等語句替換成最終輸出的可執行的 js 語句。

看完了各個 entryPlugin 的共同點之後,我們縱向深入每個 plugin,對比一下不同之處。

SingleEntryPlugin

SingleEntryPlugin 邏輯很簡單:將 SingleEntryDependency 和 normalModuleFactory 關聯起來,所以後續的 create 方法會執行 normalModuleFactory.create 方法。

apply(compiler) {
	compiler.hooks.compilation.tap(
		"SingleEntryPlugin",
		(compilation, { normalModuleFactory }) => {
		   // SingleEntryDependency 對應的是 normalModuleFactory
			compilation.dependencyFactories.set(
				SingleEntryDependency,
				normalModuleFactory
			);
		}
	);

	compiler.hooks.make.tapAsync(
		"SingleEntryPlugin",
		(compilation, callback) => {
			const { entry, name, context } = this;

			const dep = SingleEntryPlugin.createDependency(entry, name);
			// dep 的 constructor 為 SingleEntryDependency
			compilation.addEntry(context, dep, name, callback);
		}
	);
}

static createDependency(entry, name) {
	const dep = new SingleEntryDependency(entry);
	dep.loc = name;
	return dep;
}
複製程式碼

MultiEntryPlugin

與上面 SingleEntryPlugin 相比,

  1. 在 compilation 中,dependencyFactories 設定了兩個對應值
MultiEntryDependency: multiModuleFactory
SingleEntryDependency: normalModuleFactory
複製程式碼
  1. createDependency: 將 entry 中每一個值作為一個 SingleEntryDependency 處理。
static createDependency(entries, name) {
	return new MultiEntryDependency(
		entries.map((e, idx) => {
			const dep = new SingleEntryDependency(e);
			// Because entrypoints are not dependencies found in an
			// existing module, we give it a synthetic id
			dep.loc = `${name}:${100000 + idx}`;
			return dep;
		}),
		name
	);
}
複製程式碼

3.multiModuleFactory.create

在第二步中,由 MultiEntryPlugin.createDependency 生成的 dep,結構如下:

{
  dependencies:[]
  module: MultiModule
  //...
}
複製程式碼

dependencies 是一個陣列,包含多個 SingleEntryDependency。這個 dep 會當做引數傳給 multiModuleFactory.create 方法,即下面程式碼中 data.dependencies[0]

// multiModuleFactory.create
create(data, callback) {
	const dependency = data.dependencies[0];
	callback(
		null,
		new MultiModule(data.context, dependency.dependencies, dependency.name)
	);
}
複製程式碼

create 中生成了 new MultiModule,在 callback 中會執行 MultiModule 中 build 方法,

build(options, compilation, resolver, fs, callback) {
	this.built = true; // 標記編譯已經完成
	this.buildMeta = {};
	this.buildInfo = {};
	return callback();
}
複製程式碼

這個方法中將編譯是否完成的變數值設定為 true,然後直接進入的成功的回撥。此時,入口已經完成了編譯被轉化為一個 module, 並且是一個只有 dependencies 的 module。由於在 createDependency 中每一項都作為一個 SingleEntryDependency 處理,所以 dependencies 中每一項都是一個 SingleEntryDependency。隨後進入對這個 module 的依賴處理階段,我們配置在 entry 中的多個檔案就被當做依賴加入到編譯鏈中,被作為 SingleEntryDependency 處理。

總的來看,對於多檔案的入口,可以簡單理解為 webpack 內部先把入口轉化為一個下面的形式:

import './demo1.js'
import './demo2.js'
複製程式碼

然後對其做處理。

DynamicEntryPlugin

動態的 entry 配置中同時支援同步方式和返回值為 Promise 型別的非同步方式,所以在處理 addEntry 的時候首先呼叫 entry 函式,然後根據返回的結果型別的不同,進入 string/array/object 的邏輯。

compiler.hooks.make.tapAsync(
	"DynamicEntryPlugin",
	(compilation, callback) => {
		const addEntry = (entry, name) => {
			const dep = DynamicEntryPlugin.createDependency(entry, name);
			return new Promise((resolve, reject) => {
				compilation.addEntry(this.context, dep, name, err => {
					if (err) return reject(err);
					resolve();
				});
			});
		};
		Promise.resolve(this.entry()).then(entry => {
			if (typeof entry === "string" || Array.isArray(entry)) {
				addEntry(entry, "main").then(() => callback(), callback);
			} else if (typeof entry === "object") {
				Promise.all(
					Object.keys(entry).map(name => {
						return addEntry(entry[name], name);
					})
				).then(() => callback(), callback);
			}
		});
	}
);
複製程式碼

所以動態入口與其他的差別僅在於多了一層函式的呼叫。

入口找到了之後,就是將檔案轉為 module 了。接下來的一篇文章中,將詳細介紹轉 module 的過程。

相關文章