超詳細webpack的plugin講解,看不懂算我輸,案例>原理>實踐

前言

本篇文章爲webpack系列文章的第三篇,主要內容是對webpack的plugin進行詳細的講解,從使用,到原理,再到自己開發一個plugin,對每個過程都會進行詳細的分析介紹。如果你對webpack瞭解的還比較少,建議你先閱讀以下往期文章。

如何使用webpack實現模塊化打包

webpack的核心機制之loader的祕密

介紹

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同步更新,快來關注我吧,不錯過我的每一篇推送。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章