作者:崔靜
引言
對於 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();
}
}
}
}
複製程式碼
對外暴露的只有兩個個方法:
- acquire: 申請處理資源,如果有閒置資源(即併發數量)則立即執行處理,並且閒置的資源減1;否則存入等待佇列中。
- release: 釋放資源。在 acquire 中會呼叫 callback 方法,在這裡需要使用 release 釋放資源,將閒置資源加1。同時會檢查是否還有待處理內容,如果有則繼續處理
這個 Semaphore 類借鑑了在多執行緒環境中,對使用資源進行控制的 Semaphore(訊號量)的概念。其中併發個數通過 available 來定義,那麼預設值是多少呢?在 Compilation.js
中可以找到
this.semaphore = new Semaphore(options.parallelism || 100);
複製程式碼
預設的併發數是 100,注意這裡說的併發只是程式碼設計中的併發,不要和js的單執行緒特性搞混了。總的來看編譯流程如下圖
從入口到 _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,都做了兩件事:
- 註冊了 compilation 事件回撥(這個事件會在下面 make 事件之前會觸發),在 compilation 階段設定
dependencyFactories
compiler.hooks.compilation.tap('xxEntryPlugin', (compilation, { normalModuleFactory }) => {
//...
compilation.dependencyFactories.set(...)
})
複製程式碼
- 註冊了 make 事件回撥,在 make 階段的時候呼叫 addEntry 方法,然後進入
_addModuleChain
進入正式的編譯階段。
compiler.hooks.make.tapAsync('xxEntryPlugin',(compilation, callback) => {
// ...
compilation.addEntry(...)
})
複製程式碼
結合 webpack 的打包流程,我們從 Compiler.js 中的 compile 方法開始,看一下 compilation 事件和 make 事件回撥起了什麼作用
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 相比,
- 在 compilation 中,dependencyFactories 設定了兩個對應值
MultiEntryDependency: multiModuleFactory
SingleEntryDependency: normalModuleFactory
複製程式碼
- 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 的過程。