自定义loader

我们对loader已经很熟悉了,其实它就是对我们模块的源代码进行转化的。比如css-loader、style-loader、babel-loader等。

自定义loader的本质

  • Loader本质上是一个导出为函数的JavaScript模块;
  • Webpack会使用loader-runner库的runLoaders方法,将上一个loader转化出来的结果传给下一个loader处理。

写一个自定义loader

自定义loader:my-loader.js

  1. //my-loader.js
  2. //context:资源文件内容; map:sourcemap相关数据; meta:一些元数据
  3. module.exports = function (content,map,meta) {
  4. console.log(content, "这就是一个自定义loader");
  5. return content + 123;
  6. }

自定义loader的使用:

  1. //wenpack.config.js
  2. module: {
  3. rules: [
  4. {
  5. test: /\.js$/i,
  6. use: {
  7. //默认情况下webpack会到node_modules里找loader,
  8. //自定义loader没有在node_modules里,所以只能写路径(路径受context影响)
  9. loader: "./MyLoaders/my-loader",
  10. }
  11. },
  12. ]
  13. },

写一个自定义loader本质就这么几行代码。我们可以看下webpack打包后的代码:确实是将我们自定义loader添加的123添加到了源代码后边。
wps1.jpg

resolveLoader属性

自定义loader只能写路径名吗?
Webpack默认情况下只会到node_modules里去找对应的loader。
而我们的自定义loader在node_modules里并没有,所以我们自定义loader只能写路径才能被webpack找到。

resolveLoader就是告诉webpack还可以到哪里去找,就可以让自定义loader写名称就能被webpack找到。

  1. //wenpack.config.js
  2. module: {
  3. rules: [
  4. {
  5. test: /\.js$/i,
  6. use: {
  7. //写名称就可以
  8. loader: "my-loader",
  9. }
  10. },
  11. ]
  12. },
  13. resolveLoader: {
  14. //告诉webpack:如果在node_modules找不到,可以到哪里去找
  15. modules: ["node_modules", "./myLoaders"]
  16. },

Loader执行顺序和enforce

在最开始学习loader的时候,我们就知道了Loader的执行顺序是从后往前执行的了。但是其实webpack官方给我们之前所见到的loader都称为NormalLoader。还有另一种Loader,叫做PitchLoader。(PitchLoader的执行顺序是从前往后)

  1. // NormalLoader
  2. module.exports = function (content) {
  3. return content
  4. }
  5. // PitchLoader
  6. module.exports.pitch = function (content) {
  7. return content
  8. }

下边写了三个自定义loader(每个loader里既有NormalLoader也有PitchLoader),它的执行顺序是怎样的呢?
wps2.jpgwps3.jpg

执行顺序:先从前往后执行PitchLoader,再从后往前执行NormalLoader。

我们还可以通过配置enforce,改变执行顺序。
Enforce:让某个loader不过怎么放,都是第一个执行的。

  1. //webpack.config.js
  2. module: {
  3. rules: [
  4. //要使用enforce,需要拆开来
  5. {
  6. test: /\.js$/i,
  7. use: {
  8. loader: "my-loader01",
  9. }
  10. },
  11. {
  12. test: /\.js$/i,
  13. use: "my-loader02",
  14. //enforce:让当前loader永远是第一个执行
  15. enforce: "pre"
  16. },
  17. {
  18. test: /\.js$/i,
  19. use: "my-loader03"
  20. },
  21. ]
  22. },

上边配置我们看下结果:我们给my-loader2设置了enforce:’pre’。
我们可以看到:my-loader2的NormalLoader永远在第一个执行,而其PitchLoader会在最后执行。
wps4.jpg

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怎么返回结果:

  1. // 同步Loader
  2. module.exports = function(content) {
  3. console.log(content);
  4. // 同步的loader, 两种方法返回数据
  5. // return content;
  6. //this.callback:两个参数;参数一:Err或者null、参数二:string或buffer
  7. this.callback('myLoader报错了', content);
  8. }

Loader默认就是同步的,那异步Loader是什么呢?异步Loader的使用场景是什么?

  1. // 异步Loader: this.async()
  2. module.exports = function(content) {
  3. //webpack提供的async方法让我们的Loader变成了异步Loader
  4. const callback = this.async();
  5. //setTimeout是异步的,我们的callback被放到了异步里
  6. setTimeout(() => {
  7. callback(null, content);
  8. }, 2000);
  9. }

异步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()  
  ]  
}