[TOC]
* * * * *
### **一、插件(plugins) 的認識**
官方解釋:插件是 `webpack` 的支柱功能。`webpack` 自身也是構建于,你在 `webpack` 配置中用到的相同的插件系統之上!插件目的在于解決 `loader` 無法實現的其他事。[詳情](https://webpack.docschina.org/concepts/plugins/#%E5%89%96%E6%9E%90)
說白了就是我們在使用webpack 編譯的時候如果想改編譯后生成的文件的名字或文件的中的內容這時我們將會用到一些插件,在webpack編譯過程中(鉤子中)去做一些手腳(處理)
webpack 的插件有很多,有內置插件當然還有社區提供的插件,當然你自己也可以開發一個插件。社區的插件我們需要使用npm先進行安裝,然后在引入,內置的插件在 `webpack.optimize` 對象上,我們可以直接使用。[常見的內置插件](https://webpack.docschina.org/plugins)
### **二、插件(plugins) 的使用**
我們使用wepback 的插件官方提供了兩種方法
1. 在 webpack.config.js 中配置使用
在`webpack.config.js `中我們可以在plugins 字段數組中鏡進行配置,這個方法也是官網比較推薦的方法,如:
```JavaScript
const HtmlWebpackPlugin = require('html-webpack-plugin'); //通過 npm 安裝
const webpack = require('webpack'); //訪問內置的插件
const path = require('path');
module.exports = {
entry: './path/to/my/entry/file.js',
output: {
filename: 'test.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
}
]
},
plugins: [ // 配置插件
new HtmlWebpackPlugin({template: './src/index.html'}),
new webpack.optimize.CommonsChunkPlugin()
]
};
```
2. 通過Node API 調用
```JavaScript
const webpack = require('webpack'); //訪問 webpack 運行時(runtime)
const HtmlWebpackPlugin = require('html-webpack-plugin'); //通過 npm 安裝
let compiler = webpack(configuration);
compiler.apply(new HtmlWebpackPlugin());
```
### **三、插件(plugins) 的編寫**
[webpack官網](https://webpack.docschina.org/contribute/writing-a-plugin/)已經很詳細的介紹了編寫插件的放法。 webpack提供了兩個在編寫插件時的很重的接口(資源) `compiler ` 和 `compilation`,了解了這兩個資源,我們基本就可以上手了。官網對這個兩個插件的解釋:
> compiler 對象代表了完整的 webpack 環境配置。這個對象在啟動 webpack 時被一次性建立,并配置好所有可操作的設置,包括 options,loader 和 plugin。當在 webpack 環境中應用一個插件時,插件將收到此 compiler 對象的引用。可以使用 compiler 來訪問 webpack 的主環境。
> compilation 對象代表了一次資源版本構建。當運行 webpack 開發環境中間件時,每當檢測到一個文件變化,就會創建一個新的 compilation,從而生成一組新的編譯資源。一個 compilation 對象表現了當前的模塊資源、編譯生成資源、變化的文件、以及被跟蹤依賴的狀態信息。compilation 對象也提供了很多關鍵時機的回調,以供插件做自定義處理時選擇使用。
通過上面解釋我們可以大概理解為,`compiler` 包含著wepback 的一些配置,當我們需要一些配置時,可以操作`complier`, 事實上`compiler` 上包含了`webpack` 啟時的一些鉤子函數,例如:
- `entryOption `在 webpack 選項中的 entry 配置項 處理過之后執行
-` beforeRun` 在 `compiler.run()` 執行之前,添加一個鉤子
- `watchRun` 監聽模式下,一個新的編譯(compilation)觸發之后,執行一個插件,但是是在實際編譯開始之前。
- `compile` 一個新的編譯(compilation)創建之后,鉤入`(hook into) compiler`
- `emit` 生成資源到 `output `目錄之前。
- `done` 編譯(compilation)完成后執行
更多鉤子函數:[compiler鉤子](https://webpack.docschina.org/api/compiler-hooks/)
這些鉤子函數在` compiler.hooks` 對象上,并且按官網所說插件是由「具有 `apply` 方法的` prototype` 對象」所實例化出來的。這個 `apply` 方法在安裝插件時,會被 `webpack compiler` 調用一次。`apply` 方法可以接收一個 `webpack compiler` 對象的引用,從而可以在回調函數中訪問到 `compiler `對象。一個簡單的插件結構如下:使用方法如下:
```JavaScript
class HelloWorldPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
compiler.hooks.done.tap('HelloWorldPlugin', () => {
console.log('Hello World!');
console.log(this.options);
});
}
}
module.exports = HelloWorldPlugin;
// apply 方法在wepback構建時會自動被webpack調用,并傳入complier 對象,
//當構建完成時調用done 鉤子函數,這里tab 表示以同步方式調用,
//調用的鉤子函數第一個參數是注釋說明,第二個回調函數,當然回調函數中也會注入一些參數,下面講
```
然后在webpack 配置plugin數組中添加一個實例
```JavaScript
// webpack.config.js
var HelloWorldPlugin = require('hello-world');
module.exports = {
// ... 這里是其他配置 ...
plugins: [
new HelloWorldPlugin({setting: true})
]
};
```
上面是有的鉤子函數時同步的,`webpack` 也提供了兩種異步的方式:`tapAsync` 和 `tapPromise`。
```JavaScript
class HelloWorldPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
// tapAsync 方式 具有回調函數 callback
compiler.hooks.emit.tapAsync('HelloWorldPlugin', (compilation,callback) => {
setTimeout(()=>{
console.log("異步鉤子")
callback() // 執行回調函數
},2000)
});
// tapPromise 方式
compiler.hooks.emit.tapPromise('HelloWorldPlugin', (compilation) => {
return new Promise(resolve=> setTimeout(()=>{
resolve()
},2000)).then(() => {
console.log('異步處理');
});
});
}
}
module.exports = HelloWorldPlugin;
```
我們設置wepack 執行時的鉤子我們還需要在`wepack`構建資源時做一些處理,`wepack`構建資源模塊的一些信息和鉤子在 `compilation `對象上,而`compilation`對象是在`complier`對象的上的鉤子函數執行時傳入的第一個參數。如:
```JavaScript
class HelloCompilationPlugin {
apply(compiler) {
// 置回調來訪問 compilation 對象:
compiler.hooks.compilation.tap('HelloCompilationPlugin', (compilation) => {
// 現在,設置回調來訪問 compilation 中的步驟:
compilation.hooks.optimize.tap('HelloCompilationPlugin', () => {
console.log('Hello compilation!');
});
});
}
}
module.exports = HelloCompilationPlugin;
```
compilation 的hooks 對象上也是有一些鉤子函數:
- buildModule 在模塊構建開始之前觸發。
- rebuildModule 在重新構建一個模塊之前觸發。
- optimize 優化階段開始時觸發。
更多的鉤子見:[compilation鉤子API](https://webpack.docschina.org/api/compilation-hooks/)
`compilation` 除了`hooks` 對象 還包含一些對象,這里只寫出了幾個主要的:
- compilation.modules:編譯后的(內置輸入的)模塊數組。每個模塊管理控制來源代碼庫(source library)中的原始文件(raw file)的構建。
module.fileDependencies:模塊中引入的源文件路徑構成的數組。這包括源 JavaScript 文件本身(例如:index.js)以及它所需的所有依賴資源文件(樣式表、圖像等)。審查依賴,可以用于查看一個模塊有哪些從屬的源文件。
- compilation.chunks:編譯后的(構建輸出的)chunk 數組。每個 chunk 所管理控制的最終渲染資源的組合。
chunk.modules:chunk 中引入的模塊構成的數組。通過擴展(extension)可以審查每個模塊的依賴,來查看哪些原始源文件被注入到 chunk 中。
chunk.files:chunk 生成的輸出文件名構成的數組。你可以從 compilation.assets 表中訪問這些資源來源。
- compilation.assets 包含所有模塊的對象,我們可以通過他來獲取某個文件信息和內容,也可也可以修改獲取的文件,并且也可以向該對象中添加文件(或文件夾)
這里有個官方例子:
```JavaScript
function MyPlugin() {}
MyPlugin.prototype.apply = function(compiler) {
compiler.plugin('emit', function(compilation, callback) {
// 檢索每個(構建輸出的)chunk:
compilation.chunks.forEach(function(chunk) {
// 檢索 chunk 中(內置輸入的)的每個模塊:
chunk.modules.forEach(function(module) {
// 檢索模塊中包含的每個源文件路徑:
module.fileDependencies.forEach(function(filepath) {
// 含有每個模塊引入的路徑
});
});
// 檢索由 chunk 生成的每個資源(asset)文件名:
chunk.files.forEach(function(filename) {
// 獲取某個模塊(圖片,文件,音頻視頻)
var source = compilation.assets[filename].source();
// 將文件寫入名為index.html 的新文件
compilation.assets['index.html'] = {
source() { // 向文件中寫入內容
return source;
},
size() {
return source.length;
}
};
});
});
callback();
});
};
module.exports = MyPlugin;
```
### **四、練習**
現在我們在打包時將js的開頭注入一段注釋,注釋作者和時間。
先將我們的目錄建立起來:
```JavaScript
|-----lib
|---- index.js // 我們要編寫插件的文件
|-----src
|---- index.js // 入口文件
|-----.babelrc. // babel配置文件
|-----webpack.config.js // webpack配置文件
```
初始化項目,生成`package.json` 文件
```JavaScript
npm init
```
安裝`webpack`、`webpack-cli`,在`webpack4`中編譯和CLI命令已經分開了。
```JavaScript
npm install -D webpack webpack-cli
```
安裝`babel`轉碼ES6
```JavaScript
npm install -D babel-core babel-loader babel-preset-env
```
我們在` .babelrc. `文件中定義es6 轉碼規則
```JavaScript
{
"presets":[
"env"
]
}
```
另外我們還需要在`webpack.config.js` 中對`webpack `進行編譯配置
```JavaScript
module.exports = {
mode: 'development',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
}
]
}
};
```
然后我們在 package.json 中添加打包命令
```JavaScript
"scripts": {
"build": "npx webpack",
},
```
這次我們執行 `npm run build` 會生成一個 `dist `目錄 并且dist目錄中有一個`main.js` 文件,`main.js` 文件開頭并沒有我們想要添加的注釋。接下來我們來在`lib `目錄下的`index.js` 中編寫我們的插件來添加我們想要的注釋
```JavaScript
// lib/indx.js 文件
class TestPlugin {
constructor(options) {
this.options = Object.assign({ // 配置項默認webxiaoma
name: 'webxiaoma'
},options);
}
apply(compiler) {
//生成資源到 output 目錄之前 的鉤子中操作
compiler.hooks.emit.tap('HelloWorldPlugin', (compilation) => {
compilation.chunks.forEach(chunk => {
chunk.files.forEach(filename=>{
let filesSource = compilation.assets[filename].source(); // 獲取文件內容
let source = `//作者:${this.options.name} \n//時間:${new Date()} \n${filesSource}`;
compilation.assets[filename] = { // 修改文件內容
source() {
return source;
},
size() {
return source.length;
}
};
});
});
console.log('修改完成');
});
}
}
module.exports = TestPlugin;
```
寫好我們的簡單插件后我們在` webpack.config.js `文件中引入
```JavaScript
const TestPlugin = require('./lib/index.js');
module.exports = {
mode: 'development',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
}
]
},
plugins: [
new TestPlugin({
name: '小明'
})
]
};
```
之后我們在經常打包操作`npm run buld ` 在生成的main.js中的頭部會生成作者為小明,和時間的注釋。
```JavaScript
//作者:小明
//時間:Tue Aug 14 2018 10:32:16 GMT+0800 (中國標準時間)
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
... 省略部分
```
接下來我們想將生成的`main.js` 放在`dist `文件夾里的js文件夾里,目錄結構如下:
```JavaScript
|---- dist
|---- js
|---- main.js
```
編寫的插件如下:
```JavaScript
class TestPlugin {
constructor(options) {
this.options = Object.assign({
name: 'webxiaoma'
},options);
}
apply(compiler) {
compiler.hooks.emit.tap('HelloWorldPlugin', (compilation) => {
compilation.chunks.forEach(chunk => {
chunk.files.forEach(filename=>{
let filesSource = compilation.assets[filename].source();
let source = `//作者:${this.options.name} \n//時間:${new Date()} \n${filesSource}`;
// 判斷是否是js文件,如果是拷貝一份放到js文件夾下,并清除原來js
if ((/\.js$/).test(filename)) {
// 這里將js文件放入到js文件夾下后,刪除以前assets存在的該文件。
delete compilation.assets[filename];
compilation.assets[`js/${filename}`] = {
source() {
return source;
},
size() {
return source.length;
}
};
} else {
compilation.assets[filename] = {
source() {
return source;
},
size() {
return source.length;
}
};
}
});
console.log('構建完成');
});
}
}
module.exports = TestPlugin;
```
接下來我們可以在`dist `目錄下生成一個` index.html `文件 來引入`main.js`,修改上邊代碼
```JavaScript
class TestPlugin {
constructor(options) {
this.options = Object.assign({
name: 'webxiaoma'
},options);
}
apply(compiler) {
compiler.hooks.emit.tap('HelloWorldPlugin', (compilation) => {
compilation.chunks.forEach(chunk => {
chunk.files.forEach(filename=>{
let filesSource = compilation.assets[filename].source();
let source = `//作者:${this.options.name} \n//時間:${new Date()} \n${filesSource}`;
if ((/\.js$/).test(filename)) { // 判斷是否是js文件,如果是放到js文件夾下
// 這里將js文件放入到js文件夾下后,刪除以前assets存在的該文件。
delete compilation.assets[filename];
compilation.assets[`js/${filename}`] = {
source() {
return source;
},
size() {
return source.length;
}
};
} else {
compilation.assets[filename] = {
source() {
return source;
},
size() {
return source.length;
}
};
}
});
});
// 創建index.html
let contentHTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>測試</title>
</head>
<body>
<div id="root"></div>
<script src="./dist/js/main.js"></script>
</body>
</html>`;
compilation.assets['index.html'] = {
source() {
return contentHTML;
},
size() {
return contentHTML.length;
}
};
});
}
}
module.exports = TestPlugin;
```
我們執行 `npm run buld `, 最后我們的目錄變成了下面這樣:
```JavaScript
|---- dist
|---- js
---- main.js
---- index.html
```
如果我們的目錄是下面這樣的:根目錄多了一個index.html
```JavaScript
|-----lib
|---- index.js // 我們要編寫插件的文件
|-----src
|---- index.js // 入口文件
|-----.babelrc. // babel配置文件
|-----webpack.config.js // webpack配置文件
|-----index.html
```
我們想將`index.html` 打包到`dist `中,并且在`index.html `內容的基礎之上引入我們打包后的` main.js`文件。
我們可以這樣實現,我們在編寫的插件中暴露出一個接口來獲取`index.html` 文件的路徑,然后在打包輸出時,讀取`index.html` 文件并進行修改,最后輸出。我們可以在原來基礎上進行修改
```JavaScript
// lib/index.js文件
const fs = require('fs');
class TestPlugin {
constructor(options) {
this.options = Object.assign({
name: 'webxiaoma',
htmlPath: '' // 獲取index.html路徑
},options);
}
apply(compiler) {
compiler.hooks.emit.tap('HelloWorldPlugin', (compilation) => {
compilation.chunks.forEach(chunk => {
chunk.files.forEach(filename=>{
let filesSource = compilation.assets[filename].source();
let source = `//作者:${this.options.name} \n//時間:${new Date()} \n${filesSource}`;
if ((/\.js$/).test(filename)) { // 判斷是否是js文件,如果是放到js文件夾下
// 這里將js文件放入到js文件夾下后,刪除以前assets存在的該文件。
delete compilation.assets[filename];
compilation.assets[`js/${filename}`] = {
source() {
return source;
},
size() {
return source.length;
}
};
} else {
compilation.assets[filename] = {
source() {
return source;
},
size() {
return source.length;
}
};
}
});
});
// 生成index,html 文件
let contentHTML;
if (this.options.htmlPath) { // 如果文件路徑存在,讀取文件
let htmlFile = fs.readFileSync(this.options.htmlPath).toString();
// 添加打包后的js文件
let content = '\n<script src="./dist/js/main.js"></script>\n</body>';
contentHTML = htmlFile.replace('</body>',content);
} else {
contentHTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>測試</title>
</head>
<body>
<div id="root"></div>
<script src="./dist/js/main.js"></script>
</body>
</html>`;
}
compilation.assets['index.html'] = {
source() {
return contentHTML;
},
size() {
return contentHTML.length;
}
};
console.log('編譯完成!');
});
}
}
module.exports = TestPlugin;
```
然后我們修改一些 webpack.config.js 文件
```JavaScript
const path = require('path');
const TestPlugin = require('./lib/index.js');
module.exports = {
mode: 'development',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader'
}
]
},
plugins: [
new TestPlugin({
name: '小明',
htmlPath: path.join(__dirname,'index.html') // 添加index.html 路徑
})
]
};
```
之后我們打包就可以實現我們想要的效果了。最后貼出源碼 [github 倉庫](https://github.com/webxiaoma/webpack-demos/tree/master/webpack4/plugins)