- 細說 webpack 之流程篇
- webpack之plugin內部執行機制
- How webpack works
- github.com/webpack/tap…
- 玩轉webpack(一)上篇:webpack的基本架構和構建流程
怎麼寫webpack外掛
在寫webpack的外掛時,內部必須要實現一個apply方法,傳入compiler引數,就可以通過呼叫plugin方法,在對應的事件鉤子訂閱事件,然後取出原始碼進行自定義開發後,return回去即可。
這是因為webpack的內部機制是通過tapable來實現的,通過plugin方法訂閱事件,我們可以根據不同事件鉤子型別,執行對應的同步程式碼,或者觸發對應的非同步回撥--序列、並行、瀑布流(上一個操作的輸出是下一個操作的輸入,有點類似pipeLine),在非同步鉤子的回撥函式裡,要返回callback引數。
簡單的同步外掛示例如下:
class MyPlugin {
apply(compiler) {
compiler.plugin(“compilation”, compilation => {
compilation.plugin(“optimize-modules”, modules => {
modules.forEach(…);
}
}
}
}
複製程式碼
Webpack執行機制
我們可以看到,遵循對應的同步、非同步規則編寫簡單的webpack外掛其實是不難的,但是最難的怎麼找到合適的事件鉤子執行對應事件,也就是怎麼去理解webpack執行機制,找到合適突破口。
理解webpack執行機制有幾個概念是需要注意一下:
-
compiler-編譯器物件
compiler物件代表了完整的webpack環境配置。該物件在啟動webpack時就被一次性建立,由webpack組合所有的配置項(包括原始配置,載入器和外掛)構建生成。
當在webpack環境中應用一個外掛時,外掛會收到compiler的引用,通過使用compiler,外掛就可以訪問到整個webpack的環境(包括原始配置,載入器和外掛)。
-
compilation-構建過程
compilation物件在compiler的compile方法裡建立,它代表了一次單一的版本構建以及構建生成資源的彙總:
- compilation物件負責組織整個打包過程,包含了每個構建環節及輸出環節所對應的方法
- 該物件內部存放著所有module、chunk、生成的assets以及用來生成最後打包檔案的template的資訊
當執行webpack-dev-server時,每當檢測到一個檔案變化,就會建立一次新的編譯,從而生成一組新的編譯資源。
-
外掛
外掛本質上是被例項化的帶有apply原型方法的物件,其apply方法在安裝外掛時將被webpack編譯器呼叫一次,apply入參提供了一個compiler(編譯器物件)的引用,從而可以在外掛內部訪問到webpack的環境。
具體使用方式,在webpack.config.js裡require對應的外掛,然後在module.exports物件的plugins陣列裡例項化對應外掛即可。
Compiler執行模式
Compiler有兩種執行模式,一種是run執行模式,另一種是watch監測模式(熱載入模式webpack-dev-server)。在執行模式下,compiler有三個主要的事件鉤子:
-
compile/編譯
開始編譯,建立對應的compilation物件。
-
make/構建
根據配置的不同Entry入口構建對應的chunk塊,並輸出assets資源。
-
emit/提交
將最終的assets資源寫入硬碟,輸出至指定的檔案路徑。
相比較,監測模式則分為兩部分:
- run執行模式
- 監測依賴如果發生改動,重新回到第一步
Compiler事件鉤子
在執行模式下,Compiler的事件鉤子如下:
-
entry-option:生成不同的外掛應用
解析傳給 webpack 的配置中的 entry 屬性,然後生成不同的外掛應用到 Compiler 例項上。這些外掛可能是 SingleEntryPlugin, MultiEntryPlugin 或者 DynamicEntryPlugin。但不管是哪個外掛,內部都會監聽 Compiler 例項物件的 make 任務點
-
(before-)run:開始執行
開始執行,啟動構建
-
(before/after-)compile:編譯原始碼,建立對應的Compilation物件
Compiler 例項將會開始建立 Compilation 物件,這個物件是後續構建流程中最核心最重要的物件,它包含了一次構建過程中所有的資料。也就是說一次構建過程對應一個 Compilation 例項。
-
make:構建
-
(after-)emit:輸出結果
-
done:結束
在監測模式下則對應下面三個鉤子
- watch-run
- invalid
- watch-close
同時,在Compiler裡內嵌了compilation/編譯物件、normal-module-factory/常規模組工廠、context-module-factory/上下文模組工廠,可以在事件鉤子的回撥函式裡直接讀取。
Compilation事件鉤子
compiler的幾個事件鉤子貫穿了webpack的整個生命週期,但是具體到實際版本構建等操作,卻是由compilation來具體執行。compilation物件是指單一的版本構建以及構建生成資源的彙總,它是在compiler的compile/編譯事件裡被建立,重點負責後續的新增模組、構建模組,打包並輸出資源的具體操作(對應了compiler的make構建、emit輸出)。
addEntry開始構建模組
在compiler的make方法裡開始構建模組,對應了compilation的addEntry方法開始新增模組該方法實際上是呼叫了_addModuleChain/私有方法,根據模組的型別獲取對應的模組工廠並建立模組、構建模組。
首先,這裡用了工廠模式來建立module例項,compilation通過讀取每個module/模組的dependency/依賴資訊,在依賴裡讀取對應的模組工廠,然後在用工廠的create事件建立出module例項。
建立出module/模組後,再新增至compilation物件裡,然後由compilation來統籌模組的構建和module的依賴處理。
在處理module的依賴時,每一個依賴模組還是按照第一個步驟進行操作,迭代迴圈。直至模組及依賴完全處理完。
addModule新增模組
我們可以看看addModule事件裡發生了什麼事情,整個addModule裡分為兩塊:讀取依賴工廠,把模組新增至compilation。在後面一個步驟裡webpack專門做了一系列的優化邏輯。
在addModule裡,compilation會通過identifier判斷是否已經有當前module,如果有則跳過;
- 如果沒有的話,會在cache快取裡判斷是否有這個模組,如果也沒有那麼直接新增至compilation裡;
- 如果有那麼就需要判斷快取裡的模組是否已過期,是否需要重構(通過timeStamps判斷),如果需要重構那麼就觸發disconnect事件,如果不需要重構,那麼觸發unbuild事件;
- 不管是否需要重構,只要快取裡有這個模組,都要新增至compilation裡。
buildModule構建模組
經過上面的步驟,已經把所有的模組新增至compilation裡,接下來是由compilation統籌進行構建
構建模組是整個webpack最耗時的操作,在這裡分為幾個步驟:
-
讀取module/模組,呼叫對應Loader載入器進行處理,並輸出對應原始碼
- 遇到依賴時,遞迴地處理依賴的module/模組
- 把處理完的module/模組依賴新增至當前模組
-
呼叫acorn解析載入器輸出的原始檔,並生成抽象語法樹 AST
-
遍歷AST樹,構建該模組以及所依賴的模組
-
整合模組和所有對應的依賴,輸出整體的module
seal打包輸出模組
在構建完模組後,就會在回撥函式裡呼叫compilation的seal方法。Compilation的seal事件裡,會根據webpack裡配置的每個Entry入口開始打包,每個Entry對應地打包出一個chunk,逐次對每個module和chunk進行整理,生成編譯後的原始碼chunk,合併、拆分、生成hash,最終輸出Assets資源。
針對每一個Entry入口構建chunk的過程有點類似上面的buildModule,具體細節可以檢視流程圖。在這裡我們需要重點關注createChunkAssets生成最終的資源並輸出至指定檔案路徑。
在整個createChunkAssets過程裡,有個有意思的地方需要注意,就是mainTemplate、chunkTemplate和moduleTemplate。template是用來處理上面輸出的chunk,打包成最終的Assets資源。但是mainTemplate是用來處理入口模組,chunkTemplate是處理非入口模組,即引用的依賴模組。
通過這兩個template的render處理輸出source原始碼,都會彙總到moduleTemplate進行render處理輸出module。接著module呼叫source抽象方法輸出assets,最終由compiler統一呼叫emitAssets,輸出至指定檔案路徑。
我們需要注意一下mainTemplate的render(render-with-entry)方法,整個webpack通過該方法找到整個入口開始構建,引用依賴。如果我們想自定義AMD外掛,把整個webpack編譯結果包裹起來,那麼我們可以通過mainTemplate的render-with-entry方法,進行自定義開發。
關於webpack執行機制
關於webpack執行機制的理解,建議大家有空再看看下面兩個流程圖,講得很仔細,有助於理解。
編寫AMD外掛
假設我們現在要寫一個外掛使得其能夠生成 AMD 模組,那麼我們要怎麼操作?我們可能會有下面這些疑惑。
具體開發的思路,是通過在webpack打包輸出最終資源時,從入口模組構建的節點入手,訂閱compilation.mainTemplate的render-with-entry事件,在裡邊用AMD宣告包裹住原始碼,然後返回即可。這樣子後續展開依賴模組時也會統一被AMD宣告所包裹。
具體外掛原始碼如下:
/** plugin.js **/
const ConcatSource = require('webpack-sources').ConcatSource,
path = require('path')
class DefPlugin {
constructor(name) {
}
apply(compiler) {
compiler.plugin('compilation', (compilation)=>{
compilation.mainTemplate.plugin('render-with-entry', function(source, chunk, hash){
return new ConcatSource(`global.define(['require','module','exports'],function(require, module, exports) {
${source.source()}
})`);
})
})
}
}
module.exports = DefPlugin
複製程式碼
實際呼叫時,只需要在webpack.config.js的plugins陣列裡例項化該外掛即可。
/** webpack.config.js **/
const path = require('path')
const Plugin = require('./plugin')
module.exports = {
context: path.join(__dirname, 'src'),
entry: './index.js',
output: {
libraryTarget: 'commonjs2',
path: path.join(__dirname, 'dist'),
filename: './bundle.js'
},
plugins: [
new Plugin
]
}
複製程式碼