Webpack簡(jiǎn)單實(shí)現(xiàn)兩個(gè)自定義插件詳解
基礎(chǔ)理論知識(shí)
1、插件的本質(zhì)是一個(gè)函數(shù)或一個(gè)類;
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;
類實(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
,這取決于鉤子的類型,主要分為同步和異步,訪問(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-07uniapp實(shí)現(xiàn)下拉刷新與上拉觸底加載功能的示例代碼
這篇文章主要記錄一下uniapp實(shí)現(xiàn)下拉刷新與上拉觸底加載功能的示例代碼,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-04-04javascript在事件監(jiān)聽(tīng)方面的兼容性小結(jié)
javascript 在事件監(jiān)聽(tīng)方面的兼容性總結(jié),注意是由于多個(gè)瀏覽器的不一致,導(dǎo)致大家在js書(shū)寫(xiě)時(shí)需要考慮多個(gè)瀏覽器的兼容性。2010-04-04JavaScript實(shí)現(xiàn)音樂(lè)導(dǎo)航效果
這篇文章主要為大家詳細(xì)介紹了JavaScript實(shí)現(xiàn)音樂(lè)導(dǎo)航效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-11-11uniapp實(shí)現(xiàn)日期時(shí)間選擇器
這篇文章主要為大家詳細(xì)介紹了uniapp實(shí)現(xiàn)日期時(shí)間選擇器,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-10-10JavaScript實(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-08javascript實(shí)現(xiàn)劃詞標(biāo)記劃詞搜索功能修正版
javascript實(shí)現(xiàn)劃詞標(biāo)記劃詞搜索功能修正版...2006-12-12