【webpack】loader进阶

94 阅读4分钟

Loader进阶

rule.enforce

该参数为webpackloader配置时的可选配置项,其参数值有三种:pre(前置),normal(默认),post(后置)。

除此之外还有一种叫做inline(行内,也称内联)

使用方式及执行顺序

通过之前的学习,有了解到loader的执行顺序应该是从右到左,从下到上

不过在了解完这一节之后就开始不一样了。(题外话:虽然我觉得大部分项目一般都不会使用这个配置,不过也可能是我接触的项目太少了)

module: {
  rules: [
    {
      enforce: "pre",
      test: /\.js$/,
      loader: "loader1",
    },
    {
      // 写与不写都一样,一般来说normal都是不写的,因为它是默认值
      enforce: "normal",
      test: /\.js$/,
      loader: "loader2",
    },
    {
      enforce: "post",
      test: /\.js$/,
      loader: "loader3",
    },
  ],
},

如果无视以上代码中的enforce参数,那么以上loader的执行顺序应该是loader3 => loader2 => loader1

但是如果考虑到这个配置项的时候,他的执行顺序就变成了loader1 => loader2 => loader3

inline loader

inline的使用方式比较特殊,它是在每次import样式的时候指定loader

import Styles from 'style-loader!css-loader!./styles.css';

含义:

  • 使用css-loaderstyle-loader处理 styles.css 文件
  • 通过!将资源中的loader连接起来

执行顺序:

// 添加 ! 前缀,跳过 normal loader
import Styles from '!style-loader!css-loader?modules!./styles.css';
// 添加 -! 前缀,跳过 pre/normal loader
import Styles from '-!style-loader!css-loader?modules!./styles.css';
// 添加 !! 前缀,跳过 pre/normal/post loader
import Styles from '!!style-loader!css-loader?modules!./styles.css';

如果不配置任何前缀的话,执行优先级是:pre > normal > inline > post不推荐使用inline loader

注意事项:要区分!是作为连接符存在还是作为配置方式存在,不要混淆。

如何编写一个Loader(简单版)

写在前面

image.png loader对于webpack而言,就像是webpack官网画的图那样,多条支流汇总成为一条(姑且成为一条)大的河流。而每一条支流,就是一个或多个loader的执行过程。

而每个开发者对于河流的定义是不一样的,可能我喜欢这个,他喜欢那个。所以各种各样的loader会帮助开发者去处理得到自己期望的结果。

了解loader

既然要编写一个loader,那就得了解一下loader

module: {
  rules: [
    {
      test: /\.js$/,
      loader: "loader1",
    },
    {
      test: /\.js$/,
      loader: "loader2",
    },
    {
      test: /\.js$/,
      loader: "loader3",
    },
  ],
},

还是拿上面的代码来举例,通过上面的学习我们已经知道,JavaScript文件会先通过loader3的处理,再经过loader2的处理,最后经过loader1的处理。

那么是不是就说明,每个loader其实就像是流水线上的一环,拿到上层结果,处理之后再传递给下层。

这是非常关键的一点,你要知道loader最根本的一个原理,因为当你处理过的数据,不能被下层接受使用的话,那你写的loader就是一个失败品。

开始学习

示例

module.exports = function(content, map, meta) {
  console.log('hello world!')
  // 本来只想写上面那一行的,但是上面那一行不能很好的代表loader对文件的处理
  // 于是写了下面这一段
  const now = Date.now()
  const newContent= `
    /** 
     * compile time: ${now}
     **/
  ` + content
  return newContent;
};

先从参数开始讲起

  • content 是上层loader传下来的经过处理的内容或者文件原始内容(在没有上层loader的情况下)
  • map 是有关SourceMap的一些东西(此处不多讲,其实是因为我也一知半解)
  • meta 上层loader传过来的数据

函数体内容不用多讲,无非就是如何对文件内容进行处理,最后返回给下层loader

而返回的方式要讲一下:

  • return 直接返回处理后的文件内容给下层。

但是从参数中我们可以看到,其实是有三个参数的,而return只能返回单个值。这就引出了第二种方式。

  • this.callback this.callback(err: Error | null, content: String | Buffer, sourceMap?: SourceMap, meta?: any)
module.exports = function(content, map, meta) {
  const now = Date.now()
  const newContent= `
    /** 
     * compile time: ${now}
     **/
  ` + content
  this.callback(null, newContent);
};

参数形式基本上就比loader的执行函数入参前面多一个err错误信息。通过声明规范可以看出content有两种类型StringBuffer。后者一般常见于对资源文件(图片、文档等)的处理。

所以loader最简单的写法就是以上的实例,不考虑SourceMap的情况下。而最重要的就是处理content

loader类型

  • 同步Loader 无论是 return 还是 this.callback 都可以同步地返回转换后的 content 值。(注意事项:同步loader拒绝任何的异步操作)
  • 异步Loader 顾名思义,不多说了。区别有两个:1. callback函数需要通过this.async来获取且没有return;2. 记得把callback写对位置(笑)。
module.exports = function(content, map, meta) {
  var callback = this.async();
  someAsyncOperation(content, function(err, result, sourceMaps, meta) {
    if (err) return callback(err);
    callback(null, result, sourceMaps, meta);
  });
};
  • Raw Loader 默认情况下,资源文件会被转化为 UTF-8 字符串,然后传给 loader。通过设置 raw,loader 可以接收原始的 Buffer。每一个 loader 都可以用 String 或者 Buffer 的形式传递它的处理结果。Complier 将会把它们在 loader 之间相互转换。
module.exports = function (content) {
  assert(content instanceof Buffer);
  return someSyncOperation(content);
  // 返回值也可以是一个 `Buffer`
  // 即使不是 "raw",loader 也没问题
};
module.exports.raw = true;
  • Pitching Loader 这个得细说一下,见下一小节

Pitching Loader (越过Loader)

上一小节告诉大家,loader其实就是一个函数,而pitching loader的区别在于会在原函数的基础上再新增一个pitch方法。

那么这时候loader其实是存在两个方法的,原来的方法我们称之为loader方法,新增的方法就称之为pitch方法。

之前我们一直提到的执行顺序,都只是指loader方法的执行顺序:

  • pre > normal > inline > post
  • 从右往左,从下往上

pitch方法则刚好相反,如下图

  • post > inline > normal > pre
  • 从左往右,从上往下 image.png

那我们来研究一下pitch方法的结构

module.exports = function(content) {
  console.log(this.data.value) // 42
  return someSyncOperation(content, this.data.value);
};
module.exports.pitch = function(remainingRequest, precedingRequest, data) {
  data.value = 42;
};

入参:

  • remainingRequest 剩余需要执行的pitch loader
  • precedingRequest 已经执行过得pitch loader
  • data 如代码,此时修改data时,在进入loader方法时,可以通过this.data获取

需要注意的是,pitch是否会返回非空值:

  • 返回空值(或无返回) 继续接下来的pitch方法
  • 返回非空值 会跳过本身的loader方法,以及后面所有的loader方法和pitch方法

举个栗子: normal-loader1

module.exports = function(content) {
  return someSyncOperation(content);
};

module.exports.pitch = function(remainingRequest, precedingRequest, data) {
  if (someCondition()) {
    // 这个返回值会成为上一个执行pitch的loader中的content参数
    return "const a = 1";
    // 这个是webpack的例子 有点问题
    return "module.exports = require(" + JSON.stringify("-!" + remainingRequest) + ");";
  }
};

这时候normal-loader1pitch执行完后,会直接执行上一个loaderloader方法。

image.png

这里讲一下为什么webpack给的例子有问题,他会以CommonJS的形式导出一个inline-loader的使用方式编译文件的一个代码。而参数又是remainingRequest(剩下未执行需要执行的loader),所以实际上并未达成一个跳过的效果。

建议小伙伴们都可以去试一下,这件事告诉我们即使是官网也不可信。

this对象

Loader Interface | webpack 中文文档 (docschina.org)

loader API | webpack 中文网 (webpackjs.com)

两个webpack文档,互相对照着看吧,需要用的时候也要考虑一下。

(小声BB:毕竟文档也不一定对)

复刻两个简简单单的loader(毕竟难的我也不会写)

clean-log-loader

module.exports = function (content) {
  // 清除文件内容中console.log(xxx)
  return content.replace(/console.log(.*);?/g, "");
};

add-message-loader

module.exports = function(content) {
  const options = this.getOptions(); // 获取loader中传递的options
  const bannerText = `
    /**
     * author: ${options.author}
     * time: ${options.time}
     * */
   `
   return bannerText + content;
}

webpack配置

{
  test: /.js$/,
  loader: "./loaders/add-message-loader",
  options: {
    author: "寒拾",
    time: '2022-8-22'
  },
},