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