自定义loader
我们对loader已经很熟悉了,其实它就是对我们模块的源代码进行转化的。比如css-loader、style-loader、babel-loader等。
自定义loader的本质
- Loader本质上是一个导出为函数的JavaScript模块;
- Webpack会使用loader-runner库的runLoaders方法,将上一个loader转化出来的结果传给下一个loader处理。
写一个自定义loader
自定义loader:my-loader.js
//my-loader.js
//context:资源文件内容; map:sourcemap相关数据; meta:一些元数据
module.exports = function (content,map,meta) {
console.log(content, "这就是一个自定义loader");
return content + 123;
}
自定义loader的使用:
//wenpack.config.js
module: {
rules: [
{
test: /\.js$/i,
use: {
//默认情况下webpack会到node_modules里找loader,
//自定义loader没有在node_modules里,所以只能写路径(路径受context影响)
loader: "./MyLoaders/my-loader",
}
},
]
},
写一个自定义loader本质就这么几行代码。我们可以看下webpack打包后的代码:确实是将我们自定义loader添加的123添加到了源代码后边。
resolveLoader属性
自定义loader只能写路径名吗?
Webpack默认情况下只会到node_modules里去找对应的loader。
而我们的自定义loader在node_modules里并没有,所以我们自定义loader只能写路径才能被webpack找到。
而resolveLoader就是告诉webpack还可以到哪里去找,就可以让自定义loader写名称就能被webpack找到。
//wenpack.config.js
module: {
rules: [
{
test: /\.js$/i,
use: {
//写名称就可以
loader: "my-loader",
}
},
]
},
resolveLoader: {
//告诉webpack:如果在node_modules找不到,可以到哪里去找
modules: ["node_modules", "./myLoaders"]
},
Loader执行顺序和enforce
在最开始学习loader的时候,我们就知道了Loader的执行顺序是从后往前执行的了。但是其实webpack官方给我们之前所见到的loader都称为NormalLoader。还有另一种Loader,叫做PitchLoader。(PitchLoader的执行顺序是从前往后)
// NormalLoader
module.exports = function (content) {
return content
}
// PitchLoader
module.exports.pitch = function (content) {
return content
}
下边写了三个自定义loader(每个loader里既有NormalLoader也有PitchLoader),它的执行顺序是怎样的呢?
执行顺序:先从前往后执行PitchLoader,再从后往前执行NormalLoader。
我们还可以通过配置enforce,改变执行顺序。
Enforce:让某个loader不过怎么放,都是第一个执行的。
//webpack.config.js
module: {
rules: [
//要使用enforce,需要拆开来
{
test: /\.js$/i,
use: {
loader: "my-loader01",
}
},
{
test: /\.js$/i,
use: "my-loader02",
//enforce:让当前loader永远是第一个执行
enforce: "pre"
},
{
test: /\.js$/i,
use: "my-loader03"
},
]
},
上边配置我们看下结果:我们给my-loader2设置了enforce:’pre’。
我们可以看到:my-loader2的NormalLoader永远在第一个执行,而其PitchLoader会在最后执行。
enforce的属性:
- 默认所有的loader都是normal;
- 在行内设置的loader是inline(在前面将css加载时讲过,import ‘loader1!loader2!./test.js’);
- 也可以通过enforce设置 pre(NormalLoader永远第一个执行) 和 post(NormalLoader永远最后执行);(PitchLoader则相反)
同步Loader与异步Loader
什么是同步Loader?
默认创建的Loader就是同步Loader
Loader必须有返回结果,否则会报错。可以通过return的方式或者this.callback的方式返回结果。(如果没有返回结果下个Loader就执行不了,所以报错)
我们先看下this.callback怎么返回结果:
// 同步Loader
module.exports = function(content) {
console.log(content);
// 同步的loader, 两种方法返回数据
// return content;
//this.callback:两个参数;参数一:Err或者null、参数二:string或buffer
this.callback('myLoader报错了', content);
}
Loader默认就是同步的,那异步Loader是什么呢?异步Loader的使用场景是什么?
// 异步Loader: this.async()
module.exports = function(content) {
//webpack提供的async方法让我们的Loader变成了异步Loader
const callback = this.async();
//setTimeout是异步的,我们的callback被放到了异步里
setTimeout(() => {
callback(null, content);
}, 2000);
}
异步Loader使用场景:
比如上边代码的情况,我们需要在异步函数调用完成后,再去callback返回结果。
但是由于默认情况下都是同步Loader,runLoaders不会等待异步执行完返回的结果,也就是相当于当前同步Loader是没有返回值的,所以报错了。这时候就需要异步Loader。
Runner-loader库提供了async()方法,让我们的Loader变成异步Loader。
异步Loader的话,runLoaders就可以等待异步执行完毕返回结果。(所以上边的异步loader不会报错)
接收options参数以及参数校验
我们使用Loader时,也会根据需要传options参数。
那自定义Loader是如何接收参数的呢?
我们需要安装以通过一个webpack官方提供的一个解析库 loader-utils,它的getOptions就可以帮助我们拿到参数。
const { getOptions } = require("loader-utils");
module.exports = function(content) {
// 获取传入的参数:
const options = getOptions(this);
const callback = this.async();
setTimeout(() => {
callback(null, content);
}, 2000);
}
我们还可以安装webpack官方提供的校验库 schema-utils进行参数校验。
创建一个json文件编写校验规则:
//loader01-schema.json
{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "请输入您的名字"
},
"age": {
"type": "number",
"description": "请输入您的年龄"
}
},
// 允许添加其它的配置
"additionalProperties": true
}
在自定义Loader中使用:
const { getOptions } = require("loader-utils");
const { validate } = require('schema-utils');
const schema = require("../hy-schema/loader01-schema.json");
module.exports = function(content) {
// 获取传入的参数:
const options = getOptions(this);
//参数校验
validate(schema, options, {
name: "hy-loader02"
})
const callback = this.async();
setTimeout(() => {
callback(null, content);
}, 2000);
}
Tapable
在学习自定义plugin前,我们还需要掌握关于tapable库的内容。
之前在我们阅读源码时,webpack的两个核心compiler类与compilation类里,都使用了大量的hook。
Plugin插件的注入,离不开各种各样的hook。
而这些hook是哪里来的呢?就是通过tapable库拿到的。
什么是tapable库?
- Tapable是官方编写和维护的一个库;
- Tapable是管理着需要的Hook,这些Hook可以被应用到我们的插件中
关于hook的使用过程
- 第一步:注册hook对象;
- 第二步:注册hook中的事件;
- 第三步:触发hook,执行事件。 ```javascript const { SyncHook} = require(“tapable”);
class HYLearnTapable {
constructor() {
this.hooks = {
//1. 创建hook对象,自定义的hook名称
syncHook: new SyncHook([“name”, “age”])
}
//2. 使用tap方法,给某个hook注册事件(这里给syncHook注入了两个事件)
this.hooks.syncHook.tap(“event1”, (name, age) => {
console.log(“event1”, name, age);
return “event1”;
});
this.hooks.syncHook.tap("event2", (name, age) => {
console.log("event2", name, age);
});
//调用emit函数执行hook
emit() {
//call方法可以让某个hook开始执行,这个hook身上注入的函数也会按照顺序执行
this.hooks.syncHook.call(“why”, 18);
}
}
<a name="415fe1b4"></a>
### 关于honk的类型
![wps5.jpg](https://cdn.nlark.com/yuque/0/2022/jpeg/27275574/1653996158424-fc7c9505-92fc-47e9-a450-3962a879b244.jpeg#clientId=ud0eff1c6-fab8-4&crop=0&crop=0&crop=1&crop=1&from=ui&id=ud655b5b7&margin=%5Bobject%20Object%5D&name=wps5.jpg&originHeight=195&originWidth=643&originalType=binary&ratio=1&rotation=0&showTitle=false&size=97493&status=done&style=none&taskId=u521b9e48-02ac-495f-a0d2-c46ae8631ac&title=)<br />**同步和异步的:**
- 以sync开头的,是同步的Hook;
- 以async开头的,两个事件处理回调,不会等待上一次处理回调结束后再执行下一次回调;
**其他的类别**
- bail:当有返回值时,就不会执行后续的事件触发了;
- Loop:当返回值为true,就会反复执行该事件,当返回值为undefined或者不返回内容,就退出事件;
- Waterfall:当返回值不为undefined时,会将这次返回的结果作为下次事件的第一个参数;
- Parallel:并行,会同时执行次事件处理回调结束,才执行下一次事件处理回调;
- Series:串行,会等待上一是异步的Hook;
<a name="36dab78d"></a>
## 自定义plugin
在写一个我们自己的plugin之前,我们可以先回顾下之前阅读webpack源码时看到的webpack是如何处理plugin的。<br />webpack在创建compiler对象时,会执行createCompiler方法,在之前学习中,我们知道此时会做四件事,而其中一件事就是注册所有的plugin到compiler对象上。
```javascript
// 注册所有的plugin插件到compiler
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
//使用call或apply,将plugin插件注册到compiler上
if (typeof plugin === "function") {
// 如果plugin是一个函数, 调用call方法完成注册
// 第一个参数是this绑定值, 第二个是传入的参数
plugin.call(compiler, compiler);
} else {
// 如果plugin是一个对象, 调用apply方法完成注册
plugin.apply(compiler);
}
}
}
由于webpack 提供的plugin都是class,function比较少见,所有我们直接看它是如何处理用class写的plugin的。在11行调用了plugin的apply方法。
我们在上一节学习tapable,就是要先明白webpack是如何将plugin注册到compiler上的。
Tapable库为compiler对象提供了很多的hook,而plugin要想注册到compiler上,就肯定需要把plugins注册到某个hook上。
所以不难推测出,我们需要在plugin的apply方法上调用this.hooks.某个Hook.tap将当前plugin与想要的hook绑定。
现在我们可以写一个自定义plugin了:
class myPlugin {
constructor(options) {
this.options = options;
}
//apply
apply(compiler) {
//同步用tap,异步用tapAsync
compiler.hooks.afterEmit.tapAsync("myPlugin", async (compilation, callback) => {
console.log('在这里写plugin插件要做的事情');
callback();
});
}
}
module.exports = myPlugin;
使用自定义plugin
其实跟webpack提供的plugin使用方式一模一样。
//webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const myPlugin = require("./plugins/myPlugin");
module.exports = {
entry: "./src/main.js",
output: {
path: path.resolve(__dirname, "./build"),
filename: "bundle.js"
},
plugins: [
new HtmlWebpackPlugin(),
new myPlugin()
]
}