webpack系列之五module生成1

做者:崔靜webpack

引言

對於 webpack 來講每一個文件都是一個 module,這篇文章帶你來看 webpack 如何從配置中 entry 的定義開始,順藤摸瓜找到所有的文件,並轉化爲 module。web

總覽

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();
			}
		}
	}
}
複製代碼

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

  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 能夠有下面幾種形式:post

  • 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 進行處理,有以下一句ui

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 的過程。

相關文章
相關標籤/搜索