webpack loader和plugin编写

本文最后更新于:2023年3月19日 晚上

本文转自:https://www.jianshu.com/p/0fc6bb85ef5bhttps://juejin.im/post/6844903689442820110

webpack 中 loader 和 plugin 的区别

一、webpack 的常见配置

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
38
const webpack = require("webpack");
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
// 入口文件
entry: {
app: path.join(__dirname, "../src/js/index.js")
},
// 输出文件
output: {
filename: "[name].bundle.js",
path: path.resolve(__dirname, "dist"),
publicPath: "/"
},
// loader配置
module: {
rules: [
{
test: /\.scss/,
use: [
"style-loader",
"css-loader"
]
}
......
]
},
// plugins配置
plugins: [
// 重新创建html文件
new HtmlWebpackPlugin({
title: "首页",
filename: "index.html",
template: path.resolve(__dirname, "../src/index.html")
})
......
]
}

二、webpack 的打包原理

  1. 识别入口文件
  2. 通过逐层识别模块依赖(Commonjs、amd 或者 es6 的 import,webpack 都会对其进行分析,来获取代码的依赖)
  3. webpack 做的就是分析代码,转换代码,编译代码,输出代码
  4. 最终形成打包后的代码

三、什么是 loader

loader 是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中

  1. 处理一个文件可以使用多个 loader,loader 的执行顺序和配置中的顺序是相反的,即最后一个 loader 最先执行,第一个 loader 最后执行
  2. 第一个执行的 loader 接收源文件内容作为参数,其它 loader 接收前一个执行的 loader 的返回值作为参数,最后执行的 loader 会返回此模块的 JavaScript 源码

四、什么是 plugin

在 webpack 运行的生命周期中会广播出许多事件,plugin 可以监听这些事件,在合适的时机通过 webpack 提供的 API 改变输出结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MyPlugin{
constructor(options){
console.log("MyPlugin constructor:", options);
}
apply(compiler){
compiler.plugin("compilation", compilation => {
console.log("MyPlugin");
});
}
}
module.exports = MyPlugin;


webpack.config.js配置:
module.exports = {
...
plugins: [
new MyPlugin({param: "my plugin"})
]
}

使用该 plugin 后,执行的顺序:

  1. webpack 启动后,在读取配置的过程中会执行 new MyPlugin(options)初始化一个 MyPlugin 获取其实例
  2. 在初始化 compiler 对象后,就会通过 compiler.plugin(事件名称,回调函数)监听到 webpack 广播出来的事件
  3. 并且可以通过 compiler 对象去操作 webpack

五、loader 和 plugin 的区别

对于 loader,它是一个转换器,将 A 文件进行编译形成 B 文件,这里操作的是文件,比如将 A.scss 转换为 A.css,单纯的文件转换过程
plugin 是一个扩展器,它丰富了 webpack 本身,针对是 loader 结束后,webpack 打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听 webpack 打包过程中的某些节点,执行广泛的任务

webpack loader 和 plugin 编写

1 基础回顾

首先我们先回顾一下 webpack 常见配置,因为后面会用到,所以简单介绍一下。

1.1 webpack 常见配置

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
38
39
40
41
42
43
44
45
46
47
48
49
50
// 入口文件
entry: {
app: './src/js/index.js',
},
// 输出文件
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
publicPath: '/' //确保文件资源能够在 http://localhost:3000 下正确访问
},
// 开发者工具 source-map
devtool: 'inline-source-map',
// 创建开发者服务器
devServer: {
contentBase: './dist',
hot: true // 热更新
},
plugins: [
// 删除dist目录
new CleanWebpackPlugin(['dist']),
// 重新穿件html文件
new HtmlWebpackPlugin({
title: 'Output Management'
}),
// 以便更容易查看要修补(patch)的依赖
new webpack.NamedModulesPlugin(),
// 热更新模块
new webpack.HotModuleReplacementPlugin()
],
// 环境
mode: "development",
// loader配置
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.(png|svg|jpg|gif)$/,
use: [
'file-loader'
]
}
]
}
复制代码

这里面我们重点关注 module 和 plugins 属性,因为今天的重点是编写 loader 和 plugin,需要配置这两个属性。

1.2 打包原理

  • 识别入口文件
  • 通过逐层识别模块依赖。(Commonjs、amd 或者 es6 的 import,webpack 都会对其进行分析。来获取代码的依赖)
  • webpack 做的就是分析代码。转换代码,编译代码,输出代码
  • 最终形成打包后的代码

这些都是 webpack 的一些基础知识,对于理解 webpack 的工作机制很有帮助。

2 loader

OK 今天第一个主角登场

2.1 什么是 loader?

loader 是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中

  • 处理一个文件可以使用多个 loader,loader 的执行顺序是和本身的顺序是相反的,即最后一个 loader 最先执行,第一个 loader 最后执行。
  • 第一个执行的 loader 接收源文件内容作为参数,其他 loader 接收前一个执行的 loader 的返回值作为参数。最后执行的 loader 会返回此模块的 JavaScript 源码

2.2 手写一个 loader

需求:

  1. 处理.txt 文件
  2. 对字符串做反转操作
  3. 首字母大写

    例如:abcdefg 转换后为 Gfedcba

OK,我们开始
1)首先创建两个 loader(这里以本地 loader 为例)

为什么要创建两个 laoder?理由后面会介绍


reverse-loader.js

1
2
3
4
5
6
7
8
9
module.exports = function (src) {
if (src) {
console.log('--- reverse-loader input:', src)
src = src.split('').reverse().join('')
console.log('--- reverse-loader output:', src)
}
return src;
}
复制代码

uppercase-loader.js

1
2
3
4
5
6
7
8
9
10
11
module.exports = function (src) {
if (src) {
console.log('--- uppercase-loader input:', src)
src = src.charAt(0).toUpperCase() + src.slice(1)
console.log('--- uppercase-loader output:', src)
}
// 这里为什么要这么写?因为直接返回转换后的字符串会报语法错误,
// 这么写import后转换成可以使用的字符串
return `module.exports = '${src}'`
}
复制代码

看,loader 结构是不是很简单,接收一个参数,并且 return 一个内容就 ok 了。
然后创建一个 txt 文件

2)mytest.txt

1
2
abcdefg
复制代码

3)现在开始配置 webpack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module.exports = {
entry: {
index: './src/js/index.js'
},
plugins: [...],
optimization: {...},
output: {...},
module: {
rules: [
...,
{
test: /\.txt$/,
use: [
'./loader/uppercase-loader.js',
'./loader/reverse-loader.js'
]
}
]
}
}
复制代码

这样就配置完成了
4)我们在入口文件中导入这个脚本

为什么这里需要导入呢,我们不是配置了 webapck 处理所有的.txt 文件么?

因为 webpack 会做过滤,如果不引用该文件的话,webpack 是不会对该文件进行打包处理的,那么你的 loader 也不会执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import _ from 'lodash';
import txt from '../txt/mytest.txt'
import '../css/style.css'
function component() {
var element = document.createElement('div');
var button = document.createElement('button');
var br = document.createElement('br');
button.innerHTML = 'Click me and look at the console!';
element.innerHTML = _.join('【' + txt + '】');
element.className = 'hello'
element.appendChild(br);
element.appendChild(button);
// Note that because a network request is involved, some indication
// of loading would need to be shown in a production-level site/app.
button.onclick = e => import(/* webpackChunkName: "print" */ './print').then(module => {
var print = module.default;
print();
});
return element;
}
document.body.appendChild(component());
复制代码

package.json 配置

1
2
3
4
5
6
7
8
9
10
11
{
...,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --config webpack.prod.js",
"start": "webpack-dev-server --open --config webpack.dev.js",
"server": "node server.js"
},
...
}
复制代码

然后执行命令

1
2
npm run build
复制代码


这样我们的 loader 就写完了。

现在回答为什么要写两个 loader?

看到执行的顺序没,我们的配置的是这样的

1
2
3
4
5
use: [
'./loader/uppercase-loader.js',
'./loader/reverse-loader.js'
]
复制代码

正如前文所说,处理一个文件可以使用多个 loader,loader 的执行顺序是和本身的顺序是相反的
我们也可以自己写 loader 解析自定义模板,像 vue-loader 是非常复杂的,它内部会写大量的对.vue 文件的解析,然后会生成对应的 html、js 和 css。
我们这里只是讲述了一个最基础的用法,如果有更多的需要,可以查看 《loader 官方文档》

3 plugin

3.1 什么是 plugin?

在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。

plugin 和 loader 的区别是什么?

对于 loader,它就是一个转换器,将 A 文件进行编译形成 B 文件,这里操作的是文件,比如将 A.scss 或 A.less 转变为 B.css,单纯的文件转换过程
plugin 是一个扩展器,它丰富了 wepack 本身,针对是 loader 结束后,webpack 打包的整个过程,它并不直接操作文件,而是基于事件机制工作,会监听 webpack 打包过程中的某些节点,执行广泛的任务。

3.2 一个最简的插件

/plugins/MyPlugin.js(本地插件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyPlugin {
// 构造方法
constructor (options) {
console.log('MyPlugin constructor:', options)
}
// 应用函数
apply (compiler) {
// 绑定钩子事件
compiler.plugin('compilation', compilation => {
console.log('MyPlugin')
))
}
}
module.exports = MyPlugin
复制代码

webpack 配置

1
2
3
4
5
6
7
8
9
10
11
12
const MyPlugin = require('./plugins/MyPlugin')
module.exports = {
entry: {
index: './src/js/index.js'
},
plugins: [
...,
new MyPlugin({param: 'xxx'})
],
...
};
复制代码

这就是一个最简单的插件(虽然我们什么都没干)

  • webpack 启动后,在读取配置的过程中会先执行 new MyPlugin(options) 初始化一个 MyPlugin 获得其实例。

  • 在初始化 compiler 对象后,再调用 myPlugin.apply(compiler) 给插件实例传入 compiler 对象。

  • 插件实例在获取到 compiler 对象后,就可以通过 compiler.plugin(事件名称, 回调函数) 监听到 Webpack 广播出来的事件。

  • 并且可以通过 compiler 对象去操作 webpack。

    看到这里可能会问 compiler 是啥,compilation 又是啥?

  • Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;

  • Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。

Compiler 和 Compilation 的区别在于:

Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。

3.3 事件流

  • webpack 通过  Tapable  来组织这条复杂的生产线。
  • webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。
  • webpack 的事件流机制应用了观察者模式,和 Node.js 中的 EventEmitter 非常相似。

绑定事件

1
2
3
4
compiler.plugin('event-name', params => {
...
});
复制代码

触发事件

1
2
compiler.apply('event-name',params)
复制代码

3.4 需要注意的点

  • 只要能拿到 Compiler 或 Compilation 对象,就能广播出新的事件,所以在新开发的插件中也能广播出事件,给其它插件监听使用。
  • 传给每个插件的 Compiler 和 Compilation 对象都是同一个引用。也就是说在一个插件中修改了 Compiler 或 Compilation 对象上的属性,会影响到后面的插件。
  • 有些事件是异步的,这些异步的事件会附带两个参数,第二个参数为回调函数,在插件处理完任务时需要调用回调函数通知 webpack,才会进入下一处理流程。例如:
1
2
3
4
5
6
7
8
compiler.plugin('emit',function(compilation, callback) {
...

// 处理完毕后执行 callback 以通知 Webpack
// 如果不执行 callback,运行流程将会一直卡在这不往下执行
callback();
});
复制代码

关于 complier 和 compilation,webpack 定义了大量的钩子事件。开发者可以根据自己的需要在任何地方进行自定义处理。
《compiler 钩子文档》
《compilation 钩子文档》

3.5 手写一个 plugin

场景:
小程序 mpvue 项目,通过 webpack 编译,生成子包(我们作为分包引入到主程序中),然后考入主包当中。生成子包后,里面的公共静态资源 wxss 引用地址需要加入分包的前缀:/subPages/enjoy_given。
在未编写插件前,生成的资源是这样的,这个路径如果作为分包引入主包,是没法正常访问资源的。

所以需求来了:
修改 dist/static/css/pages 目录下,所有页面的样式文件(wxss 文件)引入公共资源的路径。
因为所有页面的样式都会引用通用样式 vender.wxss

1
2
那么就需要把@import "/static/css/vendor.wxss"; 改为:@import "/subPages/enjoy_given/static/css/vendor.wxss";
复制代码

OK 开始!
1)创建插件文件 CssPathTransfor.js

CssPathTransfor.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
class CssPathTransfor {
apply (compiler) {
compiler.plugin('emit', (compilation, callback) => {
console.log('--CssPathTransfor emit')
// 遍历所有资源文件
for (var filePathName in compilation.assets) {
// 查看对应的文件是否符合指定目录下的文件
if (/static\/css\/pages/i.test(filePathName)) {
// 引入路径正则
const reg = /\/static\/css\/vendor\.wxss/i
// 需要替换的最终字符串
const finalStr = '/subPages/enjoy_given/static/css/vendor.wxss'
// 获取文件内容
let content = compilation.assets[filePathName].source() || ''

content = content.replace(reg, finalStr)
// 重写指定输出模块内容
compilation.assets[filePathName] = {
source () {
return content;
},
size () {
return content.length;
}
}
}
}
callback()
})
}
}
module.exports = CssPathTransfor
复制代码

看着挺多,实际就是遍历 compilation.assets 模块。对符合要求的文件进行正则替换。
2)修改 webpack 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
var baseWebpackConfig = require('./webpack.base.conf')
var CssPathTransfor = require('../plugins/CssPathTransfor.js')
var webpackConfig = merge(baseWebpackConfig, {
module: {...},
devtool: config.build.productionSourceMap ? '#source-map' : false,
output: {...},
plugins: [
...,
// 配置插件
new CssPathTransfor(),
]
})
复制代码

插件编写完成后,执行编译命令

搞定~
如果有更多的需求可以参考《如何写一个插件》


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!