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

前言

本节主要介绍这些插件的基本原理并手写一些常用的 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前端开发笔记,由于笔者能力有限,文章难免有疏漏之处,欢迎指正

  • 19
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值