從零實現一個 Webpack Plugin
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 AssetSizeChunksChunk Names lib.js956 bytes0[emitted]lib [0] ./index.js 26 bytes {0} [built] 複製程式碼
驚奇的發現 applying,說明外掛已經成功執行。
傳遞引數
在應用一個 plugin 時,有時需要根據傳遞 Options 來告訴 plugin 具體應該做什麼。當new DemoPlugin()
時候會觸發class DemoPlugin
的constructor
所以
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 寫入檔案
可以通過compilation
的assets
物件來編寫新的檔案,或是修改已經建立的檔案。為了更好地寫入檔案,我們引入一個 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 AssetSizeChunksChunk Names lib.js956 bytes0[emitted]lib demo4 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.warnings
和compilation.errors
。
compilation.warnings.push("warning"); compilation.errors.push("error"); 複製程式碼