【W(wǎng)ebpack】879- 簡單易懂的 webpack 插件原理分析

本文作者:lzg9527
本文鏈接:juejin.cn/post/6901210575162834958
loader 有以下區(qū)別:loader是一個轉(zhuǎn)換器,將 A 文件進(jìn)行編譯成 B 文件,比如:將A.less轉(zhuǎn)換為A.css,單純的文件轉(zhuǎn)換過程。webpack 自身只支持 js 和 json 這兩種格式的文件,對于其他文件需要通過loader將其轉(zhuǎn)換為 commonJS 規(guī)范的文件后,webpack 才能解析到。plugin是一個擴(kuò)展器,它豐富了 webpack 本身,針對是loader結(jié)束后,webpack 打包的整個過程,它并不直接操作文件,而是基于事件機(jī)制工作,會監(jiān)聽 webpack 打包過程中的某些節(jié)點(diǎn),執(zhí)行廣泛的任務(wù)。
plugin 的特征
webpack 插件有以下特征
是一個獨(dú)立的模塊。 模塊對外暴露一個 js 函數(shù)。 函數(shù)的原型 (prototype)上定義了一個注入compiler對象的apply方法。apply函數(shù)中需要有通過compiler對象掛載的 webpack 事件鉤子,鉤子的回調(diào)中能拿到當(dāng)前編譯的compilation對象,如果是異步編譯插件的話可以拿到回調(diào) callback。完成自定義子編譯流程并處理 complition對象的內(nèi)部數(shù)據(jù)。如果異步編譯插件的話,數(shù)據(jù)處理完成后執(zhí)行 callback 回調(diào)。
class HelloPlugin {
// 在構(gòu)造函數(shù)中獲取用戶給該插件傳入的配置
constructor(options) {}
// Webpack 會調(diào)用 HelloPlugin 實(shí)例的 apply 方法給插件實(shí)例傳入 compiler 對象
apply(compiler) {
// 在emit階段插入鉤子函數(shù),用于特定時機(jī)處理額外的邏輯;
compiler.hooks.emit.tap('HelloPlugin', (compilation) => {
// 在功能流程完成后可以調(diào)用 webpack 提供的回調(diào)函數(shù);
})
// 如果事件是異步的,會帶兩個參數(shù),第二個參數(shù)為回調(diào)函數(shù),
compiler.plugin('emit', function (compilation, callback) {
// 處理完畢后執(zhí)行 callback 以通知 Webpack
// 如果不執(zhí)行 callback,運(yùn)行流程將會一直卡在這不往下執(zhí)行
callback()
})
}
}
module.exports = HelloPluginwebpack 讀取配置的過程中會先執(zhí)行 new HelloPlugin(options)初始化一個HelloPlugin獲得其實(shí)例。初始化 compiler對象后調(diào)用HelloPlugin.apply(compiler)給插件實(shí)例傳入 compiler 對象。插件實(shí)例在獲取到 compiler對象后,就可以通過compiler.plugin(事件名稱, 回調(diào)函數(shù)) 監(jiān)聽到 Webpack 廣播出來的事件。并且可以通過compiler對象去操作 Webpack。
事件流機(jī)制
webpack 本質(zhì)上是一種事件流的機(jī)制,它的工作流程就是將各個插件串聯(lián)起來,而實(shí)現(xiàn)這一切的核心就是 Tapable。
Webpack 的 Tapable 事件流機(jī)制保證了插件的有序性,將各個插件串聯(lián)起來, Webpack 在運(yùn)行過程中會廣播事件,插件只需要監(jiān)聽它所關(guān)心的事件,就能加入到這條 webapck 機(jī)制中,去改變 webapck 的運(yùn)作,使得整個系統(tǒng)擴(kuò)展性良好。
Tapable 也是一個小型的 library,是 Webpack 的一個核心工具。類似于 node 中的 events 庫,核心原理就是一個訂閱發(fā)布模式。作用是提供類似的插件接口。方法如下:
// 廣播事件
compiler.apply('event-name', params)
compilation.apply('event-name', params)
// 監(jiān)聽事件
compiler.plugin('event-name', function (params) {})
compilation.plugin('event-name', function (params) {})我們來看下 Tapable
function Tapable() {
this._plugins = {}
}
//發(fā)布name消息
Tapable.prototype.applyPlugins = function applyPlugins(name) {
if (!this._plugins[name]) return
var args = Array.prototype.slice.call(arguments, 1)
var plugins = this._plugins[name]
for (var i = 0; i < plugins.length; i++) {
plugins[i].apply(this, args)
}
}
// fn訂閱name消息
Tapable.prototype.plugin = function plugin(name, fn) {
if (!this._plugins[name]) {
this._plugins[name] = [fn]
} else {
this._plugins[name].push(fn)
}
}
//給定一個插件數(shù)組,對其中的每一個插件調(diào)用插件自身的apply方法注冊插件
Tapable.prototype.apply = function apply() {
for (var i = 0; i < arguments.length; i++) {
arguments[i].apply(this)
}
}Tapable 為 webpack 提供了統(tǒng)一的插件接口(鉤子)類型定義,它是 webpack 的核心功能庫。webpack 中目前有十種 hooks,在 Tapable 源碼中可以看到,他們是:
exports.SyncHook = require('./SyncHook')
exports.SyncBailHook = require('./SyncBailHook')
exports.SyncWaterfallHook = require('./SyncWaterfallHook')
exports.SyncLoopHook = require('./SyncLoopHook')
exports.AsyncParallelHook = require('./AsyncParallelHook')
exports.AsyncParallelBailHook = require('./AsyncParallelBailHook')
exports.AsyncSeriesHook = require('./AsyncSeriesHook')
exports.AsyncSeriesBailHook = require('./AsyncSeriesBailHook')
exports.AsyncSeriesLoopHook = require('./AsyncSeriesLoopHook')
exports.AsyncSeriesWaterfallHook = require('./AsyncSeriesWaterfallHook')
Tapable 還統(tǒng)一暴露了三個方法給插件,用于注入不同類型的自定義構(gòu)建行為:
tap:可以注冊同步鉤子和異步鉤子。 tapAsync:回調(diào)方式注冊異步鉤子。 tapPromise:Promise 方式注冊異步鉤子。
webpack 里的幾個非常重要的對象,Compiler, Compilation 和 JavascriptParser 都繼承了 Tapable 類,它們身上掛著豐富的鉤子。
編寫一個插件
一個 webpack 插件由以下組成:
一個 JavaScript 命名函數(shù)。 在插件函數(shù)的 prototype 上定義一個 apply 方法。 指定一個綁定到 webpack 自身的事件鉤子。 處理 webpack 內(nèi)部實(shí)例的特定數(shù)據(jù)。 功能完成后調(diào)用 webpack 提供的回調(diào)。
下面實(shí)現(xiàn)一個最簡單的插件
class WebpackPlugin1 {
constructor(options) {
this.options = options
}
apply(compiler) {
compiler.hooks.done.tap('MYWebpackPlugin', () => {
console.log(this.options)
})
}
}
module.exports = WebpackPlugin1然后在 webpack 的配置中注冊使用就行,只需要在 webpack.config.js 里引入并實(shí)例化就可以了:
const WebpackPlugin1 = require('./src/plugin/plugin1')
module.exports = {
entry: {
index: path.join(__dirname, '/src/main.js'),
},
output: {
path: path.join(__dirname, '/dist'),
filename: 'index.js',
},
plugins: [new WebpackPlugin1({ msg: 'hello world' })],
}此時我們執(zhí)行一下 npm run build 就能看到效果了

Compiler 對象 (負(fù)責(zé)編譯)
Compiler 對象包含了當(dāng)前運(yùn)行 Webpack 的配置,包括 entry、output、loaders 等配置,這個對象在啟動 Webpack 時被實(shí)例化,而且是全局唯一的。Plugin 可以通過該對象獲取到 Webpack 的配置信息進(jìn)行處理。
compiler 上暴露的一些常用的鉤子:

下面來舉個例子
class WebpackPlugin2 {
constructor(options) {
this.options = options
}
apply(compiler) {
compiler.hooks.run.tap('run', () => {
console.log('開始編譯...')
})
compiler.hooks.compile.tap('compile', () => {
console.log('compile')
})
compiler.hooks.done.tap('compilation', () => {
console.log('compilation')
})
}
}
module.exports = WebpackPlugin2此時我們執(zhí)行一下 npm run build 就能看到效果了

有一些編譯插件中的步驟是異步的,這樣就需要額外傳入一個 callback 回調(diào)函數(shù),并且在插件運(yùn)行結(jié)束時執(zhí)行這個回調(diào)函數(shù)
class WebpackPlugin2 {
constructor(options) {
this.options = options
}
apply(compiler) {
compiler.hooks.beforeCompile.tapAsync('compilation', (compilation, cb) => {
setTimeout(() => {
console.log('編譯中...')
cb()
}, 1000)
})
}
}
module.exports = WebpackPlugin2Compilation 對象
Compilation 對象代表了一次資源版本構(gòu)建。當(dāng)運(yùn)行 webpack 開發(fā)環(huán)境中間件時,每當(dāng)檢測到一個文件變化,就會創(chuàng)建一個新的 compilation,從而生成一組新的編譯資源。一個 Compilation 對象表現(xiàn)了當(dāng)前的模塊資源、編譯生成資源、變化的文件、以及被跟蹤依賴的狀態(tài)信息,簡單來講就是把本次打包編譯的內(nèi)容存到內(nèi)存里。Compilation 對象也提供了插件需要自定義功能的回調(diào),以供插件做自定義處理時選擇使用拓展。
簡單來說,Compilation 的職責(zé)就是構(gòu)建模塊和 Chunk,并利用插件優(yōu)化構(gòu)建過程。
Compiler 代表了整個 Webpack 從啟動到關(guān)閉的生命周期,而 Compilation 只是代表了一次新的編譯,只要文件有改動,compilation 就會被重新創(chuàng)建。
Compilation 上暴露的一些常用的鉤子:

Compiler 和 Compilation 的區(qū)別
Compiler代表了整個 Webpack 從啟動到關(guān)閉的生命周期Compilation只是代表了一次新的編譯,只要文件有改動,compilation就會被重新創(chuàng)建。
手寫插件 1:文件清單
在每次 webpack 打包之后,自動產(chǎn)生一個一個 markdown 文件清單,記錄打包之后的文件夾 dist 里所有的文件的一些信息。
思路:
1.通過 compiler.hooks.emit.tapAsync() 來觸發(fā)生成資源到 output 目錄之前的鉤子 2.通過 compilation.assets 獲取文件數(shù)量 3.定義 markdown 文件的內(nèi)容,將文件信息寫入 markdown 文件內(nèi)
4.給 dist 文件夾里添加一個資源名稱為 fileListName 的變量
5.寫入資源的內(nèi)容和文件大小
6.執(zhí)行回調(diào),讓 webpack 繼續(xù)執(zhí)行
class FileListPlugin {
constructor(options) {
// 獲取插件配置項(xiàng)
this.filename = options && options.filename ? options.filename : 'FILELIST.md'
}
apply(compiler) {
// 注冊 compiler 上的 emit 鉤子
compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, cb) => {
// 通過 compilation.assets 獲取文件數(shù)量
let len = Object.keys(compilation.assets).length
// 添加統(tǒng)計(jì)信息
let content = `# ${len} file${len > 1 ? 's' : ''} emitted by webpack\n\n`
// 通過 compilation.assets 獲取文件名列表
for (let filename in compilation.assets) {
content += `- ${filename}\n`
}
// 往 compilation.assets 中添加清單文件
compilation.assets[this.filename] = {
// 寫入新文件的內(nèi)容
source: function () {
return content
},
// 新文件大小(給 webapck 輸出展示用)
size: function () {
return content.length
},
}
// 執(zhí)行回調(diào),讓 webpack 繼續(xù)執(zhí)行
cb()
})
}
}
module.exports = FileListPlugin手寫插件 2:去除注釋
開發(fā)一個插件能夠去除打包后代碼的注釋,這樣我們的 bundle.js 將更容易閱讀
思路:
1.通過 compiler.hooks.emit.tap() 來觸發(fā)生成文件后的鉤子
2.通過 compilation.assets 拿到生產(chǎn)后的文件,然后去遍歷各個文件
3.通過 .source() 獲取構(gòu)建產(chǎn)物的文本,然后用正則去 replace 調(diào)注釋的代碼
4.更新構(gòu)建產(chǎn)物對象
5.執(zhí)行回調(diào),讓 webpack 繼續(xù)執(zhí)行
class RemoveCommentPlugin {
constructor(options) {
this.options = options
}
apply(compiler) {
// 去除注釋正則
const reg = /("([^\\\"]*(\\.)?)*")|('([^\\\']*(\\.)?)*')|(\/{2,}.*?(\r|\n))|(\/\*(\n|.)*?\*\/)|(\/\*\*\*\*\*\*\/)/g
compiler.hooks.emit.tap('RemoveComment', (compilation) => {
// 遍歷構(gòu)建產(chǎn)物,.assets中包含構(gòu)建產(chǎn)物的文件名
Object.keys(compilation.assets).forEach((item) => {
// .source()是獲取構(gòu)建產(chǎn)物的文本
let content = compilation.assets[item].source()
content = content.replace(reg, function (word) {
// 去除注釋后的文本
return /^\/{2,}/.test(word) || /^\/\*!/.test(word) || /^\/\*{3,}\//.test(word) ? '' : word
})
// 更新構(gòu)建產(chǎn)物對象
compilation.assets[item] = {
source: () => content,
size: () => content.length,
}
})
})
}
}
module.exports = RemoveCommentPlugin
回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~
點(diǎn)擊“閱讀原文”查看 100+ 篇原創(chuàng)文章
