前言

嘛,这是这个系列的第二篇,上篇随缘介绍了Webpack的基本配置和打包的基本过程和原理,这篇文章我们来介绍Webpack中两个很重要的扩展功能,loaderplugin

Loader

Loader简介

还记得这张图嘛,Webpack支持对各种文件进行打包

image-20201221211457125

但是Webpack本身只支持对js文件进行解析打包,因为AST抽象语法树的解析只能针对js

2020-01-13-09-29-08

那对其他的文件怎么处理呢,其实很简单,我们对其他文件使用loader进行解析和处理,把它转化成一个可以转化成AST的代码就可以了

image-20201221212305583

Loader的配置

新建一个src/index.css

1
2
3
4
5
6
:root, body {
height: 100%;
}
body {
background: darkseagreen;
}

修改src/index.js

1
2
3
import add from "./cal.js";
import "./index.css";
console.log(add(Math.random(),Math.random()));

运行命令

1
webpack

非常快乐的报错了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[webpack-cli] Compilation finished
asset bundle.js 6.43 KiB [emitted] (name: main)
runtime modules 931 bytes 4 modules
cacheable modules 752 bytes
modules by path ./src/*.js 260 bytes
./src/index.js 98 bytes [built] [code generated]
./src/cal.js 162 bytes [built] [code generated]
./src/index.css 77 bytes [built] [code generated] [1 error]
./src/util/index.js 415 bytes [built] [code generated]

ERROR in ./src/index.css 1:0
Module parse failed: Unexpected token (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js
.org/concepts#loaders
> :root, body {
| height: 100%;
| }
@ ./src/index.js 2:0-21

很明显,报错信息告诉我们,要用一个loader进行解析css这个文件类型,那我们可以给它加一个loader

新建src/loaders/style-loader

1
2
3
4
5
6
7
module.exports = function (sourceCode) {
var code = `var style = document.createElement("style");
style.innerHTML = \`${sourceCode}\`;
document.head.appendChild(style);
module.exports = \`${sourceCode}\``;
return code;
}

修改webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let path = require("path");
module.exports = {
entry: "./src/index.js",
mode: "development",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js"
},
devServer: {
contentBase: path.resolve(__dirname, 'dist'),
port: 9000
},
module: {
rules: [{
test: /\.css$/,
use: ["./src/loaders/style-loader"]
}]
}
}

运行命令

1
webpack

打包成功√

1
2
3
4
5
6
7
8
E:\Workspaces\Project\WebPro\WebpackTest>webpack
[webpack-cli] Compilation finished
asset bundle.js 669 bytes [emitted] [minimized] (name: main)
orphan modules 577 bytes [orphan] 2 modules
cacheable modules 961 bytes
./src/index.js + 2 modules 675 bytes [built] [code generated]
./src/index.css 286 bytes [built] [code generated]
webpack 5.10.1 compiled successfully in 250 ms

看看打包结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
({
"./src/add.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) =>
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => /* binding */ add\n/* harmony export */ });\n/* harmony import */ var _util__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./util */ \"./src/util/index.js\");\n\r\n\r\nfunction add(a, b) {\r\n return a + b;\r\n}\r\n\r\n\n\n//# sourceURL=webpack://webpacktest/./src/add.js?");
}),
"./src/index.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _add_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./add.js */ \"./src/add.js\");\n/* harmony import */ var _index_css__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./index.css */ \"./src/index.css\");\n/* harmony import */ var _index_css__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_index_css__WEBPACK_IMPORTED_MODULE_1__);\n\r\n\r\nconsole.log((0,_add_js__WEBPACK_IMPORTED_MODULE_0__.default)(1,2));\n\n//# sourceURL=webpack://webpacktest/./src/index.js?");
}),
"./src/index.css":
((module) => {
eval("var style = document.createElement(\"style\");\n style.innerHTML = `:root, body {\r\n height: 100%;\r\n}\r\nbody {\r\n background: darkseagreen;\r\n}`;\n document.head.appendChild(style);\n module.exports = `:root, body {\r\n height: 100%;\r\n}\r\nbody {\r\n background: darkseagreen;\r\n}`\n\n//# sourceURL=webpack://webpacktest/./src/index.css?");
}),
"./src/util/index.js":
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"default\": () => __WEBPACK_DEFAULT_EXPORT__\n/* harmony export */ });\nfunction randomNum(minNum, maxNum) {\r\n switch (arguments.length) {\r\n case 1:\r\n return parseInt(Math.random() * minNum + 1, 10);\r\n break;\r\n case 2:\r\n return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10);\r\n break;\r\n default:\r\n return 0;\r\n break;\r\n }\r\n}\r\nconsole.log(randomNum(1,2));\r\n/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (randomNum);\n\n//# sourceURL=webpack://webpacktest/./src/util/index.js?");
})
});

然后把对应的内容拿出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
"./src/index.css":
((module) => {
var style = document.createElement("style");
style.innerHTML =
`:root, body {
height: 100%;
}
body {
background: darkseagreen;
}`;
document.head.appendChild(style);
module.exports =
`:root, body {
height: 100%;
}
body {
background: darkseagreen;
}`
}),

看到了吗,这就是我们通过loader处理过的css代码

所以loader本质上就是一个函数,接收原文件并且转化为Webpack可以识别处理的代码

更多的配置参见:webpack-loader

练习一下

嘛,这次我们试一下通过loader处理图片文件

新建src/loaders/img-loader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 这是一个工具库
let loaderUtil = require("loader-utils")
// 使用loader.raw = true;后传入的是一个buffer
// 表示文件的二进制代码
function loader(buffer) {
// 获取loader配置
let { limit = 1000, filename = "[contenthash].[ext]" } = loaderUtil.getOptions(this);
// 导出的值
let content = "";
if (buffer.byteLength >= limit) {
content = getFilePath.call(this, buffer, filename);
}
else{
content = getBase64(buffer)
}
return `module.exports = \`${content}\``;
}

loader.raw = true; // 标记该loader要处理的是原始数据

module.exports = loader;

// 把文件转化成base64编码
function getBase64(buffer) {
return "data:image/png;base64," + buffer.toString("base64");
}
// 把文件打包到输出文件夹
function getFilePath(buffer, name) {
// 获取文件名
let filename = loaderUtil.interpolateName(this, name, {
content: buffer
});
// this是webpack调用时绑定的一个对象
// 通过this.emitFile我们可以把一个文件加入到最终生成的文件列表中
this.emitFile(filename, buffer);
return filename;
}

修改index.js

1
2
3
4
5
6
7
import add from "./add.js";
import "./index.css";
import src from "./assets/img.png";
console.log(add(1,2));
let img = document.createElement("img");
img.src = src;
document.body.appendChild(img);

修改webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let path = require("path");
module.exports = {
entry: "./src/index.js",
mode: "development",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js"
},
devServer: {
contentBase: path.resolve(__dirname, 'dist'),
port: 9000
},
module: {
rules: [{
test: /\.css$/,
use: ["./src/loaders/style-loader"]
}, {
test: /\.(png)|(jpg)|(gif)$/, use: [{
loader: "./src/loaders/img-loader.js",
options: {
limit: 3000, // 3000字节以上使用图片,3000字节以内使用base64
filename: "img-[contenthash:5].[ext]"
}
}]
}]
}
}

运行webpack命令打包

1
2
3
4
5
6
7
8
9
10
11
12
13
E:\Workspaces\Project\WebPro\WebpackTest>webpack
[webpack-cli] Compilation finished
asset img-e7819.png 189 KiB [emitted] (auxiliary name: main)
asset bundle.js 7.05 KiB [emitted] (name: main)
runtime modules 931 bytes 4 modules
cacheable modules 1.01 KiB
modules by path ./src/*.js 298 bytes
./src/index.js 202 bytes [built] [code generated]
./src/add.js 96 bytes [built] [code generated]
./src/index.css 286 bytes [built] [code generated]
./src/assets/img.png 32 bytes [built] [code generated]
./src/util/index.js 415 bytes [built] [code generated]
webpack 5.10.1 compiled successfully in 114 ms

打开index.html

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script src="./bundle.js"></script>
</body>
</html>

啊哈打包成功

image-20201221234723173

Plugin

Plugin简介

loader的功能定位是转换代码,而有的时候我们需要在打包流程里执行一些操作,这时候就要用到我们的plugin了

那么plugin是什么呢,其实plugin本质上就是一个带有apply方法的对象

1
2
3
4
5
let plugin = {
apply: function(compiler){

}
}

但是我们一般会写成构造函数的形式,在需要时通过new来创建

1
2
3
4
5
6
7
class MyPlugin {
apply(compiler) {
console.log("apply方法运行");
}
}

module.exports = MyPlugin

如果要在打包时应用我们的插件,可以在webpack.config.js里进行下面的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
let path = require("path");
let MyPlugin = require("./src/plugins/MyPlugin");
module.exports = {
entry: "./src/index.js",
mode: "development",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js"
},
devServer: {
contentBase: path.resolve(__dirname, 'dist'),
port: 9000
},
module: {
rules: [{
test: /\.css$/,
use: ["./src/loaders/style-loader"]
}, {
test: /\.(png)|(jpg)|(gif)$/, use: [{
loader: "./src/loaders/img-loader.js",
options: {
limit: 3000,
filename: "img-[contenthash:5].[ext]"
}
}]
}]
},
// 应用插件
plugins: [
new MyPlugin()
]
}

get √

1
2
3
4
5
6
7
8
9
10
11
12
13
14
E:\Workspaces\Project\WebPro\WebpackTest>webpack
apply方法运行
[webpack-cli] Compilation finished
asset img-e7819.png 189 KiB [compared for emit] (auxiliary name: main)
asset bundle.js 7.05 KiB [compared for emit] (name: main)
runtime modules 931 bytes 4 modules
cacheable modules 1.01 KiB
modules by path ./src/*.js 298 bytes
./src/index.js 202 bytes [built] [code generated]
./src/add.js 96 bytes [built] [code generated]
./src/index.css 286 bytes [built] [code generated]
./src/assets/img.png 32 bytes [built] [code generated]
./src/util/index.js 415 bytes [built] [code generated]
webpack 5.10.1 compiled successfully in 259 ms

插件的apply函数会在初始化阶段,创建好compiler对象后运行,并且把compiler对象传入,我们可以在apply函数里给compiler注册一些hook,这些hook就类似于webpack的生命周期函数,会在特定的时机被执行

注册方法如下

1
2
3
4
5
6
7
class MyPlugin{
apply(compiler){
compiler.hooks.事件名称.事件类型(name, function(compilation){
//事件处理函数
})
}
}

事件名称

即要监听的事件名,即钩子名,compiler所有的钩子

事件类型

这部分使用了一个库:Tapable Api

如果想详细了解各个事件类型可以看这篇文章:Webpack Tappable使用研究

在注册事件时,你需要传入两个参数,一个是事件的名称,我们一般直接使用插件名就好了,另一个是一个回调函数,这个回调函数会在触发事件时执行,传入一个compilation参数,这个参数代表了这次的编译打包结果,也可以在这上面挂载一些钩子

详情请看comliation

练习一下

在这里我们编写一个可以输出打包出来的资源到文件的插件

新建src/plugins/FileListPlugin.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class FileListPlugin {

constructor(filename = "fileList.txt"){
this.filename = filename;
}

apply(compiler) {
// emit事件在生成资源到output目录之前触发
compiler.hooks.emit.tap("FileListPlugin", compilation => {
let fileList = [];
// 获取要生成的资源列表
Object.keys(compilation.assets).forEach((key) => {
let content = `【${key}
大小:${compilation.assets[key].size()/1000}KB`;
fileList.push(content);
})
let str = fileList.join("\n\n");
// 把文件添加到要生成的资源列表中
compilation.assets[this.filename] = {
source() {
return str
},
size() {
return str.length;
}
}
})
}
}
module.exports = FileListPlugin;

更新webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
let path = require("path");
let MyPlugin = require("./src/plugins/MyPlugin");
let FileListPlugin = require("./src/plugins/FileListPlugin");
module.exports = {
entry: "./src/index.js",
mode: "development",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js"
},
devServer: {
contentBase: path.resolve(__dirname, 'dist'),
port: 9000
},
module: {
rules: [{
test: /\.css$/,
use: ["./src/loaders/style-loader"]
}, {
test: /\.(png)|(jpg)|(gif)$/, use: [{
loader: "./src/loaders/img-loader.js",
options: {
limit: 3000, // 3000字节以上使用图片,3000字节以内使用base64
filename: "img-[contenthash:5].[ext]"
}
}]
}]
},
plugins: [
new MyPlugin(),
new FileListPlugin()
]
}

运行命令

1
webpack

dist目录下生成了一个fileList.txt

1
2
3
4
5
【img-e7819.png】
大小:193.314KB

【bundle.js】
大小:7.222KB

这样我们的插件就运行成功了