这是我参与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的描述。