Webpack簡(jiǎn)單實(shí)現(xiàn)兩個(gè)自定義插件詳解
基礎(chǔ)理論知識(shí)
1、插件的本質(zhì)是一個(gè)函數(shù)或一個(gè)類(lèi);
2、webpack 在使用插件時(shí),會(huì)初始化一個(gè)插件實(shí)例并調(diào)用其原型對(duì)象上的 apply 方法,apply 方法接收一個(gè) compiler 參數(shù),下文將詳細(xì)介紹這個(gè)參數(shù)。
函數(shù)實(shí)例
function MyPlugin (options) {
}
MyPlugin.prototype.apply = compiler => {
};
module.exports = MyPlugin;類(lèi)實(shí)例
class MyPlugin {
constructor (options) {
}
apply (compiler) {
}
}
module.exports = MyPlugin;compiler 和 compilation
compiler
上文提到的 apply 方法中接收的 compiler 對(duì)象代表了完整的 webpack 環(huán)境配置。
這個(gè)對(duì)象在啟動(dòng) webpack 時(shí)被一次性建立,并配置好所有可操作的設(shè)置,包括 options,loader 和 plugin。
可以簡(jiǎn)單地把它理解為 webpack 實(shí)例,使用它來(lái)訪問(wèn) webpack 的主環(huán)境和配置信息。
另外,compiler 對(duì)象暴露了很多生命周期的鉤子,通過(guò)如下方式使用:
compiler.hooks.someHook.tap("MyPlugin", params => {
/* ... */
});
鉤子的訪問(wèn)方式并不固定為 tap,這取決于鉤子的類(lèi)型,主要分為同步和異步,訪問(wèn)的方式有:tapAsync, tapPromise 等。
compilation
compilation 對(duì)象是從 compiler 鉤子的回調(diào)函數(shù)中傳遞回來(lái)的。
compilation 對(duì)象代表了一次資源版本構(gòu)建。每當(dāng)檢測(cè)到一個(gè)文件變化,就會(huì)創(chuàng)建一個(gè)新的 compilation,從而生成一組新的編譯資源。
一個(gè) compilation 對(duì)象表現(xiàn)了當(dāng)前的模塊資源、編譯生成資源、變化的文件、以及被跟蹤依賴的狀態(tài)信息。
compilation 對(duì)象也暴露了很多生命周期的鉤子,以供插件做自定義處理時(shí)選擇使用。訪問(wèn)方式與 compiler 相同,此處不再贅述。
小結(jié)
compiler 是一個(gè)全局的對(duì)象,是一整個(gè)構(gòu)建的過(guò)程,可以訪問(wèn) webpack 的環(huán)境和配置。
compilation 是對(duì)于某個(gè)模塊而言的,它可以更加精細(xì)地處理各個(gè)模塊的構(gòu)建過(guò)程。
簡(jiǎn)單實(shí)現(xiàn)自定義插件
官方文檔中給了一個(gè) File List Plugin 自定義插件的示例,該插件主要展示了如何獲取構(gòu)建過(guò)程中的資源。
除此之外,我們?cè)賮?lái)實(shí)現(xiàn)兩個(gè)簡(jiǎn)單的自定義插件。
Watch Plugin
這個(gè)插件的作用是:在 webpack 的監(jiān)視(watch)模式下,輸出每次修改變更的資源文件信息。
在查閱官方文檔后,發(fā)現(xiàn)有個(gè) watchRun 的鉤子很符合我們的需求:“在監(jiān)聽(tīng)模式下,一個(gè)新的 compilation 觸發(fā)之后,但在 compilation 實(shí)際開(kāi)始之前執(zhí)行。”。
也就是說(shuō),項(xiàng)目發(fā)生了改動(dòng),會(huì)進(jìn)行一次新的構(gòu)建,生成一個(gè)新的 compilation,并且在這個(gè) compilation 執(zhí)行之前觸發(fā)。
watchRun 的訪問(wèn)方式是 tapAsync,所以除了接收 compiler 參數(shù)外,還會(huì)接收一個(gè)回調(diào)函數(shù),我們需要在邏輯執(zhí)行完畢后調(diào)用這個(gè)回調(diào)函數(shù)。
代碼如下:
/**
* 在 webpack 的 watch 模式下觸發(fā)
*/
class WatchPlugin {
apply (webpackCompiler) {
// watchRun - 在監(jiān)聽(tīng)模式下觸發(fā),在一個(gè) compilation 出現(xiàn)后,在 compilation 執(zhí)行前觸發(fā)
webpackCompiler.hooks.watchRun.tapAsync("WatchPlugin", (compiler, callback) => {
console.log(" 監(jiān)聽(tīng)到了! ");
const mtimes = compiler.watchFileSystem.watcher.mtimes;
if (!mtimes) return;
// 通過(guò)正則處理,避免顯示 node_modules 文件夾下依賴的變化
const mtimesKeys = Object.keys(mtimes).filter(path => !/(node_modules)/.test(path));
if (mtimesKeys.length) {
console.log(` 本次改動(dòng)了 ${mtimesKeys.length} 個(gè)文件,路徑為:\n `, mtimes);
}
callback();
});
// watchClose - 在一個(gè)監(jiān)聽(tīng)中的 compilation 結(jié)束時(shí)觸發(fā)
webpackCompiler.hooks.watchClose.tap("WatchPlugin", () => {
console.log(" 監(jiān)聽(tīng)結(jié)束,再見(jiàn)! ");
});
}
}
module.exports = WatchPlugin;Clean Plugin
模仿 clean-webpack-plugin 實(shí)現(xiàn)一個(gè)每次構(gòu)建時(shí)將上一次構(gòu)建結(jié)果中不再需要的文件刪除。
梳理邏輯
項(xiàng)目文件改動(dòng)后,構(gòu)建結(jié)果的文件 hash 值會(huì)發(fā)生變化,此時(shí)將舊文件刪除;如果沒(méi)有發(fā)生改動(dòng),則無(wú)需刪除。
實(shí)現(xiàn)邏輯
1、考慮在什么時(shí)機(jī)執(zhí)行邏輯?
要根據(jù) hash 判斷文件是否發(fā)生了變化,所以要拿到新、舊文件,那么可以在新的一次構(gòu)建完成后執(zhí)行,此時(shí)可以獲取新構(gòu)建出的文件。
查閱文檔后,compiler.done 這個(gè)鉤子會(huì)在 compilation 完成后觸發(fā),符合我們的需求。
2、如何獲取上一次構(gòu)建出的舊文件?
先獲取 output 的路徑,根據(jù)路徑就可以獲取到 output 文件夾下所有的文件了。這個(gè)操作要放在構(gòu)建開(kāi)始之前。
上文說(shuō)過(guò) compiler 可以訪問(wèn) webpack 的環(huán)境與配置,因此通過(guò) compiler 可以獲取 output 的路徑:
apply (compiler) {
const outputPath = compiler.options.output.path;
}
獲取 output 文件夾中的文件,可以通過(guò) fs.readdirSync 方法和 fs.statSync 方法。
前者是用于讀取某一路徑下所有的文件名,包括文件和文件夾。
后者是用于判斷某一路徑是文件還是文件夾。
使用這兩個(gè)方法,我們可以遞歸獲取 output 文件夾以及其子級(jí)文件夾下所有的文件:
/**
* 獲取文件夾下所有的文件名(包括子級(jí)文件夾中的文件)
* @param {string} dir 文件夾路徑
*/
readAllFiles (dir) {
let fileList = [];
const files = fs.readdirSync(dir);
files.forEach(file => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
fileList = fileList.concat(this.readAllFiles(filePath));
} else {
fileList.push(filePath);
}
});
return fileList;
}
3、如何獲取新構(gòu)建出的文件?
compiler.done 這個(gè)鉤子的回調(diào)函數(shù)接收一個(gè)參數(shù) stat,通過(guò) stat 可以獲取到最新構(gòu)建出的 assets。
這幾個(gè)問(wèn)題解決后,我們將新、舊文件路徑統(tǒng)一化就可以進(jìn)行對(duì)比了。篩選出需要?jiǎng)h除的文件,用 fs.unlinkSync 方法就可以直接刪除了。
代碼如下:
const fs = require("fs");
const path = require("path");
/** 每次編譯時(shí)刪除上一次編譯結(jié)果中不再需要的文件 */
class CleanPlugin {
constructor (options) {
this.options = options;
}
apply (compiler) {
const pluginName = CleanPlugin.name;
// 編譯輸出文件的路徑,根據(jù)此路徑可獲取對(duì)應(yīng)目錄下的所有文件
const outputPath = compiler.options.output.path;
const outputPathPrefix = path.basename(outputPath);
const oldFiles = this.readAllFiles(outputPath);
// console.log(" old files ", oldFiles);
// done - 完成新的編譯后執(zhí)行,此時(shí)能獲取新的輸出文件與現(xiàn)有文件進(jìn)行對(duì)比
compiler.hooks.done.tap(pluginName, stats => {
// 新的一次編譯完成后的輸出文件的相對(duì)路徑
const newFiles = stats.toJson().assets.map(assets => fs.realpathSync(`${outputPathPrefix}\\${assets.name}`));
// console.log(" new files ", newFiles);
// 新舊文件對(duì)比,篩選出需要?jiǎng)h除的文件
const removeFiles = [];
oldFiles.forEach(oldFile => {
if (newFiles.indexOf(oldFile) === -1) {
removeFiles.push(oldFile);
}
});
// 刪除文件
removeFiles.forEach(removeFile => fs.unlinkSync(removeFile));
});
}
/**
* 獲取文件夾下所有的文件名(包括子級(jí)文件夾中的文件)
* @param {string} dir 文件夾路徑
*/
readAllFiles (dir) {
let fileList = [];
const files = fs.readdirSync(dir);
files.forEach(file => {
const filePath = path.join(dir, file);
const stat = fs.statSync(filePath);
if (stat.isDirectory()) {
fileList = fileList.concat(this.readAllFiles(filePath));
} else {
fileList.push(filePath);
}
});
return fileList;
}
}
module.exports = CleanPlugin;
到此這篇關(guān)于Webpack簡(jiǎn)單實(shí)現(xiàn)兩個(gè)自定義插件詳解的文章就介紹到這了,更多相關(guān)Webpack自定義插件內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
js判斷移動(dòng)端橫豎屏視口檢測(cè)實(shí)現(xiàn)的幾種方法
最近做歌一個(gè)小項(xiàng)目,但是要放到我們的app上,然而需要橫豎屏使用不同的樣式,本文就來(lái)介紹一下js判斷移動(dòng)端橫豎屏視口檢測(cè)實(shí)現(xiàn)的幾種方法,感興趣的可以了解一下2021-07-07
uniapp實(shí)現(xiàn)下拉刷新與上拉觸底加載功能的示例代碼
這篇文章主要記錄一下uniapp實(shí)現(xiàn)下拉刷新與上拉觸底加載功能的示例代碼,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-04-04
javascript在事件監(jiān)聽(tīng)方面的兼容性小結(jié)
javascript 在事件監(jiān)聽(tīng)方面的兼容性總結(jié),注意是由于多個(gè)瀏覽器的不一致,導(dǎo)致大家在js書(shū)寫(xiě)時(shí)需要考慮多個(gè)瀏覽器的兼容性。2010-04-04
JavaScript實(shí)現(xiàn)音樂(lè)導(dǎo)航效果
這篇文章主要為大家詳細(xì)介紹了JavaScript實(shí)現(xiàn)音樂(lè)導(dǎo)航效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-11-11
uniapp實(shí)現(xiàn)日期時(shí)間選擇器
這篇文章主要為大家詳細(xì)介紹了uniapp實(shí)現(xiàn)日期時(shí)間選擇器,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-10-10
JavaScript實(shí)現(xiàn)網(wǎng)頁(yè)tab欄效果制作
這篇文章主要為大家詳細(xì)介紹了JavaScript實(shí)現(xiàn)網(wǎng)頁(yè)tab欄效果制作,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-11-11
響應(yīng)式表格之固定表頭的簡(jiǎn)單實(shí)現(xiàn)
下面小編就為大家?guī)?lái)一篇響應(yīng)式表格之固定表頭的簡(jiǎn)單實(shí)現(xiàn)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-08-08
javascript實(shí)現(xiàn)劃詞標(biāo)記劃詞搜索功能修正版
javascript實(shí)現(xiàn)劃詞標(biāo)記劃詞搜索功能修正版...2006-12-12

