你盼世界,我盼望你無bug
。Hello 你們好!我是霖呆呆!javascript
有不少小夥伴在打算學寫一個webpack
插件的時候,就被官網上那一長條一長條的API
給嚇到了,亦或者翻閱了幾篇文章以後但仍是不知道從何下手。css
而呆呆認爲,當你瞭解了整個插件的建立方式以及執行機制以後,那些個長條的API
就只是你後期用來開發的"工具庫"
而已,我須要什麼,我就去文檔上找,大可沒必要以爲它有多難 😊。html
本篇文章會教你們從淺到深的實現一個個webpack
插件,案例雖然都不是什麼特別難的插件,可是一旦你掌握瞭如何寫一個插件的方法以後,剩下的就只是在上面作增量了。呆呆仍是那句話:"授人予魚不如授人予漁"
。前端
OK👌,讓咱們來看看經過閱讀本篇文章你能夠學習到:java
No1-webpack-plugin
案例Tapable
compiler?compile?compilation?
No2-webpack-plugin
案例fileList.md
案例Watch-plugin
案例Decide-html-plugin
案例Clean-plugin
案例全部文章內容都已整理至 LinDaiDai/niubility-coding-js 快來給我Star呀😊~node
此係列記錄了我在webpack
上的學習歷程。若是你也和我同樣想要好好的掌握webpack
,那麼我認爲它對你是有必定幫助的,由於教材中是以一名webpack
小白的身份進行講解, 案例demo
也都很詳細, 涉及到:webpack
建議先mark
再花時間來看。git
(其實這個系列在很早以前就寫了,一直沒有發出來,當時還寫了一大長串前言可把我感動的,想看廢話的能夠點這裏:GitHub地址,不過如今讓咱們正式開始學習吧)github
全部文章webpack
版本號^4.41.5
, webpack-cli
版本號^3.3.10
。web
(本章節教材案例GitHub地址: LinDaiDai/webpack-example/tree/webpack-custom-plugin ⚠️:請仔細查看README說明)
好了,我已經準備好閱讀呆呆的這篇文章而後寫一個炒雞牛x的插件了,趕忙的。
額,等等,在這以前咱們不是得知道須要怎麼去作嗎?咱們老是聽到的插件插件的,它究竟是個啥啊?
對象?函數?類?
小夥伴們不妨結合咱們已經用過的一些插件來猜猜,好比HtmlWebpackPlugin
,咱們會這樣使用它:
module.exports = {
plugins: [
new HtmlWebpackPlugin({
title: 'custom-plugin'
})
]
}
複製代碼
能夠看到,這很明顯的就是個構造函數,或者是一個類嘛。咱們使用new
就能夠實例化一個插件的對象。而且,這個函數或者類是可讓咱們傳遞參數進去的。
那你腦子裏是否是已經腦補出一個輪廓了呢?
function CustomPlugin (options) {}
// or
class CustomPlugin {
constructor (options) {}
}
複製代碼
知道了plugin
大概的輪廓,讓咱們從構建的角度來看看它。插件不一樣於loader
一個很大的區別就是,loader
它是一個轉換器,它只專一於轉換這一個領域,例如babel-loader
能將ES6+
的代碼轉換爲ES5
或如下,以此來保證兼容性,那麼它是運行在打包以前的。
而plugin
呢?你會發現市場上有各類讓人眼花繚亂的插件,它可能運行在打包以前,也可能運行在打包的過程當中,或者打包完成以後。總之,它不侷限於打包,資源的加載,還有其它的功能。因此它是在整個編譯週期都起做用。
那麼若是讓咱們站在一個編寫插件者的角度上來看的話,是否是在編寫的時候須要明確兩件事情:
webpack
環境配置呢?由於我在編寫插件的時候確定是要與webpack
的主環境結合起來的webpack
個人插件是在何時發揮做用呢?在打包以前?仍是以後?也就是咱們常常聽到的鉤子。因此這時候咱們就得清楚這幾個硬知識點:
(看不懂?問題不大,呆呆也是從官網cv過來的,不事後面會詳細講到它們哦)
compiler
對象表明了完整的 webpack 環境配置。這個對象在啓動 webpack 時被一次性創建,並配置好全部可操做的設置,包括 options,loader 和 plugin。當在 webpack 環境中應用一個插件時,插件將收到此 compiler 對象的引用。可使用它來訪問 webpack 的主環境。
compilation
對象表明了一次資源版本構建。當運行 webpack 開發環境中間件時,每當檢測到一個文件變化,就會建立一個新的 compilation,從而生成一組新的編譯資源。一個 compilation 對象表現了當前的模塊資源、編譯生成資源、變化的文件、以及被跟蹤依賴的狀態信息。compilation 對象也提供了不少關鍵時機的回調,以供插件作自定義處理時選擇使用。
鉤子的本質其實就是事件
老規矩,爲了能更好的讓咱們掌握本章的內容,咱們須要本地建立一個案例來進行講解。
建立項目的這個過程我就快速的用指令來實現一下哈:
mkdir webpack-custom-plugin && cd webpack-custom-plugin
npm init -y
cnpm i webpack webpack-cli clean-webpack-plugin html-webpack-plugin --save-dev
touch webpack.config.js
mkdir src && cd src
touch index.js
複製代碼
(mkdir
:建立一個文件夾;touch
:建立一個文件)
OK👌,此時項目目錄變成了:
webpack-custom-plugin
|- package.json
|- webpack.config.js
|- /src
|- index.js
複製代碼
接着讓咱們給src/index.js
隨便加點東西意思一下,免得太空了:
src/index.js
function createElement () {
const element = document.createElement('div')
element.innerHTML = '孔子曰:中午不睡,下午崩潰!孟子曰:孔子說的對!';
return element
}
document.body.appendChild(createElement())
複製代碼
webpack.config.js
也簡單的來配置一下吧,這些應該都是基礎了,以前有詳細說過了喲:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new HtmlWebpackPlugin({
title: 'custom-plugin'
}),
new CleanWebpackPlugin()
]
}
複製代碼
(clean-webpack-plugin
插件會在咱們每次打包以前自動清理掉舊的dist
文件夾,對這些內容還不熟悉的小夥伴得再看看這篇文章了:跟着"呆妹"來學webpack(基礎篇))
另外還須要在package.json
中配置一條打包指令哈:
{
"script": {
"build": "webpack --mode development"
}
}
複製代碼
這裏的"webpack"
其實是"webpack --config webpack.config.js"
的縮寫,這點在基礎篇中也有說到咯。
--mode development
就是指定一下環境爲開發環境,由於咱們後續可能有須要看到打包以後的代碼內容,若是指定了爲production
的話,那麼webpack
它會自動開啓UglifyJS
的也就是會對咱們打包成功以後的代碼進行壓縮輸出,那一坨一坨的代碼咱們就不利於咱們查看了。
好的了,基本工做已經準備完畢了,讓咱們動手來編寫咱們的第一個插件吧。
這個插件案例主要是爲了幫助你瞭解插件大概的建立流程。
從易到難,讓咱們來實現這麼一個簡單的功能:
"good boy!"
在剛剛的案例目錄中新建一個plugins
文件夾,而後在裏面建立上咱們的第一個插件: No1-webpack-plugin
:
webpack-custom-plugin
|- package.json
|- webpack.config.js
|- /src
|- index.js
+ |- /plugins
+ |-No1-webpack-plugin.js
複製代碼
如今依照前面所說的插件的結構,以及咱們的需求,能夠寫出如下代碼:
plugins/No1-webpack-plugin.js:
// 1. 建立一個構造函數
function No1WebpackPlugin (options) {
this.options = options
}
// 2. 重寫構造函數原型對象上的 apply 方法
No1WebpackPlugin.prototype.apply = function (compiler) {
compiler.plugin('done', () => {
console.log(this.options.msg)
})
}
// 3. 將咱們的自定義插件導出
module.exports = No1WebpackPlugin;
複製代碼
接着,讓咱們來看看如何使用它,也就是:
webpack.config.js:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
+ const No1WebpackPlugin = require('./plugins/No1-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new HtmlWebpackPlugin({
title: 'custom-plugin'
}),
new CleanWebpackPlugin(),
+ new No1WebpackPlugin({ msg: 'good boy!' })
]
}
複製代碼
OK👌,代碼已經編寫完啦,快npm run build
一下看看效果吧:
能夠看到,控制檯已經在誇你"good boy!"
了😄。
那麼讓咱們回到剛剛的那段自定義插件的代碼中:
plugins/No1-webpack-plugin.js:
// 1. 建立一個構造函數
function No1WebpackPlugin (options) {
this.options = options
}
// 2. 在構造函數原型對象上定義一個 apply 方法
No1WebpackPlugin.prototype.apply = function (compiler) {
compiler.plugin('done', () => {
console.log(this.options.msg)
})
}
// 3. 將咱們的自定義插件導出
module.exports = No1WebpackPlugin;
複製代碼
注意到這裏,咱們一共是作了這麼三件事情,也就是我在代碼中的註釋。
很顯然,爲了能拿到webpack.config.js
中咱們傳遞的那個參數,也就是{ msg: 'good boy!' }
,咱們須要在構造函數中定義一個實例對象上的屬性options
。
而且在prototype.apply
中呢:
compiler.plugin()
並傳入第一個參數來指定咱們的插件是發生在哪一個階段,也就是這裏的"done"
(一次編譯完成以後,即打包完成以後);this
獲取到的是咱們的實例對象,也就是爲了能保證咱們拿到options
,併成功的打印出msg
。(若是對this
還不熟悉的小夥伴你該看看呆呆的這篇文章了:【建議👍】再來40道this面試題酸爽繼續(1.2w字用手整理))因此,如今你的思惟是否是已經很清晰了呢?咱們想要編寫一個插件,只須要這麼幾步:
webpack.config.js
中的配置);compiler.plugin
註冊咱們的自定義插件。那麼除了用構造函數的方式來建立插件,是否也能夠用類呢?讓咱們一塊兒來試試,將剛剛的代碼改動一下:
plugins/No1-webpack-plugin.js:
// // 1. 建立一個構造函數
// function No1WebpackPlugin (options) {
// this.options = options
// }
// // 2. 重寫構造函數原型對象上的 apply 方法
// No1WebpackPlugin.prototype.apply = function (compiler) {
// compiler.plugin('done', () => {
// console.log(this.options.msg)
// })
// }
class No1WebpackPlugin {
constructor (options) {
this.options = options
}
apply (compiler) {
compiler.plugin('done', () => {
console.log(this.options.msg)
})
}
}
// 3. 將咱們的自定義插件導出
module.exports = No1WebpackPlugin;
複製代碼
這時候你執行打包指令效果也是同樣的哈。這其實也很好理解,class
它不就是我們構造函數的一個語法糖嗎,因此它確定也能夠用來實現一個插件啦。
不過不知道小夥伴們注意到了,在咱們剛剛輸出"good boy!"
的上面,還有一段小小的警告:
Tabable.plugin
這種的調用形式已經被廢棄了,請使用新的
API
,也就是
.hooks
來替代
.plugin
這種形式。
若是你和呆呆同樣,開始看的官方文檔是 《編寫一個插件》這裏的話,那麼如今請讓咱們換個方向了戳這裏了: 《Plugin API》。
但並非說上面的文檔就不能看了,咱們依然仍是能夠經過閱讀它來了解更多插件相關的知識。
既然官方都推薦咱們用compiler.hooks
了,那咱們就遵循唄。不過若是你直接去看Plugin API的話對新手來講好像又有點繞,裏面的Tapable
、compiler
、compile
、compilation
它們直接究竟是存在怎樣的關係呢?
不要緊,呆呆都會依次的進行講解。
如今讓咱們將No1-webpack-plugin
使用compiler.hooks
改造一下吧:
plugins/No1-webpack-plugin.js:
// 初版
// function No1WebpackPlugin (options) {
// this.options = options
// }
// No1WebpackPlugin.prototype.apply = function (compiler) {
// compiler.plugin('done', () => {
// console.log(this.options.msg)
// })
// }
// 第二版
// class No1WebpackPlugin {
// constructor (options) {
// this.options = options
// }
// apply (compiler) {
// compiler.plugin('done', () => {
// console.log(this.options.msg)
// })
// }
// }
// 第三版
function No1WebpackPlugin (options) {
this.options = options
}
No1WebpackPlugin.prototype.apply = function (compiler) {
compiler.hooks.done.tap('No1', () => {
console.log(this.options.msg)
})
}
module.exports = No1WebpackPlugin;
複製代碼
能夠看到,第三版中,關鍵點就是在於:
compiler.hooks.done.tap('No1', () => {
console.log(this.options.msg)
})
複製代碼
它替換了咱們以前的:
compiler.plugin('done', () => {
console.log(this.options.msg)
})
複製代碼
讓咱們來拆分一下compiler.hooks.done.tap('No1', () => {})
:
compiler
:一個擴展至Tapable
的對象compiler.hooks
:compiler
對象上的一個屬性,容許咱們使用不一樣的鉤子函數.done
:hooks
中經常使用的一種鉤子,表示在一次編譯完成後執行,它有一個回調參數stats
(暫時沒用上).tap
:表示能夠註冊同步的鉤子和異步的鉤子,而在此處由於done
屬於異步AsyncSeriesHook
類型的鉤子,因此這裏表示的是註冊done
異步鉤子。.tap('No1')
:tap()
的第一個參數'No1'
,其實tap()
這個方法它的第一個參數是能夠容許接收一個字符串或者一個Tap類的對象的,不過在此處咱們不深究,你先隨便傳一個字符串就好了,我把它理解爲此次調用鉤子的方法名。因此讓咱們連起來理解這段代碼的意思就是:
new No1WebpackPlugin()
的時候,會初始化一個插件實例且調用其原型對象上的apply
方法webpack
當你在一次編譯完成以後,得執行一下個人箭頭函數裏的內容,也就是打印出msg
如今咱們雖然會寫一個簡單的插件了,可是對於上面的一些對象、屬性啥的好像還不是很懂耶。想要一口氣吃完一頭大象🐘是有點難的哦(並且那樣也是犯法的),因此接下來讓咱們來大概瞭解一下這些Tapable
、compiler
等等的東西是作什麼的😊。
首先是Tapable
這個東西,我看了一下網上有不少對它的描述:
固然這些說法確定都是對的哈,因此總結一下:
其實若是你去看了它Git上的文檔的話,它就是暴露了9個Hooks
類,以及3種方法(tap、tapAsync、tapPromise
),可用於爲插件建立鉤子。
9種Hooks
類與3種方法之間的關係:
Hooks
類表示的是你的鉤子是哪種類型的,好比咱們上面用到的done
,它就屬於AsyncSeriesHook
這個類tap、tapAsync、tapPromise
這三個方法是用於注入不一樣類型的自定義構建行爲,由於咱們的鉤子可能有同步的鉤子,也可能有異步的鉤子,而咱們在注入鉤子的時候就得選對這三種方法了。對於Hooks
類你大可沒必要全都記下,通常來講你只須要知道咱們要用的每種鉤子它們其實是有類型區分的,而區分它們的就是Hooks
類。
若是你想要清楚它們以前的區別的話,呆呆這裏也有找到一個解釋的比較清楚的總結:
Sync*
Async*
(總結來源:XiaoLu-寫一個簡單webpack plugin所引起的思考)
而對於這三種方法,咱們必須得知道它們分別是作什麼用的:
tap
:能夠註冊同步鉤子也能夠註冊異步鉤子tapAsync
:回調方式註冊異步鉤子tapPromise
:Promise
方式註冊異步鉤子OK👌,聽了霖呆呆這段解釋以後,我相信你起碼能看得懂官方文檔-compiler 鉤子這裏面的鉤子是怎樣用的了:
就好比,我如今想要註冊一個compile
的鉤子,根據官方文檔,我發現它是SyncHook
類型的鉤子,那麼咱們就只能使用tap
來註冊它。若是你試圖用tapAsync
的話,打包的話你就會發現控制檯已經報錯了,好比這樣:
(額,不過我在使用compiler.hooks.done.tapAsync()
的時候,查閱文檔上它也是SyncHook
類,可是卻能夠用tapAsync
方法註冊,這邊呆呆也有點沒搞明白是爲何,有知道的小夥伴還但願能夠評論區留言呀😄)
接下來就得說一說插件中幾個重要的東西了,也就是這一小節的標題裏的這三個東西。
首先讓咱們在官方的文檔上找尋一下它們的足跡:
能夠看到,這幾個屬性都長的好像啊,並且更過度的是,compilation
居然還有兩個同名的,你這是給👴整真假美猴王呢?
那麼呆呆這邊就對這幾個屬性作一下說明。
首先對於文檔左側菜單上的compiler
鉤子和compilation
鉤子(也就是第一個和第四個)咱們在以後稱它們爲Compiler
和Compilation
好了,也是爲了和compile
作區分,其實我認爲你能夠把"compiler鉤子"
理解爲"compiler的鉤子"
,這樣會更好一些。
Compiler
:是一個對象,該對象表明了完整的webpack
環境配置。整個webpack
在構建的時候,會先初始化參數也就是從配置文件(webpack.config.js
)和Shell
語句("build": "webpack --mode development"
)中去讀取與合併參數,以後開始編譯,也就是將最終獲得的參數初始化這個Compiler
對象,而後再會加載全部配置的插件,執行該對象的run()
方法開始執行編譯。所以咱們能夠理解爲它是webpack
的支柱引擎。Compilation
:也是一個對象,不過它表示的是某一個模塊的資源、編譯生成的資源、變化的文件等等,由於咱們知道咱們在使用webpack
進行構建的時候多是會生成不少不一樣的模塊的,而它的顆粒度就是在每個模塊上。因此你如今能夠看到它倆的區別了,一個是表明了整個構建的過程,一個是表明構建過程當中的某個模塊。
還有很重要的一點,它們兩都是擴展至咱們上面👆提到的Tapable
類,這也就是爲何它兩都能有這麼多生命週期鉤子的緣由。
再來看看兩個小寫的compile和compilation
,這兩個其實就是Compiler
對象下的兩個鉤子了,也就是咱們能夠經過這樣的方式來調用它們:
No1WebpackPlugin.prototype.apply = function (compiler) {
compiler.hooks.compile.tap('No1', () => {
console.log(this.options.msg)
})
compiler.hooks.compilation.tap('No1', () => {
console.log(this.options.msg)
})
}
複製代碼
區別在於:
compile
:一個新的編譯(compilation)建立以後,鉤入(hook into) compiler。compilation
:編譯(compilation)建立以後,執行插件。(爲何感受仍是沒太讀懂它們的意思呢?別急,呆呆會在下個例子中來進行說明的)
這個插件案例主要是爲了幫你理解Compiler、Compilation、compile、compilation
四者之間的關係。
仍是在上面👆那個項目中,讓咱們在plugins
文件夾下再新增一個插件,叫作No2-webpack-plugin
:
plugins/No2-webpack-plugin.js:
function No2WebpackPlugin (options) {
this.options = options
}
No2WebpackPlugin.prototype.apply = function (compiler) {
compiler.hooks.compile.tap('No2', () => {
console.log('compile')
})
compiler.hooks.compilation.tap('No2', () => {
console.log('compilation')
})
}
module.exports = No2WebpackPlugin;
複製代碼
在這個插件中,我分別調用了compile
和compilation
兩個鉤子函數,等會讓咱們看看會發生什麼事情。
同時,把webpack.config.js
中的No1
插件替換成No2
插件:
webpack.config.js:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
// const No1WebpackPlugin = require('./plugins/No1-webpack-plugin');
const No2WebpackPlugin = require('./plugins/No2-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new HtmlWebpackPlugin({
title: 'custom-plugin'
}),
new CleanWebpackPlugin(),
// new No1WebpackPlugin({ msg: 'good boy!' })
new No2WebpackPlugin({ msg: 'bad boy!' })
]
}
複製代碼
如今項目的目錄結構是這樣的:
webpack-custom-plugin
|- package.json
|- webpack.config.js
|- /src
|- index.js
|- /plugins
|-No1-webpack-plugin.js
+ |-No2-webpack-plugin.js
複製代碼
OK👌,來執行npm run build
看看:
哈哈哈😄,是否是給了你點什麼啓發呢?
咱們最終生成的dist
文件夾下會有兩個文件,那麼compilation
這個鉤子就被調用了兩次,而compile
鉤子就只被調用了一次。
有小夥伴可能就要問了,咱們這裏的src
下明明就只有一個index.js
文件啊,爲何最終的dist
下會有兩個文件呢?
main.bundle.js
index.html
別忘了,在這個項目中咱們但是使用了html-webpack-plugin
這個插件的,它會幫我自動建立一個html
文件。
爲了驗證這個compilation
是跟着文件的數量走的,咱們暫時先把new HtmlWebpackPlugin
給去掉看看:
const path = require('path');
// const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
// const No1WebpackPlugin = require('./plugins/No1-webpack-plugin');
const No2WebpackPlugin = require('./plugins/No2-webpack-plugin');
module.exports = {
entry: './src/index.js',
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
// new HtmlWebpackPlugin({
// title: 'custom-plugin'
// }),
new CleanWebpackPlugin(),
// new No1WebpackPlugin({ msg: 'good boy!' })
new No2WebpackPlugin({ msg: 'bad boy!' })
]
}
複製代碼
試試效果?
這時候,compilation
就只執行一次了,並且dist
中也沒有再生成html
文件了。
(固然,我這裏只是爲了演示哈,在肯定完了以後,我又把html-webpack-plugin
給啓用了)
想必上面兩個鉤子函數的區別你們應該都搞懂了吧,接下來就讓咱們看看Compiler
和Compilation
這兩個對象的區別。
經過查看官方文檔,咱們發現,剛剛用到的compiler.hooks.compilation
這個鉤子,是可以接收一個參數的:
貌似這個形參的名字就是叫作compilation
,它和Compilation
對象是否是有什麼聯繫呢?或者說,它就是一個Compilation
?。
OK👌,我就假設它是吧,接下來我去查看了一下compilation鉤子
,哇,這鉤子的數量是有點多哈,隨便挑個順眼的來玩玩?額,翻到最下面,有個chunkAsset
,要不就它吧:
能夠看到這個鉤子函數是有兩個參數的:
chunk
:表示的應該就是當前的模塊吧filename
:模塊的名稱接着讓咱們來改寫一下No2-webpack-plugin
插件:
src/No2-webpack-plugin.js:
function No2WebpackPlugin (options) {
this.options = options
}
No2WebpackPlugin.prototype.apply = function (compiler) {
compiler.hooks.compile.tap('No2', (compilation) => {
console.log('compile')
})
compiler.hooks.compilation.tap('No2', (compilation) => {
console.log('compilation')
+ compilation.hooks.chunkAsset.tap('No2', (chunk, filename) => {
+ console.log(chunk)
+ console.log(filename)
+ })
})
}
module.exports = No2WebpackPlugin;
複製代碼
咱們作了這麼幾件事:
Compiler
的compilation
鉤子函數中,獲取到Compilation
對象Compilation
對象調用它的chunkAsset
鉤子chunkAsset
鉤子是一個SyncHook
類型的鉤子,因此只能用tap
去調用若是和咱們猜想的同樣,每一個Compilation
對象都對應着一個輸出資源的話,那麼當咱們執行npm run build
以後,控制檯確定會打印出兩個chunk
以及兩個filename
。
一個是index.html
,一個是main.bundle.js
。
OK👌,來瞅瞅。
如今看看你的控制檯是否是打印出了一大長串呢?呆呆這裏簡寫一下輸出結果:
'compile'
'compilation'
'compilation'
Chunk {
id: 'HtmlWebpackPlugin_0',
...
}
'__child-HtmlWebpackPlugin_0'
Chunk {
id: 'main',
...
}
'main.bundle.js'
複製代碼
能夠看到,確實是有兩個Chunk
對象,還有兩個文件名稱。
只不過index.html
不是按照咱們預期的輸出爲"index.html"
,而是輸出爲了__child-HtmlWebpackPlugin_0
,這點呆呆猜想是html-webpack-plugin
插件自己作了一些處理吧。
若是你們把這兩個對象打印在控制檯上的話會發現有一大長串,呆呆這邊找到了一份比較全面的對象屬性的清單,你們能夠看一下:
(圖片與總結來源:編寫一個本身的webpack插件plugin)
Compiler 對象包含了 Webpack 環境全部的的配置信息,包含 options
,hook
,loaders
,plugins
這些信息,這個對象在 Webpack
啓動時候被實例化,它是全局惟一的,能夠簡單地把它理解爲 Webpack
實例;Compiler
中包含的東西以下所示:
Compilation 對象包含了當前的模塊資源、編譯生成資源、變化的文件等。當 Webpack
以開發模式運行時,每當檢測到一個文件變化,一次新的 Compilation
將被建立。Compilation
對象也提供了不少事件回調供插件作擴展。經過 Compilation
也能讀取到 Compiler
對象。
好了,看到這裏我相信你已經掌握了一個webpack
插件的基本開發方式了。這個東西咋說呢,只有本身去多試試,多玩玩上手才能快,下面呆呆也會爲你們演示一些稍微複雜一些的插件的開發案例。能夠跟着一塊兒來玩玩呀。
唔...看了網上挺多這個fileList.md
案例的,要不咱也給整一個?
它的功能點其實很簡單:
webpack
打包以後,自動產生一個打包文件清單,實際上就是一個markdown
文件,上面記錄了打包以後的文件夾dist
裏全部的文件的一些信息。你們在接收到這個需求的時候,能夠先想一想要如何去實現:
markdown
文件並塞到dist
裏markdown
文件內的內容是長什麼樣的針對第一點,我認爲咱們能夠傳遞一個最終生成的文件名進去,例如這樣調用:
module.exports = {
new FileListPlugin({
filename: 'fileList.md'
})
}
複製代碼
第二點,由於是在打包完成以前,因此咱們能夠去compiler 鉤子來查查有沒有什麼能夠用的。
咦~這個叫作emit
的好像挺符合的:
AsyncSeriesHook
output
目錄以前。compilation
第三點的話,難道要弄個node
的fs
?再建立個文件之類的?唔...不用搞的那麼複雜,等會讓咱們看個簡單點的方式。
第四點,咱們就簡單點,例如寫入這樣的內容就能夠了:
# 一共有2個文件
- main.bundle.js
- index.html
複製代碼
因爲功能也並不算很複雜,呆呆這裏就直接上代碼了,而後再來一步一步解析。
仍是基於剛剛的案例,讓咱們繼續在plugins
文件夾下建立一個新的插件:
plugins/File-list-plugin.js:
function FileListPlugin (options) {
this.options = options || {};
this.filename = this.options.filename || 'fileList.md'
}
FileListPlugin.prototype.apply = function (compiler) {
// 1.
compiler.hooks.emit.tapAsync('FileListPlugin', (compilation, cb) => {
// 2.
const fileListName = this.filename;
// 3.
let len = Object.keys(compilation.assets).length;
// 4.
let content = `# 一共有${len}個文件\n\n`;
// 5.
for (let filename in compilation.assets) {
content += `- ${filename}\n`
}
// 6.
compilation.assets[fileListName] = {
// 7.
source: function () {
return content;
},
// 8.
size: function () {
return content.length;
}
}
// 9.
cb();
})
}
module.exports = FileListPlugin;
複製代碼
代碼分析:
compiler.hooks.emit.tapAsync()
來觸發生成資源到output
目錄以前的鉤子,且回調函數會有兩個參數,一個是compilation
,一個是cb
回調函數markdown
文件的名稱compilation.assets
獲取到全部待生成的文件,這裏是獲取它的長度markdown
文件的內容,也就是先定義一個一級標題,\n
表示的是換行符markdown
文件內dist
文件夾裏添加一個新的資源,資源的名稱就是fileListName
變量webpack
展現tapAsync
異步調用,因此必須執行一個回調函數cb
,不然打包後就只會建立一個空的dist
文件夾。好滴,大功告成,讓咱們趕忙來試試這個新插件吧,修改webpack.config.js
的配置:
webpack.config.js:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
// const No1WebpackPlugin = require('./plugins/No1-webpack-plugin');
// const No2WebpackPlugin = require('./plugins/No2-webpack-plugin');
const FileListPlugin = require('./plugins/File-list-plugin');
module.exports = {
entry: './src/index.js',
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new HtmlWebpackPlugin({
title: 'custom-plugin'
}),
new CleanWebpackPlugin(),
// new No1WebpackPlugin({ msg: 'good boy!' })
// new No2WebpackPlugin({ msg: 'bad boy!' })
new FileListPlugin()
]
}
複製代碼
來執行一下npm run build
看看吧:
能夠看到,上面👆的案例咱們是使用tapAsync
來調用鉤子函數,這個tapPromise
好像尚未玩過,唔...咱們看看它是怎樣用的。
如今讓咱們來改下需求,剛剛咱們好像看不太出來是異步執行的。如今咱們改成1s
後才輸出資源。
重寫一下剛剛的插件:
plugins/File-list-plugin.js:
function FileListPlugin (options) {
this.options = options || {};
this.filename = this.options.filename || 'fileList.md'
}
FileListPlugin.prototype.apply = function (compiler) {
// 第二種 Promise
compiler.hooks.emit.tapPromise('FileListPlugin', compilation => {
return new Promise(resolve => {
setTimeout(() => {
resolve()
}, 1000)
}).then(() => {
const fileListName = this.filename;
let len = Object.keys(compilation.assets).length;
let content = `# 一共有${len}個文件\n\n`;
for (let filename in compilation.assets) {
content += `- ${filename}\n`;
}
compilation.assets[fileListName] = {
source: function () {
return content;
},
size: function () {
return content.length;
}
}
})
})
}
module.exports = FileListPlugin;
複製代碼
能夠看到它與第一種tapAsync
寫法的區別了:
compilation
,不須要再調用一下cb()
Promise
,這個Promise
在1s
後才resolve()
。你們能夠本身寫寫看看效果,應該是和咱們預期的同樣的。
另外,tapPromise
還容許咱們使用async/await
的方式,好比這樣:
function FileListPlugin (options) {
this.options = options || {};
this.filename = this.options.filename || 'fileList.md'
}
FileListPlugin.prototype.apply = function (compiler) {
// 第三種 await/async
compiler.hooks.emit.tapPromise('FileListPlugin', async (compilation) => {
await new Promise(resolve => {
setTimeout(() => {
resolve()
}, 1000)
})
const fileListName = this.filename;
let len = Object.keys(compilation.assets).length;
let content = `# 一共有${len}個文件\n\n`;
for (let filename in compilation.assets) {
content += `- ${filename}\n`;
}
compilation.assets[fileListName] = {
source: function () {
return content;
},
size: function () {
return content.length;
}
}
})
}
module.exports = FileListPlugin;
複製代碼
嘻嘻😁,貌似真的也不難。
話很少說,讓咱們接着來看一個監聽的案例。需求以下:
watch
模式的時候,監聽每一次資源的改動"本次監聽中止了喲~"
那麼首先爲了知足第一個條件,咱們得設計一條watch
的指令,以保證使用npm run watch
命令以後,會看到編譯過程,可是不會退出命令行,而是實時監控文件。這也很簡單,加一條腳本命令就能夠了。
呆呆在霖呆呆向你發起了多人學習webpack-構建方式篇(2)中也有說的很詳細了。
package.json:
{
"script": "webpack --watch --mode development"
}
複製代碼
而後想想咱們的插件該如何設計,這時候就要知道咱們須要調用哪一個鉤子函數了。
去官網上看一看,這個watchRun
就很符合呀:
AsyncSeriesHook
compiler
針對第三點,監聽結束以後,watchClose
就能夠了:
SyncHook
好的👌,讓咱們開幹吧。在此項目的plugins
文件夾下再新建一個叫作Watch-plugin
的插件。
先搭一下插件的架子吧:
plugins/Watch-plugin.js:
function WatcherPlugin (options) {
this.options = options || {};
}
WatcherPlugin.prototype.apply = function (compiler) {
compiler.hooks.watchRun.tapAsync('WatcherPlugin', (compiler, cb) => {
console.log('我但是時刻監聽着的 🚀🚀🚀')
console.log(compiler)
cb()
})
compiler.hooks.watchClose.tap('WatcherPlugin', () => {
console.log('本次監聽中止了喲~👋👋👋')
})
}
module.exports = WatcherPlugin;
複製代碼
(額,這個火箭🚀呆呆是用Mac自帶的輸入法打出來的,其它輸入法應該也有吧)
經過上面幾個案例的講解,這段代碼你們應該都沒有什麼疑問了吧。
那麼如今的問題就是如何知道哪些文件改變了。其實咱們在研究一個新東西的時候,若是沒啥思路,不如就在已有的條件上先找一下,好比這裏咱們就只知道一個compiler
,那麼咱們就能夠查找一下它裏面的屬性,看看有什麼是咱們能用的嗎。
也就是上面的這張圖:
能夠看到,有一個叫作watchFileSystem
的屬性應該就是咱們想要的監聽文件的屬性了,打印出來看看?
好滴👌,那就先讓我啓動這個插件吧,也就是改一下webpack.config.js
那裏的配置,因爲上面幾個案例都已經演示過了,呆呆這裏就再也不累贅,直接跳過講解這一步了。
直接讓咱們來npm run watch
一下吧,控制檯已經輸出了它,但是因爲咱們是須要監聽文件的改變,因此雖然控制檯輸出了watchFileSystem
,可是這一次是初始化時打印的,也就是說咱們須要改動一下本地的一個資源而後保存再來看看效果。
例如,我隨便改動一下src/index.js
中的內容而後保存。這時候就觸發了監聽事件了,讓咱們來看一下打印的結果:
能夠看到watchFileSystem
中確實有一個watch
屬性,並且裏面有一個fileWatchers
的列表,還有一個mtimes
對象。這兩個屬性引發了個人注意。貌似mtimes
對象就是咱們想要的了。
它是一個鍵值對,鍵名爲改動的文件的路徑,值爲時間。
那麼咱們就能夠直接來獲取它了:
function WatcherPlugin (options) {
this.options = options || {};
}
WatcherPlugin.prototype.apply = function (compiler) {
compiler.hooks.watchRun.tapAsync('WatcherPlugin', (compiler, cb) => {
console.log('我但是時刻監聽着的 🚀🚀🚀')
let mtimes = compiler.watchFileSystem.watcher.mtimes;
let mtimesKeys = Object.keys(mtimes);
if (mtimesKeys.length > 0) {
console.log(`本次一共改動了${mtimesKeys.length}個文件,目錄爲:`)
console.log(mtimesKeys)
console.log('------------分割線-------------')
}
cb()
})
compiler.hooks.watchClose.tap('WatcherPlugin', () => {
console.log('本次監聽中止了喲~👋👋👋')
})
}
module.exports = WatcherPlugin;
複製代碼
好滴,接着:
npm run watch
src/index.js
,隨便加個註釋src/index.js
文件,打印結果以下:好滴👌,這樣就實現了一個簡單的文件監聽功能。不過使用mtimes
只能獲取到簡單的文件的路徑和修改時間。若是要獲取更加詳細的信息可使用compiler.watchFileSystem.watcher.fileWatchers
,可是我試了一下這裏面的數組是會把node_modules
裏的改變也算上的,例如這樣:
因此若是針對於這道題的話,咱們能夠寫一個正則小小的判斷一下,去除node_modules
文件夾裏的改變,代碼以下:
function WatcherPlugin (options) {
this.options = options || {};
}
WatcherPlugin.prototype.apply = function (compiler) {
compiler.hooks.watchRun.tapAsync('WatcherPlugin', (compiler, cb) => {
console.log('我但是時刻監聽着的 🚀🚀🚀')
// let mtimes = compiler.watchFileSystem.watcher.mtimes;
// let mtimesKeys = Object.keys(mtimes);
// if (mtimesKeys.length > 0) {
// console.log(`本次一共改動了${mtimesKeys.length}個文件,目錄爲:`)
// console.log(mtimesKeys)
// console.log('------------分割線-------------')
// }
const fileWatchers = compiler.watchFileSystem.watcher.fileWatchers;
console.log(fileWatchers)
let paths = fileWatchers.map(watcher => watcher.path).filter(path => !/(node_modules)/.test(path))
if (paths.length > 0) {
console.log(`本次一共改動了${paths.length}個文件,目錄爲:`)
console.log(paths)
console.log('------------分割線-------------')
}
cb()
})
compiler.hooks.watchClose.tap('WatcherPlugin', () => {
console.log('本次監聽中止了喲~👋👋👋')
})
}
module.exports = WatcherPlugin;
複製代碼
另外呆呆在讀 《深刻淺出Webpack》的時候,裏面也有提到:
默認狀況下 Webpack 只會監視入口和其依賴的模塊是否發生變化,在有些狀況下項目可能須要引入新的文件,例如引入一個 HTML 文件。 因爲 JavaScript 文件不會去導入 HTML 文件,Webpack 就不會監聽 HTML 文件的變化,編輯 HTML 文件時就不會從新觸發新的 Compilation。 爲了監聽 HTML 文件的變化,咱們須要把 HTML 文件加入到依賴列表中,爲此可使用以下代碼:
compiler.plugin('after-compile', (compilation, callback) => {
// 把 HTML 文件添加到文件依賴列表,好讓 Webpack 去監聽 HTML 模塊文件,在 HTML 模版文件發生變化時從新啓動一次編譯
compilation.fileDependencies.push(filePath);
callback();
})
複製代碼
感興趣的小夥伴能夠本身去實驗一下,呆呆這裏就不作演示了。
再來看個案例,這個插件是用來檢測咱們有沒有使用html-webpack-plugin
插件的。
還記得咱們前面說的Compiler
對象中,包含了 Webpack 環境全部的的配置信息,包含 options
,hook
,loaders
,plugins
這些信息。
那麼這樣我就能夠經過plugins
來判斷是否使用了html-webpack-plugin
了。
因爲功能不復雜,呆呆這就直接上代碼了:
function DecideHtmlPlugin () {}
DecideHtmlPlugin.prototype.apply = function (compiler) {
compiler.hooks.afterPlugins.tap('DecideHtmlPlugin', compiler => {
const plugins = compiler.options.plugins;
const hasHtmlPlugin = plugins.some(plugin => {
return plugin.__proto__.constructor.name === 'HtmlWebpackPlugin'
})
if (hasHtmlPlugin) {
console.log('使用了html-webpack-plugin')
}
})
}
module.exports = DecideHtmlPlugin
複製代碼
有須要注意的點⚠️:
afterPlugins
:設置完初始插件以後,執行插件。plugins
拿到的會是一個插件列表,包括咱們的自定義插件DecideHtmlPlugin
也會在裏面some()
是Array.prototype
上的方法,用於判斷某個數組是否有符合條件的項,只要有一項知足就返回true
,不然返回false
配置一下webpack.config.js
,來看看效果是能夠的:
還記得上面👆的項目咱們用到的那個clean-webpack-plugin
,如今咱們本身來實現一個簡易版的clean-webpack-plugin
吧,名稱就叫Clean-plugin
。
同樣的,首先仍是明確一下咱們的需求:
咱們須要設計這麼一個插件,在每次從新編譯以後,都會自動清理掉上一次殘餘的dist
文件夾中的內容,不過須要知足如下需求:
options
中有一個屬性爲exclude
,爲一個數組,用來定義不須要清除的文件列表hash
命名)例如我第一次打包以後,生成的dist
目錄結構是這樣的:
/dist
|- main.f89e7ffee29ee9dbf0de.js
|- main.f97284d8479b13c49723.css
複製代碼
而後我修改了一下js
文件並從新編譯,新的目錄結構應該是這樣的:
/dist
|- main.e0c6be8f72d73a68f73a.js
|- main.f97284d8479b13c49723.css
複製代碼
能夠看到,若是咱們是用chunkhash
給輸出文件命名的話,只改變js
文件,則js
文件的文件名會發生變化,而不會影響css
文件。
若是對三種hash
命名還不清楚的小夥伴,能夠花上十分種看下個人這篇文章:霖呆呆的webpack之路-三種hash的區別,裏面對三種hash
的使用場景以及區別都說的很清楚。
此時,咱們就須要將舊的js
文件給替換成新的,也就是隻刪除main.f89e7ffee29ee9dbf0de.js
文件。
而若是咱們在配置插件的時候加了exclude
屬性的話,則不須要把這個屬性中的文件給刪除。例如若是我是這樣配置的話:
module.exports = {
new CleanPlugin({
exclude: [
"main.f89e7ffee29ee9dbf0de.js"
]
})
}
複製代碼
那麼這時候就算你修改了js
文件,結果雖然會生成新的js
文件,可是也不會把舊的給刪除,而是共存:
/dist
|- main.f89e7ffee29ee9dbf0de.js
|- main.e0c6be8f72d73a68f73a.js
|- main.f97284d8479b13c49723.css
複製代碼
因此針對於上面這個需求,咱們先給本身幾個靈魂拷問:
dist
文件夾中的全部文件options.exclude
中的文件名稱,併合併爲一個無重複項的數組(在這個過程當中咱們確定會碰到不少本身不知道的知識點,請不要慌,你們都是有這麼一個不會到會的過程)
問題一
在哪一個鉤子函數中執行,我以爲能夠在"done"
中,由於咱們其中的一個目的就是既能拿到舊的文件夾內容,又能拿到新的。而在這個階段,表示已經編譯完成了,因此是能夠拿到最新的資源了。
問題二
獲取舊的dist
文件夾內的內容。還記得咱們的dist
文件夾是怎麼來的嗎?它是在咱們webpack.config.js
這個文件中配置的output
項:
module.exports = {
output: {
path: path.resolve(__dirname, 'dist')
}
}
複製代碼
因此很輕鬆的咱們能夠經過compiler.options.output.path
就拿到這個舊的輸出路徑了,而後咱們須要去讀取這個路徑文件夾下的全部文件,也就是遍歷dist
文件夾。
這邊咱們須要用到一個叫recursive-readdir-sync
的東西,稍後咱們須要安裝它,它的做用就是以遞歸方式同步讀取目錄路徑的內容。(github地址爲:github.com/battlejj/re…)
問題三
獲取新生成的全部文件,也就是全部的資源。這點得看"done"
回調函數中的參數stats
了。若是你把這個參數打印出來看的話會發現它包括了webpack
中的不少配置,包括options
包括assets
等等。而這裏咱們就是須要獲取打包完以後的全部最新資源也就是assets
屬性。
你覺得直接stats.assets
獲取就完了嗎?若是你試圖這樣去作的話,就會報錯了。在webpack
中它鼓勵你用stats.toJson().assets
的方式來獲取。這點呆呆也不是很清楚緣由,你們能夠看一下這裏:
而後至於options.exclude
中的文件名稱,這個在插件的構造函數中定義一個options
屬性就能夠拿到了。
合併沒有重複項咱們可使用lodash.union
方法,lodash它是一個高性能的 JavaScript 實用工具庫,裏面提供了許多的方法來使咱們更方便的操做數組、對象、字符串等。而這裏的union
方法就是能把多個數組合併成一個無重複項的數組,例如🌰:
_.union([2], [1, 2]);
// => [2, 1]
複製代碼
至於爲何要把這兩個數組組合起來呢?那也是爲了保證exclude
中定義的文件在後面比較的過程當中不會被刪除。
問題四
將新舊文件列表作對比,得出最終須要刪除的文件列表。
唔...其實最難的點應該就是在這裏了。由於這裏並非簡單的文件名稱字符串匹配,它須要涉及到路徑問題。
例如,咱們前面說到能夠經過compiler.options.output.path
拿到文件的輸出路徑,也就是dist
的絕對路徑,咱們命名爲outputPath
,它多是長這樣的:
/Users/lindaidai/codes/webpack/webpack-example/webpack-custom-plugin/dist
複製代碼
然後咱們會用一個叫recursive-readdir-sync
的東西去處理這個絕對路徑,獲取裏面的全部文件:
recursiveReadSync(outputPath)
複製代碼
這裏獲得的會是各個文件:
[
file /Users/lindaidai/codes/webpack/webpack-example/webpack-custom-plugin/dist/main.f89e7ffee29ee9dbf0de.js,
file /Users/lindaidai/codes/webpack/webpack-example/webpack-custom-plugin/dist/css/main.124248e814cc2eeb1fd4.css
]
複製代碼
以上獲得的列表就是舊的dist
文件夾中的全部文件列表。
然後,咱們須要獲得新生成的文件的列表,也就是stats.toJson().assets.map(file => file.name)
和exclude
合併後的那個文件列表,咱們稱爲newAssets
。可是這裏須要注意的就是newAssets
中的是各個新生成的文件的名稱,也就是這樣:
[
"main.e0c6be8f72d73a68f73a.js",
"main.124248e814cc2eeb1fd4.css"
]
複製代碼
因此咱們須要作一些額外的路徑轉換的處理,再來進行比較。
而若是在路徑前綴相同的狀況下,咱們只須要把recursiveReadSync(outputPath)
處理以後的結果作一層過濾,排除掉newAssets
裏的內容,那麼留下來的就是須要刪除的文件,也就是unmatchFiles
這個數組。
有點繞?讓咱們來寫下僞代碼:
const unmatchFiles = recursiveReadSync(outputPath).filter(file => {
// 這裏與 newAssets 作對比
// 過濾掉存在 newAssets 中的文件
})
// unmatchFiles 就是爲咱們須要清理的全部文件
複製代碼
在這個匹配的過程當中,咱們會須要用到一個minimatch
的工具庫,它很適合用來作這種文件路徑的匹配。
github地址能夠看這裏:github.com/isaacs/mini…
問題五
在上一步中咱們會獲得須要刪除的文件列表,這時候只須要調用一下fs
模塊中的unlinkSync
方法就能夠刪除了。
例如:
// 刪除未匹配文件
unmatchFiles.forEach(fs.unlinkSync);
複製代碼
好滴,分析了這麼多,是時候動手來寫一寫了,仍是基於以前的那個案例。讓咱們先來安裝一下上面提到的一些模塊或者工具:
cnpm i --save-dev recursive-readdir-sync minimatch lodash.union
複製代碼
唔。而後爲了能看到以後修改文件有沒有刪除掉舊的文件這個效果,咱們能夠來寫一些css
的樣式,而後用MiniCssExtractPlugin
這個插件去提取出css
代碼,這樣打包以後就能夠放到一個單獨的css
文件中了。
關於這個插件,不清楚的小夥伴你就理解它爲下面這個場景:
個人src
下有一個index.js
和一個style.css
,若是在index.js
中引用了style.css
的話:
import './style.css';
複製代碼
最終的css
代碼是會被打包進js
文件中的,webpack
並不會那麼智能的把它拆成一個單獨的css
文件。
因此這時候就能夠用MiniCssExtractPlugin
這個插件來單獨的提早css
。(不過這個插件的主要做用仍是爲了提取公共的css
代碼哈,在這裏咱們只是爲了將css
提取出來)
更多有關MiniCssExtractPlugin
的功能能夠看個人這篇介紹:霖呆呆的webpack之路-優化篇
好滴,首先讓咱們來安裝它,順便安裝一下另兩個loader
:
cnpm i --save-dev style-loader css-loader mini-css-extract-plugin
複製代碼
而後在src
目錄下新建一個style.css
文件,並寫點樣式:
src/style.css:
.color_red {
color: red;
}
.color_blue {
color: blue;
}
複製代碼
接着快速來配置一下webpack.config.js
:
(這裏面有用到一個CleanPlugin
的插件,它是咱們接下來要建立的文件)
webpack.config.js:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanPlugin = require('./plugins/Clean-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
entry: [
'./src/index.js',
'./src/style.css'
],
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new HtmlWebpackPlugin({
title: 'custom-plugin'
}),
new CleanPlugin(),
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash].css'
})
],
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
}
]
}
}
複製代碼
這裏有一點前面也提到了,就是關於output.filename
的命名和MiniCssExtractPlugin
中生成css
文件的命名,咱們採用contenthash
的方式,這樣的話,若是咱們只改變了js
文件的話,那麼從新打包以後,就只有js
文件的hash
會被從新生成,而css
不會。這也是爲了以後看到效果。
最後,在plugins
文件夾下建立咱們的Clean-plugin.js
吧:
plugins/Clean-plugin.js:
const recursiveReadSync = require("recursive-readdir-sync");
const minimatch = require("minimatch");
const path = require("path");
const fs = require("fs");
const union = require("lodash.union");
function CleanPlugin (options) {
this.options = options;
}
// 匹配文件
function getUnmatchFiles(fromPath, exclude = []) {
const unmatchFiles = recursiveReadSync(fromPath).filter(file =>
exclude.every(
excluded => {
return !minimatch(path.relative(fromPath, file), path.join(excluded), {
dot: true
})
}
)
);
return unmatchFiles;
}
CleanPlugin.prototype.apply = function (compiler) {
const outputPath = compiler.options.output.path;
compiler.hooks.done.tap('CleanPlugin', stats => {
if (compiler.outputFileSystem.constructor.name !== "NodeOutputFileSystem") {
return;
}
const assets = stats.toJson().assets.map(asset => asset.name);
// 多數組合並而且去重
const newAssets = union(this.options.exclude, assets);
// 獲取未匹配文件
const unmatchFiles = getUnmatchFiles(outputPath, newAssets);
// 刪除未匹配文件
unmatchFiles.forEach(fs.unlinkSync);
})
}
module.exports = CleanPlugin;
複製代碼
比較難的技術難點在「代碼分析」中都已經說明了,這裏主要說下:
path.relative()
:
path.relative()
方法根據當前工做目錄返回 from
到 to
的相對路徑。 若是 from
和 to
各自解析到相同的路徑(分別調用 path.resolve()
以後),則返回零長度的字符串。
path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb');
// 返回: '../../impl/bbb'
複製代碼
先來看看咱們如今的目錄結構:
首先執行一遍npm run build
,生成以下內容:
而後修改一下src/index.js
中的內容,例如添加一行代碼,以後再從新執行npm run build
:
能夠看到,只有改變的index.js
被從新刪除替換了,而css
文件沒有。
再來驗證一下options.exclude
,在webpack.config.js
中添加一個插件的參數,就用上一次生成的js
的名稱吧:
module.exports = {
plugins: [
new CleanPlugin({
exclude: [
"main.e0c6be8f72d73a68f73a.js"
]
}),
]
}
複製代碼
再去修改一下index.js
的內容,例如加兩個註釋,而後執行npm run build
,會發現此次舊的js
文件並不會被刪除,而是會在原來的基礎上添加一個新的js
文件。這也證實了咱們的exclude
屬性是可用的:
知識無價,支持原創。
參考文章:
你盼世界,我盼望你無bug。這篇文章就介紹到這裏。
可算是寫完了,但願這6個小小的插件案例可以幫助你對webpack
的執行機制有一個更深刻的瞭解,呆呆也會和你一塊兒,一塊兒加油⛽️。
(本章節教材案例GitHub地址: LinDaiDai/webpack-example/tree/webpack-custom-plugin ⚠️:請仔細查看README說明)
喜歡霖呆呆的小夥還但願能夠關注霖呆呆的公衆號 LinDaiDai
或者掃一掃下面的二維碼👇👇👇.
我會不定時的更新一些前端方面的知識內容以及本身的原創文章🎉
你的鼓勵就是我持續創做的主要動力 😊.
相關推薦:
《【建議星星】要就來45道Promise面試題一次爽到底(1.1w字用心整理)》
《【建議👍】再來40道this面試題酸爽繼續(1.2w字用手整理)》
《【何不三連】比繼承家業還要簡單的JS繼承題-封裝篇(牛刀小試)》
《【何不三連】作完這48道題完全弄懂JS繼承(1.7w字含辛整理-返璞歸真)》