做爲一個敲碼5分鐘,調試兩小時的bug大叔,天天和console.log打的交道天然很多,人到中年,愈來愈懶,因而想把console.log('bug: ', bug)變成log.bug來讓個人懶癌病發得更加完全。因而硬着頭皮看了下webpack插件的寫法,在此記錄一下webpack系統的學習筆記。webpack
去吧!皮卡丘!!!git
// webpack.config.js
const webpack = require('webpack');
const WebpackPlugin = require('webpack-plugin');
const options = {
entry: 'index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'index.bundle.js'
},
module: {
rules: []
},
plugins: [
new WebpackPlugin()
]
// ...
};
webpack(options);
複製代碼
// webpack.js
// 引入Compiler類
const Compiler = require("./Compiler");
// webpack入口函數
const webpack = (options, callback) => {
// 建立一個Compiler實例
compiler = new Compiler(options.context);
// 實例保存webpack的配置對象
compiler.options = options
// 依次調用webpack插件的apply方法,並傳入compiler引用
for (const plugin of options.plugins) {
plugin.apply(compiler);
}
//執行實例的run方法
compiler.run(callback)
//返回實例
return compiler
}
module.exports = webpack
複製代碼
compiler
實例。compiler
是什麼?—— 明顯發現compiler保存了完整的webpack的配置參數options。因此官方說:compiler 對象表明了完整的 webpack 環境配置。這個對象在啓動 webpack 時被一次性創建,並配置好全部可操做的設置,包括 options,loader 和 plugin。可使用 compiler 來訪問 webpack 的主環境。github
webpack-plugin
也在這裏經過提供一個叫 apply
的方法給webpack調用,以完成初始化的工做,而且接收到剛建立的 compiler
的引用。// Compiler.js
const {
Tapable,
SyncHook,
SyncBailHook,
AsyncParallelHook,
AsyncSeriesHook
} = require("tapable");
const Compilation = require("./Compilation");
class Compiler extends Tapable {
hooks = [
hook1: new SyncHook(),
hook2: new AsyncSeriesHook(),
hook3: new AsyncParallelHook()
]
run () {
// ...
// 觸發鉤子回調執行
this.hooks[hook1].call()
this.hooks[hook2].callAsync(() => {
this.hooks[hook4].call()
})
// 進入編譯流程
this.compile()
// ...
this.hooks[hook3].promise(() => {})
}
compile () {
// 建立一個Compilation實例
const compilation = new Compilation(this)
}
}
複製代碼
研究下Compiler.js這個文件,它引入了一個 tapable
類(源碼)的庫,這個庫提供了一些 Hook
類,內部實現了相似的 事件訂閱/發佈系統
。web
哪裏用到 Hook
類?—— 在 Compiler
類(Compiler.js源碼) 裏擁有不少有意思的hook,這些hook表明了整個編譯過程的各個關鍵事件節點,它們都是繼承於 Hook
類 ,因此都支持 監聽/訂閱
,只要咱們的插件提早作好事件訂閱,那麼編譯流程進行到該事件點時,就會執行咱們提供的 事件回調
,作咱們想作的事情了。express
如何進行訂閱呢?——在上文中每一個webpack-plugin中都有一個apply方法。其實註冊的代碼就藏在裏面,經過如下相似的代碼實現。任何的webpack插件均可以訂閱hook1,所以hook1維護了一個taps數組,保存着全部的callback。npm
compiler.hooks.hook1.tap(name, callback) // 註冊/訂閱api
compiler.hooks.hook1.call() // 觸發/發佈數組
準備工做作好了以後,當 run()
方法開始被調用時,編譯就正式開始了。在該方法裏,執行 call/callAsync/promise
這些事件時(他們由webpack內部包括一些官方使用的webpack插件進行觸發的管理,無需開發者操心),相應的hook就會把本身的taps裏的函數均執行一遍。大概的邏輯以下所示。 promise
其中,hooks的執行是按照編譯的流程順序來的,hooks之間有彼此依賴的關係。bash
compilation
實例也在這裏建立了,它表明了一次資源版本構建。每當檢測到文件變化,就會建立一個新的 compilation,從而生成一組新的編譯資源。一個 compilation 對象表現了當前的模塊資源、編譯生成資源、變化的文件、以及被跟蹤依賴的狀態信息。並且compilation也和compiler相似,擁有不少hooks(Compilation.js源碼),所以一樣提供了不少事件節點給予咱們訂閱使用。
class MyPlugin {
apply(compiler) {
// 設置回調來訪問 compilation 對象:
compiler.hooks.compilation.tap('myPlugin', (compilation) => {
// 如今,設置回調來訪問 compilation 中的任務點:
compilation.hooks.optimize.tap('myPlugin', () => {
console.log('Hello compilation!');
});
});
}
}
module.exports = MyPlugin;
複製代碼
compiler
和 complation
的事件節點都在webpack-plugin中的 apply
方法裏書寫,具體的演示如上。固然你想拿到編譯過程當中的什麼資源,首先得要找出能提供該資源引用的對應的compiler事件節點進行訂閱(上帝先創造了亞當,再有夏娃)。每一個compiler時間節點能提供什麼參數,在hook的實例化時已經作了說明(以下),更多可查看源碼this.hooks = {
// 拿到compilation和params
compilation: new SyncHook(["compilation", "params"]),
// 拿到stats
done: new AsyncSeriesHook(["stats"]),
// 拿到compiler
beforeRun: new AsyncSeriesHook(["compiler"])
}
複製代碼
通過以上的初步探索,寫webpack插件須要瞭解的幾個知識點應該有了大概的掌握:
apply
方法供 webpack
調用進行初始化tap
註冊方式鉤入 compiler
和 compilation
的編譯流程webpack
提供的 api
進行資源的個性化處理。寫插件的套路已經知道了,如今還剩如何找出合適的鉤子,修改資源這件事。在webpack系統裏,鉤子即流程,是編譯構建工做的生命週期
。固然,想要了解全部 tapable
實例對象的鉤子的具體做用,須要探索webpack全部的內部插件如何使用這些鉤子,作了什麼工做來進行總結,想一想就複雜,因此只能抽取重要流程作思路歸納,借用淘寶的一張經典圖示。![webpack_flow.jpg](file:///Users/Ando/Documents/webpack-plugin/webpack_flow.jpg) 整個編譯流程大概分紅三個階段,如今從新整理一下:
準備階段
,webpack的初始化apply
方法,讓插件們作好事件註冊。WebpackOptionsApply
接收組合了 命令行
和 webpack.config.js
的配置參數,負責webpack內部基礎流程使用的插件和compiler上下文環境的初始化工做。(*Plugin
均爲webpack內部使用的插件)// webpack.js
// 開發者自定義的插件初始化
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler)
} else {
plugin.apply(compiler)
}
}
}
// ...
// webpack內部使用的插件初始化
compiler.options = new WebpackOptionsApply().process(options, compiler)
// WebpackOptionsApply.js
class WebpackOptionsApply extends OptionsApply {
process (options, compiler) {
// ...
new WebAssemblyModulesPlugin({
mangleImports: options.optimization.mangleWasmImports
}).apply(compiler);
new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);
new CompatibilityPlugin().apply(compiler);
new HarmonyModulesPlugin(options.module).apply(compiler);
new AMDPlugin(options.module, options.amd || {}).apply(compiler);
// ...
}
}
複製代碼
run / watch
(一次打包/監聽打包模式)觸發 編譯
階段編譯階段
,生成module
,chunk
資源。
run
→compile
編譯 → 建立compilation
對象。compilation
的建立是一次編譯的起步,即將對全部模塊加載(load)
、封存(seal)
、優化(optimiz)
、分塊(chunk)
、哈希(hash)
和從新建立(restore)
。
module.exports = class Compiler extends Tapable {
run () {
// 聲明編譯結束回調
function onCompiled () {}
// 觸發run鉤子
this.hooks.run.callAsync(this, err => {
this.compile(onCompiled)
})
}
compile(callback) {
// ...
// 編譯開始前,觸發beforeCompile鉤子
this.hooks.beforeCompile.callAsync(params, err => {
// 編譯開始,觸發compile鉤子
this.hooks.compile.call(params);
// 建立compilation實例
const compilation = this.newCompilation(params);
// 觸發make鉤子
this.hooks.make.callAsync(compilation, err => {
// 模塊解析完畢,執行compilation的finish方法
compilation.finish();
// 資源封存,執行seal方法
compilation.seal(err => {
// 編譯結束,執行afterCompile鉤子
this.hooks.afterCompile.callAsync(compilation, err => {
// ...
});
});
});
});
}
}
複製代碼
加載模塊
:make
鉤子觸發 → DllEntryPlugin
內部插件調用compilation.addEntry
→ compilation
維護了一些資源生成工廠方法 compilation.dependencyFactories
,負責把入口文件及其(循環)依賴轉換成 module
( module
的解析過程會應用匹配的 loader
)。每一個 module
的解析過程提供 buildModule / succeedModule
等鉤子, 全部 module
解析完成後觸發 finishModules
鉤子。封存
:seal
方法包含了 優化/分塊/哈希
, 編譯中止接收新模塊,開始生成chunks。此階段依賴了一些webpack內部插件對module進行優化,爲本次構建生成的chunk加入hash等。createChunkAssets()會根據chunk類型使用不一樣的模板進行渲染。此階段執行完畢後就表明一次編譯完成,觸發 afterCompile
鉤子
優化
:BannerPlugin
compilation.hooks.optimizeChunkAssets.tapAsync('MyPlugin', (chunks, callback) => {
chunks.forEach(chunk => {
chunk.files.forEach(file => {
compilation.assets[file] = new ConcatSource(
'\/**Sweet Banner**\/',
'\n',
compilation.assets[file]
);
});
});
callback();
});
複製代碼
分塊
:用來分割chunk的 SplitChunksPlugin
插件監聽了optimizeChunksAdvanced
鉤子哈希
:createHash
emit
,遍歷 compilation.assets
生成全部文件。simple-log-webpack-plugin
一張效果圖先上爲敬。(對照圖片)只需寫
log.a
,經過本身的webpack插件自動補全字段標識a字段:
,加入文件路徑
,輕鬆支持打印顏色
效果,相同文件的日誌信息可摺疊
,給你一個簡潔方便的調試環境。
const ColorHash = require('color-hash')
const colorHash = new ColorHash()
const Dependency = require('webpack/lib/Dependency');
class LogDependency extends Dependency {
constructor(module) {
super();
this.module = module;
}
}
LogDependency.Template = class {
apply(dep, source) {
const before = `;console.group('${source._source._name}');`
const after = `;console.groupEnd();`
const _size = source.size()
source.insert(0, before)
source.replace(_size, _size, after)
}
};
module.exports = class LogPlugin {
constructor (opts) {
this.options = {
expression: /\blog\.(\w+)\b/ig,
...opts
}
this.plugin = { name: 'LogPlugin' }
}
doLog (module) {
if (!module._source) return
let _val = module._source.source(),
_name = module.resource;
const filedColor = colorHash.hex(module._buildHash)
// 判斷是否須要加入
if (this.options.expression.test(_val)) {
module.addDependency(new LogDependency(module));
}
_val = _val.replace(
this.options.expression,
`console.log('%c$1字段:%c%o, %c%s', 'color: ${filedColor}', 'color: red', $1, 'color: pink', '${_name}')`
)
return _val
}
apply (compiler) {
compiler.hooks.compilation.tap(this.plugin, (compilation) => {
// 註冊自定義依賴模板
compilation.dependencyTemplates.set(
LogDependency,
new LogDependency.Template()
);
// modlue解析完畢鉤子
compilation.hooks.succeedModule.tap(this.plugin, module => {
// 修改模塊的代碼
module._source._value = this.doLog(module)
})
})
}
}
複製代碼