前端模块化-探究webpack loader的原理以及实现常见的loader

news2024/11/18 23:33:59

前言

本节主要介绍这些插件的基本原理并手写一些常用的 Loader。

本节对应的 demo 可以在这里找到。

什么是 Loader

在 Webpack 中,Loader 是用于对模块的源代码进行转换的工具。Webpack 将一切视为模块,而这些模块可能是各种类型的文件,如 JavaScript、CSS、图片等。Loader 负责处理这些文件,将它们转换成 Webpack 可以处理的模块。

常用的 Loader 有:

Loader作用
babel-loader将现代 JavaScript 转译为较旧版本
style-loader将样式注入到 DOM 中
css-loader在 JavaScript/TypeScript 中解析和导入 CSS 文件
file-loader将文件复制到输出目录
url-loader类似于 file-loader,但可以将小文件转换为 Data URL
sass-loader将 SASS/SCSS 文件加载并编译为 CSS
less-loader将 LESS 文件加载并编译为 CSS
ts-loader将 TypeScript 转译为 JavaScript
postcss-loader使用 PostCSS 插件处理 CSS
eslint-loader在 JavaScript/TypeScript 文件上运行 ESLint
vue-loader加载和编译 Vue.js 组件
raw-loader将文件内容作为字符串加载
image-webpack-loader优化和压缩图像文件

完整的列表可以参考 Webpack 官方文档。

Loader 与 Plugin 的区别

作用工作方式示例
Loader用于处理模块文件,将它们转换成可以被添加到依赖图中的有效 JavaScript 代码。沿着文件的加载链应用,按照规定的顺序一个接一个地处理模块文件。Babel Loader 用于将 ECMAScript 2015+ 代码转换为向后兼容的 JavaScript 版本。
Plugin用于执行更广泛范围的任务,例如打包优化、资源管理、注入环境变量等。通过钩子机制与 Webpack 构建过程的不同阶段交互,允许你在构建流程中执行自定义操作。HtmlWebpackPlugin 用于生成 HTML 文件,并自动将打包后的脚本文件引入 HTML 中。

简而言之,Loader 处理模块文件的转换,而 Plugin 用于执行各种构建过程的自定义任务。Loader 是一个文件级别的处理器,而 Plugin 更关注整个构建流程。在配置文件中,我们会配置一系列 Loader 来处理特定类型的文件,而 Plugin 通常是一个实例,通过 plugins 数组添加到配置中。

Loader 的输入和输出

默认情况下,资源文件会被转化为 UTF-8 字符串,然后传给 loader。通过设置 raw 为 true,loader 可以接收原始的 Buffer。

// loader.js
module.exports = function (content) {
  // 对输入源进行处理,这里简单地在源代码前面添加一行注释
  const processedSource = `// This is a custom loader\n${source}`;

  // 返回处理后的结果
  return processedSource;
};

module.exports.raw = true;

loader 的输出内容必须是 String 或 Buffer 类型。

同步 Loader 和异步 Loader

同步 Loader

同步 loader 是最常见的 loader 类型。它们按照 webpack 的默认处理流程,依次处理每个模块。每个同步 loader 都会在一个模块的代码被转换之后,再将结果传递给下一个 loader。这种 loader 的编写和配置非常简单。

异步 Loader

异步 loader 具有更灵活的处理方式,它们可以在处理一个模块时进行一些异步操作,例如从网络请求数据,然后在异步操作完成后继续处理模块。

为了创建异步 loader,需要使用 this.async() 方法。这个方法返回一个回调函数,当异步操作完成时,你需要手动调用这个回调函数,将处理结果传递给下一个 loader。

以下是一个简单的异步 loader 的例子,假设它通过网络请求获取模块内容:

// async-loader.js
module.exports = function (source) {
  // 获取 loader 上下文,this 对象
  const callback = this.async();

  // 模拟一个异步操作(例如,从网络请求数据)
  setTimeout(() => {
    const simulatedData = { message: "这是模拟的异步数据" };
    const transformedSource = source.replace(
      "/* async-data-placeholder */",
      JSON.stringify(simulatedData)
    );

    // 调用回调函数,将处理后的源代码传递给下一个 loader
    callback(null, transformedSource);
  }, 1000); // 模拟异步操作耗时 1 秒钟
};

Loader 的工作原理和执行顺序

在 Webpack Loader 的执行过程中,有两个重要的阶段, pitch 阶段和 normal 阶段。

假设我们有以下三个 Loader:Loader1、Loader2、Loader3,对应的配置如下:

// webpack.config.js
module: {
  rules: [
    {
      test: /\.js$/,
      use: ['loader3', 'loader2', 'loader1'],
    },
  ],
}

对应的 webpack 执行阶段如下图

Pitch 阶段:

  • 如果存在 pitch 方法:

    • 首先执行 loader3 的 pitch 方法。
    • 接着执行 loader2 的 pitch 方法。
    • 最后执行 loader1 的 pitch 方法。
      Normal 阶段:
  • 如果存在 normal 方法:

    • 首先执行 loader1 的 normal 方法。
    • 接着执行 loader2 的 normal 方法。
    • 最后执行 loader3 的 normal 方法。

enforce 属性的作用:

enforce: “pre” 属性将 loader1 设置为预处理 Loader,这意味着在正常的 Loader 执行前,会先执行 loader1 的 pitch 方法和 normal 方法。这用于在正式加载模块之前执行一些预处理操作,例如代码静态分析或代码风格检查。

Pitch 阶段的作用:

如果某个 Loader 的 pitch 方法返回了非 undefined、非 null 或非空字符串的结果,Webpack 将停止执行 Pitch 阶段,并从该 Loader 开始执行 Normal 阶段。

Loader 的配置

在 Webpack 配置文件中,使用 module.rules 配置项来定义 Loader 的规则。每个规则是一个对象,包含两个主要属性:test 和 use。

  • test: 用于匹配需要被 Loader 处理的文件类型的正则表达式。
  • use: 用于指定应用哪些 Loader,可以是字符串或数组,按照从右到左的顺序执行。
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/, // 匹配以.js结尾的文件
        use: ["babel-loader"], // 使用babel-loader进行处理
        options: {
          presets: ["@babel/preset-env"],
        },
      },
      {
        test: /\.css$/, // 匹配以.css结尾的文件
        use: ["style-loader", "css-loader"], // 先用css-loader处理,然后再用style-loader
      },
    ],
  },
};

loader 的三种引入方式

  1. 直接在配置文件中使用 Loader 名称:

在 Webpack 配置文件中,可以直接使用 Loader 的名称,Webpack 会自动查找并使用这些 Loader。

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ["babel-loader"],
      },
      {
        test: /\.css$/,
        use: ["style-loader", "css-loader"],
      },
    ],
  },
};
  1. 使用完整的模块路径:

也可以使用 Loader 的完整模块路径,这样可以确保使用的是项目中指定的 Loader 版本。

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [require.resolve("babel-loader")],
      },
      {
        test: /\.css$/,
        use: [require.resolve("style-loader"), require.resolve("css-loader")],
      },
    ],
  },
};
  1. 通过 require 导入 Loader:

在配置文件中,你还可以使用 require 导入 Loader,并将其传递给 use 数组。

// webpack.config.js
const babelLoader = require.resolve("babel-loader");
const styleLoader = require.resolve("style-loader");
const cssLoader = require.resolve("css-loader");

module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [babelLoader],
      },
      {
        test: /\.css$/,
        use: [styleLoader, cssLoader],
      },
    ],
  },
};

常用的 Loader 实现

手写 loader 首先还是关注官方指南 编写一个 Loader。

最简单的 loader

最简单的 loader 就是将源代码原封不动的返回,例如:

module.exports = function (source) {
  return source;
};

babel-loader

主要通过使用 @babel/parser 解析源代码,然后通过 @babel/traverse 遍历 AST(抽象语法树),最后使用 @babel/generator 重新生成代码。

首先安装依赖:

npm install @babel/parser @babel/traverse @babel/generator

然后,创建一个简单的 Babel Loader 文件, 代码如下:

const { transform } = require("@babel/core");

function loader(source) {
  // 获取 Loader 配置的选项
  const options = this.getOptions();

  console.log("babel-loader: ", options);

  // 使用 Babel 转换代码
  const transformedCode = transform(source, {
    ...options,
    sourceMap: true,
  }).code;

  return transformedCode;
}

module.exports = loader;

然后,在 Webpack 配置中使用这个手写的 Babel Loader:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.js$/,
        use: ["./babel-loader"],
      },
    ],
  },
};

less-loader

首先需要安装 less 包:

 npm install less

主要代码如下:

const less = require("less");

module.exports = function (source) {
  // 使用 less 包解析 LESS 代码
  less.render(source, (error, result) => {
    if (error) {
      this.emitError(error); // 将错误传递给 Webpack 处理
      return;
    }

    // 返回转换后的 CSS 代码
    this.callback(null, result.css, result.map);
  });
};

css-loader

css-loader 的作用主要是解析 css 文件中的@import 和 url 语句,处理 css-modules,并将结果作为一个 js 模块返回。

module.exports = function (source) {
  console.log("css-loader source: ", source);

  const classRegex = /(?<=\.)(.*?)(?={)/g; // 获取字符串所有类名的正则
  const classKeyMap = Object.fromEntries(
    source.match(classRegex).map((str) => [str.trim(), str.trim()])
  ); // 取出字符串中原始 CSS 类名
  return `/**__CSS_SOURCE__${source}*//**__CSS_CLASSKEYMAP__${JSON.stringify(
    classKeyMap
  )}*/`;
};

style-loader

经过 css-loader 的转译,我们已经得到了完整的 css 样式代码,style-loader 的作用就是将结果以 style 标签的方式插入 DOM 树中, 主要源码如下:

module.exports = function (source) {
  console.log("style-loader", source);

  const cssSource = source.match(/(?<=__CSS_SOURCE__)((.|\s)*?)(?=\*\/)/g); // 获取 CSS 资源字符串
  const classKeyMap = source.match(
    /(?<=__CSS_CLASSKEYMAP__)((.|\s)*?)(?=\*\/)/g
  ); // 获取 CSS 类名 Map

  console.log("classKeyMap", classKeyMap);

  const script = `
    var style = document.createElement('style');
    style.innerHTML = ${JSON.stringify(cssSource)};
    document.head.appendChild(style);
    
    // Export classKeyMap if available
    ${classKeyMap !== null ? `module.exports = ${classKeyMap}` : ""}
  `;

  return script;
};

file-loader && url-loader

file-loader 和 url-loader 是 webpack 中的两个常用的 loader,它们都用于处理资源文件的加载和处理,但是它们有一些不同点。

file-loader 主要用于处理文件资源,将资源文件输出到指定的目录中,并返回一个资源文件的 url 地址,可以通过设置 outputPath 参数来指定资源文件的输出目录,也可以通过设置 publicPath 参数来指定资源文件的 url 地址的前缀,它会将资源文件复制到指定目录中,并且返回一个字符串路径,以便在代码中使用。

url-loader 和 file-loader 相比,它可以将较小的文件直接转换成 base64 编码格式的字符串,以减少网络请求,而不需要将文件加载到本地,可以通过 limit 参数来控制需要转换成 base64 格式的文件大小,可以通过 fallback 参数来设置在文件转换失败时使用的备用加载器。

先来看一下 file-loader 的实现:

const loaderUtils = require("loader-utils");

function fileLoader(source) {
  // 使用 loader-utils 的 interpolateName 函数根据内容生成一个文件名
  const filename = loaderUtils.interpolateName(this, "[name].[hash].[ext]", {
    content: source,
  });

  // this.emitFile(filename, source): 用于将文件输出到输出目录。它接受文件名和文件内容作为参数。这确保文件包含在输出中,并可以被构建过程中的其他部分引用。
  this.emitFile(filename, source);

  return `module.exports="${filename}"`;
}

// 将 loader 的 raw 属性设置为 true,表示该 loader 处理二进制数据。在这里,这意味着文件内容被读取并作为 Buffer 输出。
fileLoader.raw = true;

module.exports = fileLoader;

再看看 url-loader 的实现:

function urlLoader(source) {
  console.warn("url-loader: ", source.size);

  // 获取 loader 的 options(配置)
  const options = this.getOptions() || { limit: 20480 };

  // 如果文件大小小于指定的阈值,则转为 Data URL
  if (options.limit && source.length < options.limit) {
    const base64 = Buffer.from(source, "binary").toString("base64");
    return `module.exports="data:${
      this.resourceMimeType || "application/octet-stream"
    };base64,${base64}"`;
  }

  // 否则,使用file-loader处理
  return require("../file-loader").call(this, source);
}

urlLoader.raw = true;
module.exports = urlLoader;

对应的 webpack 配置

{
  test: /\.(png|jpg)$/,
  use: [
    {
      loader: "./loaders/url-loader",
      options: {
        limit: 20480, // 小于 20kb 的图片转成 base64
      },
    },
  ],
}

最终效果如下:

一些业务实践

  1. 去除 antd 中的 selection
// rm-selection-loader.js
// 去除样式文件中的 ::selection,原因是::selection难以被取消
module.exports = function runtime(params) {
  return params.replace(/::selection \{[^}]+\}/g, "");
};

// config.js
config.module
  .rule("less-in-node_modules")
  .use("custom")
  .before("css-loader")
  .loader(path.resolve(__dirname, "./rm-selection-loader.js"));

本文首发于个人 Github前端开发笔记,由于笔者能力有限,文章难免有疏漏之处,欢迎指正

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1991279.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

VSCode编译多个不同文件夹下的C++文件

实际上VSCode编译C文件就是通过向g传递参数实现的&#xff0c;因此即使是不同包下面的cpp文件或者.h文件都是可以通过修改g的编译参数实现&#xff0c;而在VSCode中&#xff0c;task.json文件其实就是在配置g的编译参数&#xff0c;因此我们可以通过修改task.json里面的参数&am…

洛谷 B2145 digit 函数 B2146 Hermite 多项式 题解

题目目录&#xff1a; No.1 B2145 digit 函数 No.2 B2146 Hermite 多项式 OK&#xff0c;开始正文&#xff01; 第一题&#xff1a;B2145 digit 函数 题目描述 在程序中定义一函数 digit(n,k)&#xff0c;它能分离出整数 n 从右边数第 k 个数字。 输入格式 正整数 n …

Topsis法模型(评价类问题)

目录 本文章内容参考&#xff1a; 一. 概念 二. 特点和适用范围 三. 实现步骤 四. 代码实现 本文章内容参考&#xff1a; TOPSIS法模型讲解(附matlab和python代码) 【数学建模快速入门】数模加油站 江北_哔哩哔哩_bilibili 一. 概念 TOPSIS&#xff08;Technique for Or…

让EHS管理更智能,一起来看物联网如何重塑企业EHS管理

随着信息技术的飞速发展&#xff0c;物联网&#xff08;IoT&#xff09;技术正逐步渗透到企业管理的各个领域&#xff0c;特别是在环境、健康与安全&#xff08;EHS&#xff09;管理方面&#xff0c;物联网技术展现出了巨大的潜力和价值。 一、物联网技术在EHS管理中的应用场景…

达梦数据库 逻辑备份还原

达梦的逻辑备份还原 1.背景2.要求3.实验步骤3.1 相关术语3.2 dexp逻辑导出3.2.1 使用dexp工具3.2.2 dexp相关参数含义3.2.3 四种级别导出3.2.3.1 FULL3.2.3.2 OWNER3.2.3.3 SCHEMAS3.2.3.4 TABLES 3.2.4 使用范例3.2.4.1 环境准备3.2.4.2 dexp逻辑导出 3.3 dimp逻辑导入3.3.1 使…

【大模型从入门到精通10】openAI API 提示链的力量1

这里写目录标题 提示链的力量核心概念理解提示链用于清晰说明的类比 实际应用与益处工作流程管理成本效率错误减少动态信息加载 方法学步骤式方法最佳实践 示例设置环境从用户查询中提取相关信息获取详细产品信息 提示链的力量 核心概念 理解提示链 提示链涉及将复杂任务分解…

C++速学day2

xia复习 上一天的学习内容&#xff1a; 重点&#xff1a;1、封装———— 就是对类的抽象 &#xff0c;将一种对象的共性 抽象成一个类。 2、三个函数——-构造函数/复制构造函数/析构函数 注意&#xff1a;析构函数和构造函数的调用顺序刚好相反。 新内容 两个类的关系 …

巨能涨!用AI做沙雕日常图文号,闭眼出大爆款!接个软广3000+!

家人们&#xff01;最近圈子陆续整理了一波在小红书上&#xff0c;适合植入软广的AI小红书商单玩法案例&#xff0c;例如&#xff1a;AI美女博主账号、AI养生博主账号、AI治愈插画Vlog短视频账号等等&#xff0c;接下来也会持续输出更多高价值的软广案例玩法。 今天刚好在刷小…

【python】PyQt5中QButtonGroup的详细用法解析与应用实战

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

正点原子imx6ull-mini-Linux驱动之Linux 网络驱动实验

网络驱动是 linux 里面驱动三巨头之一&#xff0c;linux 下的网络功能非常强大&#xff0c;嵌入式 linux 中也常 常用到网络功能。前面我们已经讲过了字符设备驱动和块设备驱动&#xff0c;本章我们就来学习一下 linux 里面的网络设备驱动。 1&#xff1a;嵌入式网络简介 1.1…

Pandas中高效的“For循环”

循环是我们编程技能中的一项固有技能。当我们熟悉任何编程语言时&#xff0c;循环就会成为一个基本的、易于解释的概念。 在这篇博文中&#xff0c;我们将探索遍历pandas dataframe的各种方法&#xff0c;检查每个循环方法的相关运行时。为了验证循环的有效性&#xff0c;我们…

3D开发工具HOOPS如何实现数字孪生高效的模型设计和分析?

数字孪生技术通过创建物理对象或系统的虚拟模型&#xff0c;实时反映其状态和行为&#xff0c;从而实现监控、优化和预测。这一技术在智能制造、建筑、城市规划等领域有着广泛应用。HOOPS SDK作为一套功能强大的软件开发工具包&#xff0c;为数字孪生技术的实现提供了全面支持。…

【启明智显分享】烹饪机HMI超值之选:个位数价工业级芯片Model3C点亮4.3寸触摸彩屏

一、方案背景 在快节奏的现代生活中&#xff0c;人们对于美食的追求从未停止&#xff0c;但繁琐的烹饪过程却常常让人望而却步。为了满足人们既能轻松享受美味又能节省时间和精力的需求&#xff0c;自动烹饪机应运而生。目前&#xff0c;自动烹饪机发展也越来越成熟&#xff0…

数据结构 - 哈希表

文章目录 前言一、哈希思想二、哈希表概念三、哈希函数1、哈希函数设计原则2、常用的哈希函数 四、哈希冲突1、什么是哈希冲突2、解决哈希冲突闭散列开散列 五、哈希表的性能分析时间复杂度分析空间复杂度分析 前言 一、哈希思想 哈希思想&#xff08;Hashing&#xff09;是计…

振动分析-18-基于振动分析进行故障诊断的思路和步骤

参考树立正确的振动诊断思路 参考振动分析相关知识的储备及振动分析仪的局限性 参考如何进行振动分析诊断(译文) 1 正确的故障诊断意识 我们通常在学习班听到的是大学教授以及专家讲解的故障诊断的基础理论,对于刚接触这个专业的人来说,微分方程和复杂的矩阵却有点让人忘…

数据库篇--八股文学习第十六天| MySQL的执行引擎有哪些?;MySQL为什么使用B+树来作索引;说一下索引失效的场景?

1、MySQL的执行引擎有哪些&#xff1f; 答&#xff1a; MySQL的执行引擎主要负责查询的执行和数据的存储, 其执行引擎主要有MyISAM、InnoDB、Memery 等。 InnoDB引擎提供了对事务ACID的支持&#xff0c;还提供了行级锁和外键的约束&#xff0c;是目前MySQL的默认存储引擎&…

227还原实战(三)

调转符号 为了方便后面处理 &#xff0c;我们先将所有的 二项表达式 进行预处理&#xff0c;将标识符放在左边&#xff0c; 数字放在右边&#xff0c; 比较简单&#xff0c;不多解释 转换逗号表达式 这里还原逗号表达式就要简单很多&#xff0c;主要是还原三元外的逗号表达式…

Agent知识库:功能、原理浅析

随着LLM不断发展&#xff0c;基于LLM的Agent今年来十分火热。Agent知识库不仅可以存储大量的信息&#xff0c;还包含了丰富的规则、算法和模型&#xff0c;是Agent实现智能化决策和行动的关键。本文将介绍Agent知识库的功能、原理以及相关落地应用。 概览 能力 1.Knowledge&a…

UDP端口可达性检测(端口扫描)工具开发

UDP端口可达性检测(端口扫描)工具开发 1、应用场景分析 主机X与主机Y部署在AB双网环境下&#xff0c;两个主机间通过UDP协议进行数据交互。应用程序发送数据时&#xff0c;优先使用A网发送数据&#xff0c;如果A网异常则通过B网发送数据。两个主机应用间没有设置心跳帧 &…

Vue 3+Vite+Eectron从入门到实战系列之(四)一Electron热身运动(二)

在electron里面能不呢实现暗黑模式和明亮模式的切换&#xff1f;我们怎么读取主进程里面的数据和系统数据。这篇就是来实现这几个效果的 实现效果 更改系统的主题色 在 App.vue 中添加代码。 <el-button type"warning" click"changeTheme">更改系…