这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战
Loader
loader 本质上是导出为函数的 JavaScript 模块。loader runner 会调用此函数,然后将上一个 loader 产生的结果或者资源文件传入进去。该函数不能是箭头函数,因为函数中的 this 作为上下文会被 webpack 填充,并且 loader runner 中包含一些实用的方法。
同一类型文件可以有多个loader,而起始loader只有一个入参:资源文件的内容。compiler 预期得到最后一个 loader 产生的处理结果。这个处理结果应该为 String 或者 Buffer(能够被转换为 string)类型,代表了模块的 JavaScript 源码。
举个例子,先编写如下demo:
// index.js
console.log('xiong ling');
// webpack.config.js
const path = require('path');
const webpack = require("webpack");
module.exports = {
mode: 'development',
entry: {
index: './src/index.js',
},
devtool: 'inline-source-map',
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist'),
clean: true,
},
resolveLoader: {
modules: ['node_modules', './src/loaders']
},
module: {
rules: [
{
test: /.js$/i,
use: ['loader'],
}
]
}
};
resolveLoader的意思是告诉webpack去哪里找loader,从左到右执行,我们项目中肯定不止我们自定义的loader,所以需要加入node_modules,在遇到.js文件结尾的时候,就用我们自定义的loader去处理,在./src/loaders下去找。因此我们还需要见一个./src/loaders/loader.js文件用来编写我们自定义loader的代码
// ./src/loaders/loader.js
module.exports = function (source) {
return source.replace('xiong ling', "hello world");
}
在这个函数里面,我们接受了一个source参数,它就是我们的源代码(String),然后我们进行替换,就这么简单,这是同步的loader。我们运行npm run build 试试,我们的内容被成功替换了。
我们知道,在webpack配置中,我们可以传入options配置,传入一些参数到我们的loader中,那我们怎么接收呢?再改一下例子,修改webpack.config.js的配置如下
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /.js$/i,
use: [
{
loader: 'loader',
options: {
name: "hello world --- options"
}
}
],
}
]
}
};
我们在这加入了options参数,怎么在loader中拿到呢。
// ./src/loaders/loader.js
module.exports = function (source) {
const options = this.getOptions();
return source.replace('xiong ling', options.name);
}
从 webpack 5 开始,this.getOptions 可以获取到 loader 上下文对象。它用来替代来自 loader-utils 中的 getOptions 方法。之前使用loader-utils这个库的。再次打包,我们的代码也会被修改成console.log('hello world --- options -- resolveLoader');
如果我们有些异步操作呢,该怎么办?还是直接return嘛?当然不是,那样会直接报错,说loader没有返回东西,大家可以尝试一下。现在我们继续修改代码
// ./src/loaders/loader.js
module.exports = function (source) {
const options = this.getOptions();
const callback = this.async()
setTimeout(() => {
const result = source.replace('xiong ling', options.name);
callback(null, result)
// return source.replace('xiong ling', options.name);
}, 1000)
}
我们不能直接在setTimeout中return,会报错,所以我们需要使用async函数,他告诉 loader-runner 这个 loader 将会异步地回调。返回 this.callback。webpack就会等待定时器执行完成,然后调用callback函数将结果返回。
至此,我们简单的loader就已经实现了,他还可以用作国际化等操作。接下来来看看webpack中另一个重要概念plugin怎么实现。
Plugin
插件也是webpack生态中的重要一部分,它提供了一种有力的方式让我们直接触及 webpack 的编译过程(compilation process)。插件能够 hook 到每一个编译(compilation)中发出的关键事件中。 在编译的每个阶段中,插件都拥有对 compiler 对象的完全访问能力, 并且在合适的时机,还可以访问当前的 compilation 对象。
Compiler模块是 webpack 的主要引擎,它通过 CLI 或者 Node API 传递的所有选项创建出一个 compilation 实例,对象包含了 Webpack 环境所有的的配置信息。Compilation模块会被Compiler用来创建新的 compilation 对象(或新的 build 对象)。compilation实例能够访问所有的模块和它们的依赖(大部分是循环依赖),这个对象包含了当前的模块资源、编译生成资源、变化的文件等。
创建插件
webpack插件应该有哪些部分组成呢?我们在使用的时候是怎么使用的呢?是new一个插件对吧new XXXPlugin(),那是不是就说明我们的插件是一个构造函数或者类呢。
- 一个 JavaScript 命名函数或 JavaScript 类。
- 在插件函数的 prototype 上定义一个
apply方法。 - 在
apply中可以绑定一个webpack的时间钩子,然后再钩子中执行我们的需求
这个 apply 方法在安装插件时,会被 webpack compiler 调用一次。apply 方法可以接收一个 webpack compiler 对象的引用,从而可以在回调函数中访问到 compiler 对象。
接下来我们可以使用一个例子来写一下我们的plugin。就以fileList.md为例吧,需求就是 在每次webpack打包之后,自动产生一个打包文件清单,实际上就是一个markdown文件,上面记录了打包之后的文件夹dist里所有的文件的一些信息。
分析步骤:
- 生成一个
markdown文件,这个文件名怎么定义,需不需要传参数进去 - 在那个钩子里执行我们的操作
- 怎么生成我们的
markdown文件 - 需要什么样的
markdown文件
所以我们的代码可以写成如下:
class FileList {
static defaultOptions = {
outputFile: 'assets.md',
};
constructor(options = {}) {
// 可以接收自定义的options,如文件名等,进行合并
this.options = { ...FileList.defaultOptions, ...options };
}
apply(compiler) {
// 在 emit 钩子里执行,他是异步钩子,所以我们需要使用tapAsync来注册,并且必须调用cb函数
compiler.hooks.emit.tapAsync('FileList', (compilation, cb) => {
const fileListName = this.options.outputFile;
// compilation.assets有我们所有的资源文件
let len = Object.keys(compilation.assets).length;
//
let content = `# 一共有${len}个文件\n\n`;
// 遍历资源文件,获取name进行拼接
for (let filename in compilation.assets) {
content += `- ${filename}\n`
}
// 在compilation.assets这资源对象中新添加一个名为fileListName的文件
compilation.assets[fileListName] = {
// 文件内容
source: function () {
return content;
},
// 文件的长度
size: function () {
return content.length;
}
}
cb()
})
}
}
module.exports = FileList;
至此我们的一个插件就写好了,大家可以测试一下。
补充一点的是,上面提到异步钩子需要使用tapAsync来注册,而同步钩子是需要tap来注册的,并且不需要调用cb函数。钩子是同步还是异步的,可以看webpack的描述。