Plugin 的作用
通過插件我們可以擴(kuò)展 webpack,加入自定義的構(gòu)建行為,使 webpack 可以執(zhí)行更廣泛的任務(wù),擁有更強(qiáng)的構(gòu)建能力。
Plugin 工作原理
webpack 就像一條生產(chǎn)線,要經(jīng)過一系列處理流程后才能將源文件轉(zhuǎn)換成輸出結(jié)果。 這條生產(chǎn)線上的每個(gè)處理流程的職責(zé)都是單一的,多個(gè)流程之間有存在依賴關(guān)系,只有完成當(dāng)前處理后才能交給下一個(gè)流程去處理。
插件就像是一個(gè)插入到生產(chǎn)線中的一個(gè)功能,在特定的時(shí)機(jī)對(duì)生產(chǎn)線上的資源做處理。webpack 通過 Tapable 來組織這條復(fù)雜的生產(chǎn)線。 webpack 在運(yùn)行過程中會(huì)廣播事件,插件只需要監(jiān)聽它所關(guān)心的事件,就能加入到這條生產(chǎn)線中,去改變生產(chǎn)線的運(yùn)作。
webpack 的事件流機(jī)制保證了插件的有序性,使得整個(gè)系統(tǒng)擴(kuò)展性很好。
——「深入淺出 Webpack」
站在代碼邏輯的角度就是:webpack 在編譯代碼過程中,會(huì)觸發(fā)一系列 Tapable 鉤子事件,插件所做的,就是找到相應(yīng)的鉤子,往上面掛上自己的任務(wù),也就是注冊(cè)事件,這樣,當(dāng) webpack 構(gòu)建的時(shí)候,插件注冊(cè)的事件就會(huì)隨著鉤子的觸發(fā)而執(zhí)行了。
Webpack 內(nèi)部的鉤子
什么是鉤子
鉤子的本質(zhì)就是:事件。為了方便我們直接介入和控制編譯過程,webpack 把編譯過程中觸發(fā)的各類關(guān)鍵事件封裝成事件接口暴露了出來。這些接口被很形象地稱做:hooks(鉤子)。開發(fā)插件,離不開這些鉤子。
Tapable
Tapable 為 webpack 提供了統(tǒng)一的插件接口(鉤子)類型定義,它是 webpack 的核心功能庫(kù)。webpack 中目前有十種 hooks,在 Tapable 源碼中可以看到,他們是:
// https://github.com/webpack/tapable/blob/master/lib/index.js
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");
exports.HookMap = require("./HookMap");
exports.MultiHook = require("./MultiHook");
Plugin 構(gòu)建對(duì)象
Compiler
compiler 對(duì)象中保存著完整的 Webpack 環(huán)境配置,每次啟動(dòng) webpack 構(gòu)建時(shí)它都是一個(gè)獨(dú)一無二,僅僅會(huì)創(chuàng)建一次的對(duì)象。
這個(gè)對(duì)象會(huì)在首次啟動(dòng) Webpack 時(shí)創(chuàng)建,我們可以通過 compiler 對(duì)象上訪問到 Webapck 的主環(huán)境配置,比如 loader 、 plugin 等等配置信息。
它有以下主要屬性:
- compiler.options 可以訪問本次啟動(dòng) webpack 時(shí)候所有的配置文件,包括但不限于 loaders 、 entry 、 output 、 plugin 等等完整配置信息。
- compiler.inputFileSystem 和 compiler.outputFileSystem 可以進(jìn)行文件操作,相當(dāng)于 Nodejs 中 fs。
- compiler.hooks 可以注冊(cè) tapable 的不同種類 Hook,從而可以在 compiler 生命周期中植入不同的邏輯。
compiler hooks 文檔
Compilation
compilation 對(duì)象代表一次資源的構(gòu)建,compilation 實(shí)例能夠訪問所有的模塊和它們的依賴。
一個(gè) compilation 對(duì)象會(huì)對(duì)構(gòu)建依賴圖中所有模塊,進(jìn)行編譯。 在編譯階段,模塊會(huì)被加載(load)、封存(seal)、優(yōu)化(optimize)、 分塊(chunk)、哈希(hash)和重新創(chuàng)建(restore)。
它有以下主要屬性:
- compilation.modules 可以訪問所有模塊,打包的每一個(gè)文件都是一個(gè)模塊。
- compilation.chunks chunk 即是多個(gè) modules 組成而來的一個(gè)代碼塊。入口文件引入的資源組成一個(gè) chunk,通過代碼分割的模塊又是另外的 chunk。
- compilation.assets 可以訪問本次打包生成所有文件的結(jié)果。
- compilation.hooks 可以注冊(cè) tapable 的不同種類 Hook,用于在 compilation 編譯模塊階段進(jìn)行邏輯添加以及修改。
compilation hooks 文檔
開發(fā)一個(gè)插件
最簡(jiǎn)單的插件
plugins/test-plugin.js
class TestPlugin {
constructor() {
console.log("TestPlugin constructor()");
}
// 1. webpack讀取配置時(shí),new TestPlugin() ,會(huì)執(zhí)行插件 constructor 方法
// 2. webpack創(chuàng)建 compiler 對(duì)象
// 3. 遍歷所有插件,調(diào)用插件的 apply 方法
apply(compiler) {
console.log("TestPlugin apply()");
}
}
module.exports = TestPlugin;
注冊(cè) hook
class TestPlugin {
constructor() {
console.log("TestPlugin constructor()");
}
// 1. webpack讀取配置時(shí),new TestPlugin() ,會(huì)執(zhí)行插件 constructor 方法
// 2. webpack創(chuàng)建 compiler 對(duì)象
// 3. 遍歷所有插件,調(diào)用插件的 apply 方法
apply(compiler) {
console.log("TestPlugin apply()");
// 從文檔可知, compile hook 是 SyncHook, 也就是同步鉤子, 只能用tap注冊(cè)
compiler.hooks.compile.tap("TestPlugin", (compilationParams) => {
console.log("compiler.compile()");
});
// 從文檔可知, make 是 AsyncParallelHook, 也就是異步并行鉤子, 特點(diǎn)就是異步任務(wù)同時(shí)執(zhí)行
// 可以使用 tap、tapAsync、tapPromise 注冊(cè)。
// 如果使用tap注冊(cè)的話,進(jìn)行異步操作是不會(huì)等待異步操作執(zhí)行完成的。
compiler.hooks.make.tap("TestPlugin", (compilation) => {
setTimeout(() => {
console.log("compiler.make() 111");
}, 2000);
});
// 使用tapAsync、tapPromise注冊(cè),進(jìn)行異步操作會(huì)等異步操作做完再繼續(xù)往下執(zhí)行
compiler.hooks.make.tapAsync("TestPlugin", (compilation, callback) => {
setTimeout(() => {
console.log("compiler.make() 222");
// 必須調(diào)用
callback();
}, 1000);
});
compiler.hooks.make.tapPromise("TestPlugin", (compilation) => {
console.log("compiler.make() 333");
// 必須返回promise
return new Promise((resolve) => {
resolve();
});
});
// 從文檔可知, emit 是 AsyncSeriesHook, 也就是異步串行鉤子,特點(diǎn)就是異步任務(wù)順序執(zhí)行
compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => {
setTimeout(() => {
console.log("compiler.emit() 111");
callback();
}, 3000);
});
compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => {
setTimeout(() => {
console.log("compiler.emit() 222");
callback();
}, 2000);
});
compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => {
setTimeout(() => {
console.log("compiler.emit() 333");
callback();
}, 1000);
});
}
}
module.exports = TestPlugin;
啟動(dòng)調(diào)試
通過調(diào)試查看 compiler 和 compilation 對(duì)象數(shù)據(jù)情況。
package.json 配置指令
{
"name": "source",
"version": "1.0.0",
"scripts": {
"debug": "node --inspect-brk ./node_modules/webpack-cli/bin/cli.js"
},
"keywords": [],
"author": "xiongjian",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.17.10",
"@babel/preset-env": "^7.17.10",
"css-loader": "^6.7.1",
"loader-utils": "^3.2.0",
"webpack": "^5.72.0",
"webpack-cli": "^4.9.2"
}
}
運(yùn)行指令
npm run debug
此時(shí)控制臺(tái)輸出以下內(nèi)容:
PS C:\Users\86176\Desktop\source> npm run debug
> source@1.0.0 debug
> node --inspect-brk ./node_modules/webpack-cli/bin/cli.js
Debugger listening on ws://127.0.0.1:9229/629ea097-7b52-4011-93a7-02f83c75c797
For help, see: https://nodejs.org/en/docs/inspecto
打開 Chrome 瀏覽器,F(xiàn)12 打開瀏覽器調(diào)試控制臺(tái)。
此時(shí)控制臺(tái)會(huì)顯示一個(gè)綠色的圖標(biāo)
調(diào)試控制臺(tái)
點(diǎn)擊綠色的圖標(biāo)進(jìn)入調(diào)試模式。
在需要調(diào)試代碼處用 debugger 打斷點(diǎn),代碼就會(huì)停止運(yùn)行,從而調(diào)試查看數(shù)據(jù)情況。
BannerWebpackPlugin
作用:給打包輸出文件添加注釋。
開發(fā)思路:
需要打包輸出前添加注釋:需要使用 compiler.hooks.emit 鉤子, 它是打包輸出前觸發(fā)。
如何獲取打包輸出的資源?compilation.assets 可以獲取所有即將輸出的資源文件。
實(shí)現(xiàn):文章來源地址http://www.zghlxwxcb.cn/news/detail-813437.html
// plugins/banner-webpack-plugin.js
class BannerWebpackPlugin {
constructor(options = {}) {
this.options = options;
}
apply(compiler) {
// 需要處理文件
const extensions = ["js", "css"];
// emit是異步串行鉤子
compiler.hooks.emit.tapAsync("BannerWebpackPlugin", (compilation, callback) => {
// compilation.assets包含所有即將輸出的資源
// 通過過濾只保留需要處理的文件
const assetPaths = Object.keys(compilation.assets).filter((path) => {
const splitted = path.split(".");
return extensions.includes(splitted[splitted.length - 1]);
});
assetPaths.forEach((assetPath) => {
const asset = compilation.assets[assetPath];
const source = `/*
* Author: ${this.options.author}
*/\n${asset.source()}`;
// 覆蓋資源
compilation.assets[assetPath] = {
// 資源內(nèi)容
source() {
return source;
},
// 資源大小
size() {
return source.length;
},
};
});
callback();
});
}
}
module.exports = BannerWebpackPlugin;
CleanWebpackPlugin
作用:在 webpack 打包輸出前將上次打包內(nèi)容清空。
開發(fā)思路:
- 如何在打包輸出前執(zhí)行?需要使用 compiler.hooks.emit 鉤子, 它是打包輸出前觸發(fā)。
- 如何清空上次打包內(nèi)容?
- 獲取打包輸出目錄:通過 compiler 對(duì)象。
- 通過文件操作清空內(nèi)容:通過 compiler.outputFileSystem 操作文件。
實(shí)現(xiàn):
// plugins/clean-webpack-plugin.js
class CleanWebpackPlugin {
apply(compiler) {
// 獲取操作文件的對(duì)象
const fs = compiler.outputFileSystem;
// emit是異步串行鉤子
compiler.hooks.emit.tapAsync("CleanWebpackPlugin", (compilation, callback) => {
// 獲取輸出文件目錄
const outputPath = compiler.options.output.path;
// 刪除目錄所有文件
const err = this.removeFiles(fs, outputPath);
// 執(zhí)行成功err為undefined,執(zhí)行失敗err就是錯(cuò)誤原因
callback(err);
});
}
removeFiles(fs, path) {
try {
// 讀取當(dāng)前目錄下所有文件
const files = fs.readdirSync(path);
// 遍歷文件,刪除
files.forEach((file) => {
// 獲取文件完整路徑
const filePath = `${path}/${file}`;
// 分析文件
const fileStat = fs.statSync(filePath);
// 判斷是否是文件夾
if (fileStat.isDirectory()) {
// 是文件夾需要遞歸遍歷刪除下面所有文件
this.removeFiles(fs, filePath);
} else {
// 不是文件夾就是文件,直接刪除
fs.unlinkSync(filePath);
}
});
// 最后刪除當(dāng)前目錄
fs.rmdirSync(path);
} catch (e) {
// 將產(chǎn)生的錯(cuò)誤返回出去
return e;
}
}
}
module.exports = CleanWebpackPlugin;
AnalyzeWebpackPlugin
作用:分析 webpack 打包資源大小,并輸出分析文件。
開發(fā)思路:
在哪做? compiler.hooks.emit, 它是在打包輸出前觸發(fā),我們需要分析資源大小同時(shí)添加上分析后的 md 文件。
實(shí)現(xiàn):
// plugins/analyze-webpack-plugin.js
class AnalyzeWebpackPlugin {
apply(compiler) {
// emit是異步串行鉤子
compiler.hooks.emit.tap("AnalyzeWebpackPlugin", (compilation) => {
// Object.entries將對(duì)象變成二維數(shù)組。二維數(shù)組中第一項(xiàng)值是key,第二項(xiàng)值是value
const assets = Object.entries(compilation.assets);
let source = "# 分析打包資源大小 \n| 名稱 | 大小 |\n| --- | --- |";
assets.forEach(([filename, file]) => {
source += `\n| ${filename} | ${file.size()} |`;
});
// 添加資源
compilation.assets["analyze.md"] = {
source() {
return source;
},
size() {
return source.length;
},
};
});
}
}
module.exports = AnalyzeWebpackPlugin;
InlineChunkWebpackPlugin
作用:webpack 打包生成的 runtime 文件太小了,額外發(fā)送請(qǐng)求性能不好,所以需要將其內(nèi)聯(lián)到 js 中,從而減少請(qǐng)求數(shù)量。
開發(fā)思路:
1、我們需要借助 html-webpack-plugin 來實(shí)現(xiàn)
- 在 html-webpack-plugin 輸出 index.html 前將內(nèi)聯(lián) runtime 注入進(jìn)去
- 刪除多余的 runtime 文件
2、如何操作 html-webpack-plugin?官方文檔文章來源:http://www.zghlxwxcb.cn/news/detail-813437.html
實(shí)現(xiàn):
// plugins/inline-chunk-webpack-plugin.js
const HtmlWebpackPlugin = require("safe-require")("html-webpack-plugin");
class InlineChunkWebpackPlugin {
constructor(tests) {
this.tests = tests;
}
apply(compiler) {
compiler.hooks.compilation.tap("InlineChunkWebpackPlugin", (compilation) => {
const hooks = HtmlWebpackPlugin.getHooks(compilation);
hooks.alterAssetTagGroups.tap("InlineChunkWebpackPlugin", (assets) => {
assets.headTags = this.getInlineTag(assets.headTags, compilation.assets);
assets.bodyTags = this.getInlineTag(assets.bodyTags, compilation.assets);
});
hooks.afterEmit.tap("InlineChunkHtmlPlugin", () => {
Object.keys(compilation.assets).forEach((assetName) => {
if (this.tests.some((test) => assetName.match(test))) {
delete compilation.assets[assetName];
}
});
});
});
}
getInlineTag(tags, assets) {
return tags.map((tag) => {
if (tag.tagName !== "script") return tag;
const scriptName = tag.attributes.src;
if (!this.tests.some((test) => scriptName.match(test))) return tag;
return { tagName: "script", innerHTML: assets[scriptName].source(), closeTag: true };
});
}
}
module.exports = InlineChunkWebpackPlugin;
到了這里,關(guān)于Webpack5入門到原理18:Plugin 原理的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!