從零實現一個 Webpack Plugin

Yzz發表於2019-04-29

Plugins expose the full potential of the webpack engine to third-party developers. ----------- Webpack

相比於 loaders,plugin 更加的靈活,因為它能夠接觸到 webpack 編譯器和編譯核心。這就使得 plugin 可以通過一些 hook 函式來攔截 webpack 的執行,甚至你可以執行一個子編譯器和 loader 串聯,像 MiniCssExtractPlugin 就是這麼做的。

示例程式碼:link

webpack plugin 基本結構

html-webpack-plugin 為例,它的使用如下

plugins: [
    new HtmlWebpackPlugin({
        ...
    }),
],
複製程式碼

不難看出,webpack plugin 的基本形式一個建構函式 new function(),同時為了能夠獲得 compiler,就需要 plugin 對外暴露一個介面(為 apply 函式)。所以,它的基本結構如下:

  • 一個命名的 JavaScript 函式物件;
  • 在其 prototype 上,定義一個 apply 方法。

JavaScript 的實現這種形式的方法有很多,本文采用 class 來實現,具體如下

module.exports = class DemoPlugin {
    apply() {
        console.log('applying')
    }
}
複製程式碼

配置開發環境

為了能夠執行這個 plugin,我們需要建立一個環境

mkdir webpack-demo-plugin
cd webpack-demo-plugin
npm init
複製程式碼

webpack.plugin.js

const path = require("path");

const PATHS = {
  lib: path.join(__dirname, "index.js"),
  build: path.join(__dirname, "build"),
};

module.exports = {
  entry: {
    lib: PATHS.lib,
  },
  output: {
    path: PATHS.build,
    filename: "[name].js",
  },
};
複製程式碼

index.js

console.log("hello world")
複製程式碼

同時向 package.json 中新增

"scripts": {
    "build:plugin": "webpack --config webpack.plugin.js --mode production",
    ...
  }
複製程式碼

實現 webpack demo

建立 plugins/demo-plugin.js 檔案,內容為之前的 webpack plugin demo,並將其引入到 webpack.plugin.js 內。

webpack.plugin.js

const DemoPlugin = require("./plugins/demo-plugin.js");

module.exports = {
  ...
  // 引入 plugin
  plugins: [new DemoPlugin()],
};
複製程式碼

嘗試執行下 npm run build:plugin,終端上列印出

applying
Hash: 98c8997160aa995a58a4
Version: webpack 4.12.0
Time: 93ms
Built at: 2019-04-29 14:34:31
 Asset       Size  Chunks             Chunk Names
lib.js  956 bytes       0  [emitted]  lib
[0] ./index.js 26 bytes {0} [built]
複製程式碼

驚奇的發現 applying,說明外掛已經成功執行。

傳遞引數

在應用一個 plugin 時,有時需要根據傳遞 Options 來告訴 plugin 具體應該做什麼。當 new DemoPlugin() 時候會觸發 class DemoPluginconstructor 所以

plugins/demo-plugin.js

module.exports = class DemoPlugin {
    constructor(options) {
        this.options = options
    }
    apply() {
        console.log('apply', this.options)
    }
}
複製程式碼

同時,還需要修改 webpack.plugin.js 來傳遞對應引數

module.exports = {
    ...
    plugins: [new DemoPlugin({ name: 'demo' })],
}
複製程式碼

執行 npm run build:plugin,可以發現 apply { name: 'demo' }。這裡介紹一個常用外掛 schema-utils 能夠用來校驗 Options。

理解 webpack 的 compiler 和 compilation

在之前的 webpack plugin 基本結構中介紹,apply 函式能夠用來訪問 webpack 的核心。具體的做法是,apply 函式的引數為 compiler

plugins/demo-plugin.js

module.exports = class DemoPlugin {
    constructor(options) {
        this.options = options
    }
    apply(compiler) {
        console.log(compiler)
    }
}
複製程式碼

再次執行 npm run build:plugin,會發現終端上列印出了 compiler 的全部資訊,其中 hooks 欄位佔了絕大部分。

對照著官方文件,你會發現每一個 hook 對應一個特定的階段。 例如,emit 實踐是在向輸出目錄傳送資源之前執行。這樣就可以通過監聽 hook 來實現控制編譯過程。

plugins/demo-plugin.js

module.exports = class DemoPlugin {
    constructor(options) {
        this.options = options
    }
    apply(compiler) {
        compiler.plugin('emit', (compilation, next) => {
            console.log(compilation)

            next()
        })
    }
}
複製程式碼

不要忘記呼叫 next,否則 webpack 將不會繼續打包。

執行 npm run build:plugin 會顯示出比以前更多的資訊,因為編譯物件包含webpack 遍歷的整個依賴關係圖。 你可以訪問與此相關的所有內容,包括 entries, chunks, modules, assets等。

通過 Compilation 寫入檔案

可以通過 compilationassets 物件來編寫新的檔案,或是修改已經建立的檔案。為了更好地寫入檔案,我們引入一個 npm 包

npm install webpack-sources --save-dev
複製程式碼

plugins/demo-plugin.js

const { RawSource } = require("webpack-sources");

module.exports = class DemoPlugin {
    constructor(options) {
        this.options = options
    }
    apply(compiler) {
        const { name } = this.options;

        compiler.plugin('emit', (compilation, next) => {
            compilation.assets[name] = new RawSource("demo");

            next()
        })
    }
}
複製程式碼

在終端執行 npm run build:plugin

Hash: 98c8997160aa995a58a4
Version: webpack 4.12.0
Time: 95ms
Built at: 2019-04-29 16:08:52
 Asset       Size  Chunks             Chunk Names
lib.js  956 bytes       0  [emitted]  lib
  demo    4 bytes          [emitted]
[0] ./index.js 26 bytes {0} [built]
複製程式碼

在 Asset 那裡一列內,出現了我們自定的 demo 檔案。

更多的鉤子函式,請見 the official compilation reference

管理 Warnings 和 Errors

做一個實驗,如果你在 apply 函式內插入 throw new Error("Message"),會發生什麼,終端會列印出 Unhandled rejection Error: Message。然後 webpack 中斷執行。

為了不影響 webpack 的執行,要在編譯期間向使用者發出警告或錯誤訊息,則應使用 compilation.warningscompilation.errors

compilation.warnings.push("warning");
compilation.errors.push("error");
複製程式碼

總結

當你著手開始設計外掛時,一定要花時間研究同型別的現有外掛。 逐個開發外掛,以便你一次只驗證一個外掛。 遇到問題,可以檢視 webpack 原始碼,它會增強你的 debug 直覺。

參考:

webpack.js.org/contribute/…

survivejs.com/webpack/ext…

webpack.js.org/api/plugins…

相關文章