在寫webpack的插件時,內部必需要實現一個apply方法,傳入compiler參數,就能夠經過調用plugin方法,在對應的事件鉤子訂閱事件,而後取出源碼進行自定義開發後,return回去便可。webpack
這是由於webpack的內部機制是經過tapable來實現的,經過plugin方法訂閱事件,咱們能夠根據不一樣事件鉤子類型,執行對應的同步代碼,或者觸發對應的異步回調--串行、並行、瀑布流(上一個操做的輸出是下一個操做的輸入,有點相似pipeLine),在異步鉤子的回調函數裏,要返回callback參數。git
簡單的同步插件示例以下:github
class MyPlugin {
apply(compiler) {
compiler.plugin(「compilation」, compilation => {
compilation.plugin(「optimize-modules」, modules => {
modules.forEach(…);
}
}
}
}
複製代碼
咱們能夠看到,遵循對應的同步、異步規則編寫簡單的webpack插件實際上是不難的,可是最難的怎麼找到合適的事件鉤子執行對應事件,也就是怎麼去理解webpack運行機制,找到合適突破口。web
理解webpack運行機制有幾個概念是須要注意一下:segmentfault
compiler-編譯器對象數組
compiler對象表明了完整的webpack環境配置。該對象在啓動webpack時就被一次性建立,由webpack組合全部的配置項(包括原始配置,加載器和插件)構建生成。緩存
當在webpack環境中應用一個插件時,插件會收到compiler的引用,經過使用compiler,插件就能夠訪問到整個webpack的環境(包括原始配置,加載器和插件)。架構
compilation-構建過程app
compilation對象在compiler的compile方法裏建立,它表明了一次單一的版本構建以及構建生成資源的彙總:異步
當運行webpack-dev-server時,每當檢測到一個文件變化,就會建立一次新的編譯,從而生成一組新的編譯資源。
插件
插件本質上是被實例化的帶有apply原型方法的對象,其apply方法在安裝插件時將被webpack編譯器調用一次,apply入參提供了一個compiler(編譯器對象)的引用,從而能夠在插件內部訪問到webpack的環境。
具體使用方式,在webpack.config.js裏require對應的插件,而後在module.exports對象的plugins數組裏實例化對應插件便可。
Compiler有兩種運行模式,一種是run執行模式,另外一種是watch監測模式(熱加載模式webpack-dev-server)。在執行模式下,compiler有三個主要的事件鉤子:
compile/編譯
開始編譯,建立對應的compilation對象。
make/構建
根據配置的不一樣Entry入口構建對應的chunk塊,並輸出assets資源。
emit/提交
將最終的assets資源寫入硬盤,輸出至指定的文件路徑。
相比較,監測模式則分爲兩部分:
在執行模式下,Compiler的事件鉤子以下:
entry-option:生成不一樣的插件應用
解析傳給 webpack 的配置中的 entry 屬性,而後生成不一樣的插件應用到 Compiler 實例上。這些插件多是 SingleEntryPlugin, MultiEntryPlugin 或者 DynamicEntryPlugin。但不論是哪一個插件,內部都會監聽 Compiler 實例對象的 make 任務點
(before-)run:開始執行
開始執行,啓動構建
(before/after-)compile:編譯源碼,建立對應的Compilation對象
Compiler 實例將會開始建立 Compilation 對象,這個對象是後續構建流程中最核心最重要的對象,它包含了一次構建過程當中全部的數據。也就是說一次構建過程對應一個 Compilation 實例。
make:構建
(after-)emit:輸出結果
done:結束
在監測模式下則對應下面三個鉤子
同時,在Compiler裏內嵌了compilation/編譯對象、normal-module-factory/常規模塊工廠、context-module-factory/上下文模塊工廠,能夠在事件鉤子的回調函數裏直接讀取。
compiler的幾個事件鉤子貫穿了webpack的整個生命週期,可是具體到實際版本構建等操做,倒是由compilation來具體執行。compilation對象是指單一的版本構建以及構建生成資源的彙總,它是在compiler的compile/編譯事件裏被建立,重點負責後續的添加模塊、構建模塊,打包並輸出資源的具體操做(對應了compiler的make構建、emit輸出)。
在compiler的make方法裏開始構建模塊,對應了compilation的addEntry方法開始添加模塊該方法其實是調用了_addModuleChain/私有方法,根據模塊的類型獲取對應的模塊工廠並建立模塊、構建模塊。
首先,這裏用了工廠模式來建立module實例,compilation經過讀取每一個module/模塊的dependency/依賴信息,在依賴裏讀取對應的模塊工廠,而後在用工廠的create事件建立出module實例。
建立出module/模塊後,再添加至compilation對象裏,而後由compilation來統籌模塊的構建和module的依賴處理。
在處理module的依賴時,每個依賴模塊仍是按照第一個步驟進行操做,迭代循環。直至模塊及依賴徹底處理完。
咱們能夠看看addModule事件裏發生了什麼事情,整個addModule裏分爲兩塊:讀取依賴工廠,把模塊添加至compilation。在後面一個步驟裏webpack專門作了一系列的優化邏輯。
在addModule裏,compilation會經過identifier判斷是否已經有當前module,若是有則跳過;
通過上面的步驟,已經把全部的模塊添加至compilation裏,接下來是由compilation統籌進行構建
構建模塊是整個webpack最耗時的操做,在這裏分爲幾個步驟:
讀取module/模塊,調用對應Loader加載器進行處理,並輸出對應源碼
調用acorn解析加載器輸出的源文件,並生成抽象語法樹 AST
遍歷AST樹,構建該模塊以及所依賴的模塊
整合模塊和全部對應的依賴,輸出總體的module
在構建完模塊後,就會在回調函數裏調用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運行機制的理解,建議你們有空再看看下面兩個流程圖,講得很仔細,有助於理解。
假設咱們如今要寫一個插件使得其可以生成 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
]
}
複製代碼