【W(wǎng)ebpack Plugin】寫(xiě)了個(gè)插件跟喜歡的女生表白,結(jié)果......
歡迎關(guān)注前端早茶,與廣東靚仔攜手共同進(jìn)階~
一、前言

二、正文
事情是這樣的

直到前兩天,公司新來(lái)了一個(gè)前端妹子。
相視的第一眼,我神迷了,我知道,終究是躲不過(guò)去了......

相逢卻似曾相識(shí),未曾相識(shí)已相思!
當(dāng)晚,徹夜未眠...

第二天早上,從同事的口中得知了女生的名字,我們暫且叫她小舒吧。
為了不暴露我的狼子野心(欲擒故縱拿捏的死死的),我決定出于同事的關(guān)心詢問(wèn)一下項(xiàng)目了解的怎么樣了,有沒(méi)有需要我?guī)兔Φ摹?/p>
沒(méi)想到小舒像抓到了救命稻草一樣:“小哥,你來(lái)得正好,過(guò)來(lái)幫我看看項(xiàng)目怎么跑不起來(lái)??”

我回到座位上,很快得發(fā)現(xiàn)是由于項(xiàng)目中部分包的版本不兼容導(dǎo)致的,更新下版本就可以了。
正準(zhǔn)備起身去找小舒時(shí),一個(gè)奇怪的念頭閃過(guò)......
我決定給我們的第一次交流一個(gè)驚喜:借著這次解決問(wèn)題的機(jī)會(huì),好好拉近一下我們之間的關(guān)系!!!

想法一來(lái)便擋也擋不住。我決定在項(xiàng)目中運(yùn)行一個(gè)插件:當(dāng)啟動(dòng)項(xiàng)目時(shí),直接在控制臺(tái)中向小舒表達(dá)我的心意!!!??????
沒(méi)辦法,單身這么多年肯定是有原因的!一定是我不夠主動(dòng)!這次我可要好好把握這個(gè)機(jī)會(huì)!!!

說(shuō)干就干
有了想法就開(kāi)干,哥從來(lái)不是一個(gè)拖拖拉拉的人。
小舒的項(xiàng)目用的是 Webpack + React 技術(shù)棧,既然想要在項(xiàng)目啟動(dòng)的時(shí)候做事情,那肯定是得寫(xiě)個(gè) Webpack 插件了。
先去官網(wǎng)了解一下 Webpack Plugin 的概念:
Webpack Plugin:向第三方開(kāi)發(fā)者提供了 Webpack 引擎中完整的能力。使用階段式的構(gòu)建回調(diào),開(kāi)發(fā)者可以在 Webpack 構(gòu)建流程中引入自定義的行為。創(chuàng)建插件比創(chuàng)建 loader 更加高級(jí),因?yàn)槟阈枰斫?Webpack 底層的特性來(lái)處理相應(yīng)的鉤子

通俗點(diǎn)說(shuō)就是可以在構(gòu)建流程中插入我們的自定義的行為,至于在哪個(gè)階段插入或者做什么事情都可以通過(guò) Webpack Plugin 來(lái)完成。

tapable的使用姿勢(shì)

舉個(gè)例子??:類比到 Vue 和 React 框架中的生命周期函數(shù),它們就是到了固定的時(shí)間節(jié)點(diǎn)就執(zhí)行對(duì)應(yīng)的生命周期,tapable 做的事情就和這個(gè)差不多,可以先注冊(cè)一系列的生命周期函數(shù),然后在合適的時(shí)間點(diǎn)執(zhí)行。
npm init //初始化項(xiàng)目
yarn add tapable -D //安裝依賴
├── dist # 打包輸出目錄
├── node_modules
├── package-lock.json
├── package.json
└── src # 源碼目錄
└── index.js # 入口文件根據(jù)官方介紹,tapable 使用起來(lái)還是挺簡(jiǎn)單的,只需三步:
實(shí)例化鉤子函數(shù)( tapable會(huì)暴露出各種各樣的 hook,這里以同步鉤子
Synchook為例)注冊(cè)事件
觸發(fā)事件
const { SyncHook } = require("tapable"); //這是一個(gè)同步鉤子
//第一步:實(shí)例化鉤子函數(shù),可以在這里定義形參
const syncHook = new SyncHook(["author"]);
//第二步:注冊(cè)事件1
syncHook.tap("監(jiān)聽(tīng)器1", (name) => {
console.log("監(jiān)聽(tīng)器1:", name);
});
//第二步:注冊(cè)事件2
syncHook.tap("監(jiān)聽(tīng)器2", (name) => {
console.log("監(jiān)聽(tīng)器2", name);
});
//第三步:觸發(fā)事件
syncHook.call("不要禿頭啊");
node ./src/index.js,拿到執(zhí)行結(jié)果:監(jiān)聽(tīng)器1 不要禿頭啊
監(jiān)聽(tīng)器2 不要禿頭啊

從上面的例子中可以看出 tapable 采用的是發(fā)布訂閱模式,通過(guò) tap 函數(shù)注冊(cè)監(jiān)聽(tīng)函數(shù),然后通過(guò) call 函數(shù)按順序執(zhí)行之前注冊(cè)的函數(shù)。
class SyncHook {
constructor() {
this.taps = [];
}
//注冊(cè)監(jiān)聽(tīng)函數(shù),這里的name其實(shí)沒(méi)啥用
tap(name, fn) {
this.taps.push({ name, fn });
}
//執(zhí)行函數(shù)
call(...args) {
this.taps.forEach((tap) => tap.fn(...args));
}
}
Synchook,還有其他類型的 hook:

這里詳細(xì)說(shuō)一下這幾個(gè)類型的概念:
Basic(基本的):執(zhí)行每一個(gè)事件函數(shù),不關(guān)心函數(shù)的返回值
Waterfall(瀑布式的):如果前一個(gè)事件函數(shù)的結(jié)果
result !== undefined,則 result 會(huì)作為后一個(gè)事件函數(shù)的第一個(gè)參數(shù)(也就是上一個(gè)函數(shù)的執(zhí)行結(jié)果會(huì)成為下一個(gè)函數(shù)的參數(shù))Bail(保險(xiǎn)的):執(zhí)行每一個(gè)事件函數(shù),遇到第一個(gè)結(jié)果
result !== undefined則返回,不再繼續(xù)執(zhí)行(也就是只要其中一個(gè)有返回了,后面的就不執(zhí)行了)Loop(循環(huán)的):不停得循環(huán)執(zhí)行事件函數(shù),直到所有函數(shù)結(jié)果
result === undefined
大家也不用死記硬背,遇到相關(guān)的需求時(shí)查文檔就好了。
在上面的例子中我們用的SyncHook,它就是一個(gè)同步的鉤子。又因?yàn)椴⒉魂P(guān)心返回值,所以也算是一個(gè)基本類型的 hook。

tabpable 和 Webpack 的關(guān)系
要說(shuō)它們倆的關(guān)系,可真有點(diǎn)像男女朋友之間的難舍難分......

Webpack 本質(zhì)上是一種事件流的機(jī)制,它的工作流程就是將各個(gè)插件串聯(lián)起來(lái),比如
在打包前需要處理用戶傳過(guò)來(lái)的參數(shù),判斷是采用單入口還是多入口打包,就是通過(guò)
EntryOptionPlugin插件來(lái)做的在打包過(guò)程中,需要知道采用哪種讀文件的方式就是通過(guò)
NodeEnvironmentPlugin插件來(lái)做的在打包完成后,需要先清空 dist 文件夾,就是通過(guò)
CleanWebpackPlugin插件來(lái)完成的......
而實(shí)現(xiàn)這一切的核心就是 tapable。Webpack 內(nèi)部通過(guò) tapable 會(huì)提前定義好一系列不同階段的 hook ,然后在固定的時(shí)間點(diǎn)去執(zhí)行(觸發(fā)
call 函數(shù))。而插件要做的就是通過(guò) tap 函數(shù)注冊(cè)自定義事件,從而讓其控制在 Webapack 事件流上運(yùn)行:
繼續(xù)拿 Vue 和 React 舉例,就好像框架內(nèi)部定義了一系列的生命周期,而我們要做的就是在需要的時(shí)候定義好這些生命周期函數(shù)就好。

Compiler 和 Compilation
compiler 對(duì)象代表了完整的 webpack 生命周期。這個(gè)對(duì)象在啟動(dòng) Webpack 時(shí)被一次性建立,并配置好所有可操作的設(shè)置,包括options,loader和plugin。當(dāng)在 Webpack 環(huán)境中應(yīng)用一個(gè)插件時(shí),插件將收到此compiler對(duì)象的引用。可以使用它來(lái)訪問(wèn) Webpack 的主環(huán)境。compilation 對(duì)象代表了一次資源版本構(gòu)建。當(dāng)運(yùn)行 Webpack 開(kāi)發(fā)環(huán)境中間件( webpack-dev-server)時(shí),每當(dāng)檢測(cè)到一個(gè)文件變化,就會(huì)創(chuàng)建一個(gè)新的 compilation,從而生成一組新的編譯資源。一個(gè)compilation對(duì)象表現(xiàn)了當(dāng)前的模塊資源、編譯生成資源、變化的文件、以及被跟蹤依賴的狀態(tài)信息。compilation對(duì)象也提供了很多關(guān)鍵時(shí)機(jī)的回調(diào),以供插件做自定義處理時(shí)選擇使用。

還是拿 React 框架舉例子...... React:

compiler比喻成 React 組件,在 React 組件中有一系列的生命周期函數(shù)(componentDidMount()、render()、componentDidUpdate()等等),這些鉤子函數(shù)都可以在組件中被定義。compilation比喻成 componentDidUpdate(),componentDidUpdate()只是組件中的某一個(gè)鉤子,它專門負(fù)責(zé)重復(fù)渲染的工作(compilation只是compiler中某一階段的 hook ,主要負(fù)責(zé)對(duì)模塊資源的處理,只不過(guò)它的工作更加細(xì)化,在它內(nèi)部還有一些子生命周期函數(shù))。如果還是不理解,這里畫(huà)個(gè)圖幫助大家理解:


至于為什么要這么處理,原因當(dāng)然是為了解耦!!!
并不需要每次都重新創(chuàng)建 compiler 實(shí)例,只需要重新創(chuàng)建一個(gè) compilation 來(lái)記錄編譯信息即可。
如何編寫(xiě)插件
說(shuō)了這么多,到底要怎么寫(xiě)一個(gè) Webpack 插件?小舒還等著我呢!!!

剛才知道了在 Webpack 中的 compiler 和 compilation 對(duì)象上掛載著一系列的生命周期 hook ,那接下來(lái)應(yīng)該怎么在這些生命周期中注冊(cè)自定義事件呢?
webpack 插件:

Webpack Plugin 其實(shí)就是一個(gè)普通的函數(shù),在該函數(shù)中需要我們定制一個(gè) apply 方法。當(dāng) Webpack 內(nèi)部進(jìn)行插件掛載時(shí)會(huì)執(zhí)行 apply 函數(shù)。我們可以在 apply 方法中訂閱各種生命周期鉤子,當(dāng)?shù)竭_(dá)對(duì)應(yīng)的時(shí)間點(diǎn)時(shí)就會(huì)執(zhí)行。

這里可能有同學(xué)要問(wèn)了,為什么非要定制一個(gè)apply方法?為什么不是其他的方法?
if (options.plugins && Array.isArray(options.plugins)) {
//這里的options.plugins就是webpack.config.js中的plugins
for (const plugin of options.plugins) {
plugin.apply(compiler); //執(zhí)行插件的apply方法
}
}

那我們就按照規(guī)范寫(xiě)一個(gè)簡(jiǎn)易版的插件趕緊來(lái)練練手:在構(gòu)建完成后打印日志。
首先我們需要知道構(gòu)建完成后對(duì)應(yīng)的的生命周期是哪個(gè),通過(guò) 查閱文檔得知是 complier 中的done 這個(gè) hook :

接下來(lái)創(chuàng)建一個(gè)新項(xiàng)目驗(yàn)證我們的想法,時(shí)間不早了!小舒現(xiàn)在肯定很著急!!!
安裝依賴:
npm init //初始化項(xiàng)目
yarn add webpack webpack-cli -D
├── dist # 打包輸出目錄
├── plugins # 自定義插件文件夾
│ └── demo-plugin.js
├── node_modules
├── package-lock.json
├── package.json
├── src # 源碼目錄
│ └── index.js # 入口文件
└── webpack.config.js # webpack配置文件
class DemoPlugin {
apply(compiler) {
//在done(構(gòu)建完成后執(zhí)行)這個(gè)hook上注冊(cè)自定義事件
compiler.hooks.done.tap("DemoPlugin", () => {
console.log("DemoPlugin:編譯結(jié)束了");
});
}
}
module.exports = DemoPlugin;
{
"name": "my_webpack_plugin",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack"
},
"author": "",
"license": "ISC",
"devDependencies": {
"tapable": "^2.2.1",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1"
},
"dependencies": {
"chalk": "^4.2.0",
"child_process": "^1.0.2"
}
}
console.log("author:","不要禿頭啊");
const DemoPlugin = require("./plugins/demo-plugin");
module.exports = {
mode: "development",
entry: "./src/index.js",
devtool: false,
plugins: [new DemoPlugin()],
};
yarn build
$ webpack
DemoPlugin:編譯結(jié)束了
asset main.js 643 bytes [emitted] (name: main)
./src/index.js 476 bytes [built] [code generated]
webpack 5.74.0 compiled successfully in 71 ms
? Done in 0.64s.

開(kāi)始我的表白之路....
好了,終于搞清楚怎么寫(xiě)插件了!!!

直接把剛才學(xué)的的demo插件改造一下:
class DonePlugin {
apply(compiler) {
//在done(構(gòu)建完成后執(zhí)行)這個(gè)hook上注冊(cè)自定義事件
compiler.hooks.done.tap("DonePlugin", () => {
console.log(
"小姐姐,我知道此刻你很意外。但不知道怎么回事,我看見(jiàn)你的第一眼就淪陷了...可以給我一個(gè)多了解了解你的機(jī)會(huì)嗎? ————來(lái)自一個(gè)熱心幫你解決問(wèn)題的人"
);
});
}
}
module.exports = DonePlugin;
正準(zhǔn)備提交代碼,思來(lái)想去,直接叫小姐姐好像不太好吧?是不是顯得我很輕浮?
再說(shuō)了,小舒怎么知道我在跟她說(shuō)呢?
const chalk = require("chalk");//給日志加顏色插件
const execSync = require("child_process").execSync;
const error = chalk.bold.red; //紅色日志
const warning = chalk.keyword("orange"); //橘色日志
class DonePlugin {
apply(compiler) {
compiler.hooks.done.tap("DonePlugin", () => {
//獲取git賬號(hào)信息的username
let name = execSync("git config user.name").toString().trim();
console.log(
error(`${name},`),
warning(
"我知道此刻你很意外。但不知道怎么回事,我看見(jiàn)你的第一眼就淪陷了...可以給我一個(gè)多了解了解你的機(jī)會(huì)嗎? ————來(lái)自一個(gè)熱心幫你解決問(wèn)題的人"
)
);
});
}
}
module.exports = DonePlugin;
大致效果就是這樣...


等待回應(yīng)
把這一切都準(zhǔn)備妥當(dāng)后,剩下的就交給天意了。
結(jié)果是左等右等,到了下午四點(diǎn)遲遲沒(méi)有等到小舒的回應(yīng)......

難道是沒(méi)看到嗎?不應(yīng)該啊,日志還加了顏色,很明顯了!!!
莫非是女孩子太含蓄了,害羞了?
不行,我得主動(dòng)出擊!!

乘興而去,敗興而歸!!!還在同事圈里鬧了個(gè)笑話!!!
但是為了下半生,豁出去了!!!
經(jīng)過(guò)我的一番解釋,小舒總算相信了我說(shuō)的話,而我也趕緊去優(yōu)化了一下代碼......
自此以后,每天一句不重樣的小情話,小舒甚至還和我互動(dòng)了起來(lái):


接下來(lái)也該考慮結(jié)婚了!!!
“滴~~~,滴~~~,滴~~~,不要命了!等個(gè)紅綠燈都能睡著?“
“喂,醒醒,醒醒。我的尿黃,讓我去漬醒他!”
只聽(tīng)旁邊有人說(shuō)到......
原來(lái)只是黃粱一夢(mèng)。

最后的結(jié)局
我決定勇敢的試一試:


面試題庫(kù)推薦



三、最后
關(guān)注我,一起攜手進(jìn)階
