Webpack5源码解读系列6 - 模块预处理器 - Loader

lxf2023-05-12 01:03:34

Loader介绍 & 使用

Loader介绍

Loaders are transformations that are applied to the source code of a module. They allow you to pre-process files as you import or “load” them. Thus, loaders are kind of like “tasks” in other build tools and provide a powerful way to handle front-end build steps. Loaders can do these things:

  • Transform files from a different language (like TypeScript) to JavaScript.
  • Load inline images as data URLs.
  • Allow you to do things like import CSS files directly from your JavaScript modules!

上面是官方文档介绍内容,主要介绍Loader的执行时机以及用途:

  • 执行时机:当模块被导入或者加载时,Loader会将源码进行转义;
  • 用途:Loader可以灵活用于代码转义(TS to JS)、图片处理、CSS文件处理等。

浏览器能够识别的语言只有JS、CSS和HTML,但在前端生态衍生出了很多新的语言以及语言糖,如TSLessJSXVue Template,编译器无法直接解析这些语言/语法,需要有前置工具处理这些语言/语法,Loader则是提供模块预处理能力。

Loader基础用法

Loader配置式和内联式用法,配置式用于配置项目维度处理逻辑,而内联式用于配置单一文件,下面介绍时会出现一些名词介绍。

配置式

配置式用法从项目维度配置应用Loader,具体配置路径为module.rules。配置在执行时按照从右到左、从上到下规则执行配置。

module.exports = {
  module: {
    rules: [
      {
        // 匹配文件后缀应用 Loader
        test: /.css$/,
        use: [
          // loader名称
          { loader: 'style-loader' },
          {
            loader: 'css-loader',
            // loader自定义配置
            options: {
              modules: true,
            },
          },
        ],
       },
       {
         test: /.js$/,
         use: ['babel-loader'],
         // 可选 pre 或 post,如果不填则是normal,按照pre -> normal -> post三个阶段顺序执行
         enfore: 'pre'
       }
     ],
  },
};

内联式

内联式用法可针对个别模块配置Loader,在import语句内配置,用法为先配置Loader后配置引用文件,Loader之间使用!做分割:

// 自右向左分别应用 css-loader style-loader
import Styles from 'style-loader!css-loader?modules!./styles.css';

内联使用Loader可通过前缀去禁用来自配置文件配置的Loader

// 使用`!`开头禁用来自配置文件的所有 normal 阶段的loader
import Styles from '!style-loader!css-loader?modules!./styles.css';

// 使用`!!`开头代表禁用所有的的配置loader,包括 pre、normal、post阶段
import Styles from '!!style-loader!css-loader?modules!./styles.css';

// 使用`-!`开头代表禁用所有 pre、normal阶段的loader
import Styles from '-!style-loader!css-loader?modules!./styles.css';

运行Loader原理

Webpack并没有直接编写处理Loader模块,而是将Loader执行模块抽离出来,单独作为第三方包loader runner

Loader本质

Loader是一个同步或者异步JS函数,loader runner在执行Loader时会传入上一个Loader执行结果以及资源内容,同时在函数内部(非箭头函数)还可以使用this语句访问执行Context对象。同时,由于Loader是在Node环境下执行,所以可以使用Node环境的任何能力。

const loaderAPI = () => {};
// Loader导出函数作为执行对象
export default loaderAPI;

Loader区分同步和异步,异步用法与我们平时使用基于Promise异步能力不同,是由loader runner内部实现一套区分同步异步能力。

  1. 同步Loader:同步Loader可通过return语句返回单个内容,也使用this.callback传递多个返回:
module.exports = function (content, map, meta) {
  // 返回单个结果
  return someSyncOperation(content);
};

module.exports = function (content, map, meta) {
  // 调用this.callback返回多个结果
  this.callback(null, someSyncOperation(content), map, meta);
  return; // always return undefined when calling callback()
};
  1. 异步Loader:需要显式调用this.async()方法才可定义为异步Loader
module.exports = function (content, map, meta) {
  // 通过调用this.async获取返回值
  var callback = this.async();
  someAsyncOperation(content, function (err, result, sourceMaps, meta) {
    if (err) return callback(err);
    // 多个返回值
    callback(null, result, sourceMaps, meta);
  });
};

loader runner内部通过runSyncOrAsync方法完成处理Loader同步、异步逻辑,函数内部流程如下:

Webpack5源码解读系列6 - 模块预处理器 - Loader

Loader执行顺序

规则优先级别

Loader可以通过enforce配置字段调整规则处理顺序,对应选项有prepost,如果不填,默认为normal,如:

module.exports = {
  module: {
    rules: [
      {
        test: /.xxx$/,
        use: ['normal-loader-1', 'normal-loader-2'],
      },
      {
        test: /.xxx$/,
        use: ['pre-loader-1', 'pre-loader-2'],
        enforce: 'pre',
      },
      {
        test: /.xxx$/,
        use: ['post-loader-1', 'post-loader-2'],
        enforce: 'post',
      },
    ],
  },
};

如果上面三个规则同时命中时,会按照pre -> normal -> post顺序执行,如果处理的文件还包含内联Loader配置时,那么会按照pre -> normal -> inline -> post顺序执行。

Webpack5源码解读系列6 - 模块预处理器 - Loader

Loader处理阶段

Loader在运行时区分Pitch阶段和Normal阶段,一般情况下执行顺序是指Normal阶段的执行顺序,按照“从右到左”顺序执行,而Pitch阶段则相反,是“从左到右”。

  • Pitch阶段:按照post -> inline -> normal -> pre处理顺序在读取源代码之前执行
  • Normal阶段:按照规则优先级别为pre -> normal -> inline -> post执行顺序修改模块源码,最终输出。

Loader可以定义普通方法和pitch方法,如果Loader函数的pitch属性如果是一个方法的话,那么loader runner会为其注册pitch阶段任务。

为什么要有pitch方法呢?

有某些场景loader需要在修改目标的metadata, 此时需要在读取资源之前做处理,而普通loader方法是在读取到文件之后再做处理,无法满足需求。

loader runner在执行时,会先进入Pitch阶段,按“从左到右”顺序执行Loader.pitch,执行完毕后读取文件内容,再进入Normal阶段,按照“从右到左”顺序执行Loader

Webpack5源码解读系列6 - 模块预处理器 - Loader

Loader熔断机制

如果一个Loaderpitch方法返回一个undefined值时,那么会执行下一个方法,但是如果返回的是一个非空值时,那么会触发Loader的熔断机制,不再继续执行后续的Loader,而是掉头往前面执行:

Webpack5源码解读系列6 - 模块预处理器 - Loader

实例讲解

Loader存在NormalPitch之分,接下来分别讲解babel-loaderstyle-loader,感受一下不同类型Loader的应用。

babel-loader

如果我们的应用需要兼容旧版本浏览器,那么我们一定会使用到babel-loaderbabel-loader是对babel进行一层封装,将应用代码转义为向后兼容代码。简而言之babel-loaderbabelwebpack的适配层。

接下来我们浅浅地解析一下代码:

Webpack5源码解读系列6 - 模块预处理器 - Loader

首先映入眼帘的是babel-loader的模块导出语句,从代码中我们能过获得两个信息:

  • babel-loader只有一个normal loader,所以该Loader是在读取文件后运行。

  • babel-loader是一个异步Loader

接下来进入loader函数,函数内部做了两件事情:

  1. 参数检验/合并

Webpack5源码解读系列6 - 模块预处理器 - Loader

  1. 调用babel/core进行代码转义、输出

Webpack5源码解读系列6 - 模块预处理器 - Loader

可以看出Loader实际上并不复杂,就是针对模块内容的字符串处理函数,如果有需要我们也可以编写属于自己的Loader

style-loader

介绍

style-loader提供自动将css代码注入到html代码中,无法独自使用,一般和css-loader或者url-loader一起使用。以css-loader为例,它们的分工如下:

  • css-loader负责将css代码转译为js代码;
  • style-loader负责将转译后的css代码自动注入到html中。

有以下例子和配置

// index.css
.container {
  width: 100px;
  height: 100px;
}

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /.css$/,
        enforce: "pre",
        use: ["style-loader", "css-loader"]
      }
    ]
  },
}

上面代码经过css-loaderstyle-loader处理之后,在运行时会自动注入到html中(在运行时动态注入):

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>webpack-loader</title>
    <style>
      .container {
        width: 100px;
        height: 100px;
      }
    </style>
</head>
<body></body>
</html>

原理

style-loader源码大体上可以分为两块内容:

  • 代码生成:承接Loader处理逻辑部分,本文仅介绍这部分能力原理。
  • 运行时代码:运行时提供动态注入css代码能力,从这里可以看出webpack有很强的灵活性。

我们进入源码阅读,首先从入口文件开始读起:

Webpack5源码解读系列6 - 模块预处理器 - Loader

可以判断出style-loader只有pitch loader pitch 函数内部会返回 字符串。根据上面讲解的Loader执行顺序,我们很容易就能够发现webpack配置项存在问题:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /.css$/,
        enforce: "pre",
        use: ["style-loader", "css-loader"]
      }
    ]
  },
}

根据「熔断机制」,如果一个pitch loader返回一个非空内容,那么就不会执行后面的Loader,此时css-loader也不会被执行。

别急,我们继续看pitch执行完毕时返回具体内容,我们以一个简单例子看一下整个过程:

// index.js
import './index.css';

export default function A() {}

// index.css
.abc {
  width: 100%;
}

左边是生成代码部分,右边是返回style-loader处理完index.css的例子:

Webpack5源码解读系列6 - 模块预处理器 - LoaderWebpack5源码解读系列6 - 模块预处理器 - Loader

从右图中观察到style-loader并没有直接处理index.css文件,而是导入了运行时注入style标签能力模块,在结尾部分使用内联 Loader 用法重新导入index.css文件,此时会使用到css-loader解析index.css文件。

Webpack视野中,index.css../../../node_modules/css-loader/dist/cjs.js!./index.css是不同文件,所以会重新编译文件,并使用css-loader解析文件,最终效果如下:

Webpack5源码解读系列6 - 模块预处理器 - Loader

小结

LoaderWebpack中充当重要角色,是Webpack能够将所有资源都视为模块的基础,提供将任何资源模块都解析为JS模块能力。

Loader本质上是一个Node环境下第三方包,能够使用任何Node能力。Loader执行时分为PitchNormal和阶段,Pitch阶段会提取Loader导出函数的pitch方法并执行(Loader.pitch),它们的执行顺序为:

  • Pitch阶段:按照注册顺序“从左到右”(或“从上到下”)执行Loader.pitch方法。
  • Normal阶段:按照注册顺序“从右到左”(或“从下到上”)执行Loader方法。

Webpack5源码解读系列6 - 模块预处理器 - Loader

特殊地,如果一个Loader.pitch返回的内容为非空字符串,那么会触发“熔断机制”,即不会执行后面任何Loader,并返回到上一个Loader并继续执行:

Webpack5源码解读系列6 - 模块预处理器 - Loader 最后分别挑选了babel-loaderstyle-loader实例讲解处理过程。

本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!