前言
本篇文章爲webpack系列文章的第三篇,主要內容是對webpack的plugin進行詳細的講解,從使用,到原理,再到自己開發一個plugin,對每個過程都會進行詳細的分析介紹。如果你對webpack瞭解的還比較少,建議你先閱讀以下往期文章。
介紹
plugin是webpack的一個插件機制,它爲項目的構建提供了更加廣泛的能力,loader的作用只是實現各種資源的轉換,使得任何資源都可以模塊化的被打包。而plugin可以解決其他的更多的自動化打包工作,plugin的範圍更大,作用也更強。
- 可以自動打包生成html文件,並自動引入打包後的結果
- 打包前清除原dist文件中的內容
- 可以將我們需要的但是並沒有引入靜態資源一同打包到dist文件中
- 對打包的結果進行特殊的處理
- 壓縮打包後的內容,對打包結果可以進行更細的自定義操作
- …
plugin的作用還遠不止這些,可以看出plugin的重要性,下面我們來看幾個常用的plugin,然後去深入它的原理,最後自己寫一個簡單的plugin。
情景再現
情景1
通過往期的學習我們知道,每次打包後的內容默認會輸出到dist文件夾中,如果在原dist文件夾中存在相同文件名,則會覆蓋原文件,使得打包結果始終爲最新的內容。但是有沒有想過這樣的場景,如果原dist中存在某些文件已經被我們遺棄,也就是說後面我們不需要這些文件了,但是他們還是存在於dist中,這種情況下我們還要去提取出來哪些使我們需要的文件,這個過程顯然是很繁瑣的。
那如何解決這個問題呢,如果我們在打包輸出前清除掉dist中原來的內容,那麼打包後的內容必然都是我們需要的。我們每次手動的去清理也比較麻煩,如果有個插件可以在我們打包前自動幫我們清理就好了。
clean-webpack-plugin這個插件就可以實現這個功能,它是一個第三方npm包,我們只需要安裝就可以使用。
npm install clean-webpack-plugin
在webpack.config.js中引入,我們只需要用到它其中的CleanWebpackPlugin就行。
const {CleanWebpackPlugin} = require('clean-webpack-plugin')
使用插件是在plugins屬性中,它是一個數組,表示可以使用多個插件,我們只需要將上面的對象實例化後寫在數組中即可。
module.exports = {
mode: 'none',
entry: './src/index.js',
output: {
filename: 'bundle.js',
},
plugins:[
new CleanWebpackPlugin(),
]
}
現在我們執行打包命令
可以看到在原dist中含有很多個文件,我這裏打包後會自動的清理掉。
情景2
在我們打包完成後會生成結果文件,我們需要手動的在index.html中去引入,如果結果文件名改變了,還需要手動的去修改引入的文件名。如果有個插件可以在打包完成後自動的生成一個html文件,並自動引入script標籤就好了。其實這些功能我們可以使用html-webpack-plugin插件來實現。
npm install clean-webpack-plugin
引入
const HtmlWebpackPlugin = require('html-webpack-plugin')
配置
...
plugins:[
new HtmlWebpackPlugin(),
]
...
我們可以在實例化時傳入配置參數,這個插件提供了很多靈活的配置項。
- title:標題
- meta:meta標籤
- filename:輸出的文件名
- template:使用已有模板,有時在index.html中含有一些其他的內容,可以選用該html文件爲模板。
- minify:壓縮html文件
- favicon:favicon的路徑
- …
這裏我只列出了其中的部分配置,更多詳情可以去查閱該插件的說明文檔。
new HtmlWebpackPlugin({
title: '測試標題',
meta:{
keywords: 'webpack,plugin'
},
filename: 'webpack.html',
template: 'index.html',
minify:{ //壓縮html文件
caseSensitive: true, //是否對大小寫敏感,默認false
collapseBooleanAttributes: true, //是否簡寫boolean格式的屬性如:disabled="disabled" 簡寫爲disabled 默認false
collapseWhitespace: true, //是否去除空格,默認false
minifyCSS: true, //是否壓縮html裏的css(使用clean-css進行的壓縮) 默認值false;
minifyJS: true, //是否壓縮html裏的js(使用uglify-js進行的壓縮)
preventAttributesEscaping: true, //Prevents the escaping of the values of attributes
removeAttributeQuotes: true, //是否移除屬性的引號 默認false
removeComments: true, //是否移除註釋 默認false
removeCommentsFromCDATA: true, //從腳本和樣式刪除的註釋 默認false
removeEmptyAttributes: true, //是否刪除空屬性,默認false
removeOptionalTags: false, // 若開啓此項,生成的html中沒有 body 和 head,html也未閉合
removeRedundantAttributes: true, //刪除多餘的屬性
removeScriptTypeAttributes: true, //刪除script的類型屬性,在h5下面script的type默認值:text/javascript 默認值false
removeStyleLinkTypeAttributes: true, //刪除style的類型屬性, type="text/css" 同上
useShortDoctype: true, //使用短的文檔類型,默認false
},
favicon: './public/logo.ico'
}),
在配置中我選用了自己的index.html作爲模板,原有的index.html內容如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>wepack-plugins</title>
</head>
<body>
<div>
<span>我是原html中的內容</span>
</div>
</body>
</html>
在index.js中動態加入了一個標籤
var dom = document.createElement('h2')
dom.innerHTML = '我是外部新增的內容'
document.body.append(dom)
執行打包,可以在dist下面出現一個webpack.html文件,這是因爲我們在配置中配置了輸出文件名爲webpack.html。在配置中還對html進行了壓縮配置,所以看到的是被壓縮後的結果。
我們取消html壓縮配置,重新打包
可以看到我們的配置全部生效,包括標題、meta等信息。在瀏覽器中預覽結果完全符合我們的預期。
情景3
最後再來介紹一個插件叫做copy-webpack-plugin,看名字就知道它與複製有關。我們在項目中會用到很多靜態資源,但有些可能並沒有被直接的使用,我們希望這些資源可以一同的被打包到dist中。
新建一個public文件夾,假如說這個文件夾中的所有內容我們都希望被打包。爲了測試結果,在裏面我們新建一個static.txt文件,這個文件我們沒有在任何地方使用到。
它接收的是一個數組,因爲可能存在多個不同的目錄
const CopyWebpackPlugin = require('copy-webpack-plugin')
...
new CopyWebpackPlugin(['./public']),
執行打包
通過上面幾個案例,是不是可以看出plugin的強大能力和它的重要地位了,總有一個插件可以滿足我們的需求。有人會說了,萬一我就是沒有找到適合我的需求的插件呢?
這個世界上本是沒有輪子的,沒有輪子我們就要學會去造輪子,下面我們就來造一個自己的輪子。
要學會造輪子
在造輪子之前我們必須要知道它的原理,plugin相比loader還有一點很大的不同,loader只工作於模塊的加載環節,而plugin即可可以作用於打包過程的每一個環節,有點像vue中的生命週期,我們可以在一個合適的週期進行相應的操作。webpack的插件機制就是我們常說的鉤子機制,整個打包過程可以分爲多個環節,爲了便於插件的擴展,webpack機會在每個環節都提供了一個鉤子,我們就可以利用這些鉤子來造輪子。
webpack爲我們提供了哪些hooks呢?
- entry-option 初始化 option
- run 開始編譯
- compile 真正開始的編譯,在創建 compilation 對象之前
- compilation 生成好了 compilation 對象
- make 從 entry 開始遞歸分析依賴,準備對每個模塊進行 build
- after-compile 編譯 build 過程結束
- emit 在將內存中 assets 內容寫到磁盤文件夾之前
- after-emit 在將內存中 assets 內容寫到磁盤文件夾之後
- done 完成所有的編譯過程
- failed 編譯失敗的時候
- …
更多hooks請參考官方文檔:https://www.webpackjs.com/api/compiler-hooks/
現在我們知道了生產線,我們在生產線的不同階段做不同的事情。但是我們還是不知道怎麼造輪子啊。
webpack要求我們的插件必須是一個函數,或者是一個包含apply的對象。一般來說我們都會定義一個類型,然後在這個類型中定義apply方法,最後再通過這個類型來創建一個實例對象去使用這個插件。
const pluginName = 'myplugin'
module.exports = class myplugin {
apply(){}
}
這個apply方法接收一個叫compiler的參數對象,這個對象是webpack工作中最核心的對象,包含了此次打包構建的所有配置信息,我們就可以通過這個對象去註冊鉤子函數。
const pluginName = 'myplugin'
module.exports = class myplugin {
apply(compiler){
compiler.hooks.run.tap(pluginName, () =>{
{
console.log('開始編譯');
}
})
}
}
我們想在run階段輸出‘開始編譯’這句話,在webpack.config.js中引入並配置
const myplugin = require('./myplugin')
...
plugins:[
new myplugin()
]
...
在控制檯可以看到在開始階段輸出了內容,說明我們的plugin生效了,這只是測試,接下來我們就來實現點功能。
我們在使用node或者development模式下,打包後的js文件中,前面會有許多這樣的註釋符,看起來很不舒服。
我們可以自己來造個輪子讓webpack打包後的內容中沒有這些東西。
第一步,我們要找到合適的環節執行我們要進行的操作。通過查看API文檔,可以找到emit這個hooks很符合我們的場景,它在生成資源到output目錄之前執行,也就是在還沒有輸出打包文件時執行。
我們的思路是這樣的,在即將輸出文件的前面,獲取到要輸出的文件內容,找到以js爲後綴的文件,然後去掉裏面的註釋符,最後再重新將處理後的內容替換原來要輸出的內容。
./remove-comments-plugin
const pulginName = 'RemoveCommentsPlugin'
module.exports = class RemoveCommentsPlugin {
apply(compiler) {
compiler.hooks.emit.tap(pulginName, (compilation) => {
for (const name in compilation.assets) {
if (name.endsWith('js')) {
let contents = compilation.assets[name].source()
let noComments = contents.replace(/\/\*{2,}\/\s?/g, '')
compilation.assets[name] = {
source: () => noComments,
size: () => noComments.length
}
}
}
})
}
}
它接收一個叫 compilation 的參數對象,這個對象可以理解爲本次運行打包的上下文,它包含了所有打包過程中產生的結果。它的assets屬性中包含的就是所有打包後將要輸出的文件,source()方法可以獲取到文件內的內容。
這裏我們遍歷所有即將要輸出的文件,使用endsWith(‘js’)方法匹配到js文件(這是ES6的一個方法),然後使用正則將所有的註釋符替換爲空字符串,最後將修改後的內容覆蓋掉原來的內容。這裏要注意一點,在改變文件內容後需要重新計算文件的大小,否則size的值可能會與實際值不匹配。
const RemoveCommentsPlugin from './remove-comments-plugin'
...
plugins:[
new RemoveCommentsPlugin()
]
...
查看打包後的結果,已經不存在註釋符。
對於有些插件需要傳遞配置參數,這個也很簡單,我們只需要在構造函數中進行接收即可。
const pulginName = 'RemoveCommentsPlugin'
module.exports = class RemoveCommentsPlugin {
constructor(params){
this.config = {}
for (const key in params) {
if (this.config.hasOwnProperty(key)) {
this.config[key] = params[key]
}
}
}
apply(compiler) {
compiler.hooks.emit.tap(pulginName, (compilation, params) => {
for (const name in compilation.assets) {
if (name.endsWith('js')) {
let contents = compilation.assets[name].source()
let noComments = contents.replace(/\/\*{2,}\/\s?/g, '')
compilation.assets[name] = {
source: () => noComments,
size: () => noComments.length
}
}
}
})
}
}
每次都不知道怎麼寫結語,這都不重要,重要到的可以學到東西,學以致用就夠了。
最後覺得我的文章對你有所幫助的話,希望可以收藏、點贊、關注,你們的支持是我最大的動力
歡迎訪問我的個人網站:www.dengzhanyong.com
我的個人公衆號:【前端筱園】與CSDN同步更新,快來關注我吧,不錯過我的每一篇推送。