實現一個 webpack loader 和 webpack plugin

loader

官網上的定義:css

loader 是一個轉換器,用於對源代碼進行轉換。html

例如 babel-loader 能夠將 ES6 代碼轉換爲 ES5 代碼;sass-loadersass 代碼轉換爲 css 代碼。webpack

通常 loader 的配置代碼以下:git

module: {
        rules: [
            {
                test: /\.js$/,
                use: [
                    // loader 的執行順序從下到上
                    {
                        loader: path.resolve('./src/loader2.js'),
                    },
                    {
                        loader: path.resolve('./src/loader1.js'),
                    },
                ]
            }
        ]
    },
複製代碼

rules 數組包含了一個個匹配規則和具體的 loader 文件。github

上述代碼中的 test: /\.js$/ 就是匹配規則,表示對 js 文件使用下面的兩個 loader。web

而 loader 的處理順序是自下向上的,即先用 loader1 處理源碼,而後將處理後的代碼再傳給 loader2。npm

loader2 處理後的代碼就是最終的打包代碼。api

loader 的實現

loader 實際上是一個函數,它的參數是匹配文件的源碼,返回結果是處理後的源碼。下面是一個最簡單的 loader,它什麼都沒作:數組

module.exports = function (source) {
    return source
}
複製代碼

這麼簡單的 loader 沒有挑戰性,咱們能夠寫一個稍微複雜一點的 loader,它的做用是將 var 關鍵詞替換爲 constsass

module.exports = function (source) {
    return source.replace(/var/g, 'const')
}
複製代碼

寫完以後,咱們來測試一下,測試文件爲:

function test() {
    var a = 1;
    var b = 2;
    var c = 3;
    console.log(a, b, c);
}

test()
複製代碼

wepback.config.js 配置文件爲:

const path = require('path')

module.exports = {
    mode: 'development',
    entry: {
        main: './src/index.js'
    },
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                use: [
                    {
                        loader: path.resolve('./src/loader1.js'),
                    },
                ]
            }
        ]
    },
}
複製代碼

運行 npm run build,獲得打包文件 bundle.js,咱們來看一看打包後的代碼:

eval("function test() {\r\n const a = 1;\r\n const b = 2;\r\n const c = 3;\r\n console.log(a, b, c);\r\n}\r\n\r\ntest()\n\n//# sourceURL=webpack:///./src/index.js?");
複製代碼

能夠看到,代碼中的 var 已經變成了 const

異步 loader

剛纔實現的 loader 是一個同步 loader,在處理完源碼後用 return 返回。

下面咱們來實現一個異步 loader:

module.exports = function (source) {
    const callback = this.async()

    // 因爲有 3 秒延遲,因此打包時須要 3+ 秒的時間
    setTimeout(() => {
        callback(null, `${source.replace(/;/g, '')}`)
    }, 3000)
}
複製代碼

異步 loader 須要調用 webpack 的 async() 生成一個 callback,它的第一個參數是 error,這裏可設爲 null,第二個參數就是處理後的源碼。當你異步處理完源碼後,調用 callback 便可。

下面來試一下異步 loader 到底有沒生效,這裏設置了一個 3 秒延遲。咱們來對比一下打包時間:

在這裏插入圖片描述 在這裏插入圖片描述 上圖是調用同步 loader 的打包時間,爲 141 ms;下圖是調用異步 loader 的打包時間,爲 3105 ms,說明異步 loader 生效了。

若是想看完整 demo 源碼,請點擊個人 github

plugin

webpack 在整個編譯週期中會觸發不少不一樣的事件,plugin 能夠監聽這些事件,而且能夠調用 webpack 的 API 對輸出資源進行處理。

這是它和 loader 的不一樣之處,loader 通常只能對源文件代碼進行轉換,而 plugin 能夠作得更多。plugin 在整個編譯週期中均可以被調用,只要監聽事件。

對於 webpack 編譯,有兩個重要的對象須要瞭解一下:

Compiler 和 Compilation

在插件開發中最重要的兩個資源就是 compiler 和 compilation 對象。理解它們的角色是擴展 webpack 引擎重要的第一步。

compiler 對象表明了完整的 webpack 環境配置。這個對象在啓動 webpack 時被一次性創建,並配置好全部可操做的設置,包括 options,loader 和 plugin。當在 webpack 環境中應用一個插件時,插件將收到此 compiler 對象的引用。能夠使用它來訪問 webpack 的主環境。

compilation 對象表明了一次資源版本構建。當運行 webpack 開發環境中間件時,每當檢測到一個文件變化,就會建立一個新的 compilation,從而生成一組新的編譯資源。一個 compilation 對象表現了當前的模塊資源、編譯生成資源、變化的文件、以及被跟蹤依賴的狀態信息。compilation 對象也提供了不少關鍵時機的回調,以供插件作自定義處理時選擇使用。

這兩個組件是任何 webpack 插件不可或缺的部分(特別是 compilation),所以,開發者在閱讀源碼,並熟悉它們以後,會感到獲益匪淺。

plugin 的實現

咱們看一下官網的定義,webpack 插件由如下部分組成:

  1. 一個 JavaScript 命名函數。
  2. 在插件函數的 prototype 上定義一個 apply 方法。
  3. 指定一個綁定到 webpack 自身的事件鉤子。
  4. 處理 webpack 內部實例的特定數據。
  5. 功能完成後調用 webpack 提供的回調。

簡單的說,一個具備 apply 方法的函數就是一個插件,而且它要監聽 webpack 的某個事件。下面來看一個簡單的示例:

function Plugin(options) { }

Plugin.prototype.apply = function (compiler) {
    // 全部文件資源都被 loader 處理後觸發這個事件
    compiler.plugin('emit', function (compilation, callback) {
        // 功能完成後調用 webpack 提供的回調
        console.log('Hello World')
        callback()
    })
}

module.exports = Plugin
複製代碼

寫完插件後要怎麼調用呢?

先在 webpack 配置文件中引入插件,而後在 plugins 選項中配置:

const Plugin = require('./src/plugin')

module.exports = {
	...
    plugins: [
        new Plugin()
    ]
}
複製代碼

這就是一個簡單的插件了。

下面咱們再來寫一個複雜點的插件,它的做用是將通過 loader 處理後的打包文件 bundle.js 引入到 index.html 中:

function Plugin(options) { }

Plugin.prototype.apply = function (compiler) {
    // 全部文件資源通過不一樣的 loader 處理後觸發這個事件
    compiler.plugin('emit', function (compilation, callback) {
        // 獲取打包後的 js 文件名
        const filename = compiler.options.output.filename
        // 生成一個 index.html 並引入打包後的 js 文件
        const html = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <script src="${filename}"></script> </head> <body> </body> </html>`
        // 全部處理後的資源都放在 compilation.assets 中
        // 添加一個 index.html 文件
        compilation.assets['index.html'] = {
            source: function () {
                return html
            },
            size: function () {
                return html.length
            }
        }

        // 功能完成後調用 webpack 提供的回調
        callback()
    })
}

module.exports = Plugin
複製代碼

OK,執行一下,看看效果。

在這裏插入圖片描述 完美,和預測的結果如出一轍。

完整 demo 源碼,請看個人 github

想了解更多的事件,請看官網介紹 compiler 鉤子

參考資料

相關文章
相關標籤/搜索