likes
comments
collection
share

webpack 中 loader 的简单实现

作者站长头像
站长
· 阅读数 20

本文将解释 webpack 中 loader 的作用,并根据官方文档,逐步编写一个简单的 loader。

loader 是什么

首先,对于 loader ,官方网站上有一个简短的解释:

A loader is a node module that exports a function. This function is called when a resource should be transformed by this loader. The given function will have access to the Loader API using the this context provided to it.


loader 是一个导出函数的 node 模块,当一个资源文件需要被这个 loader 转换时,该 loader 导出的函数将被调用。这个导出的函数可以通过其 this 上下文访问 Loader API 。

因此可以得知 loader 本质上是一个导出一个函数的模块。这个函数将被调用以转换资源文件。

而 webpack 本身只能处理 js 文件,因此,通过 loader 将非 js 文件进行转换,我们可以使用 webpack 解析任意格式的文件(例如通过 css-loaderless-loader 使 webpack 能够解析 *.css*.less 文件)。

loader 编写

目标

首先先决定我们需要编写的 loader 能够完成什么任务。这里我定义的 loader 如下:

  • 其读取 *.txt 文件;
  • 通过 loader 的 options 传递一组键值对 key: value,将 *.txt 文件中出现的形如 [key] 的字符串都替代为 value
  • 最后转换为 js 文件,该文件导出处理后的字符串;

实际操作

初始化

首先通过 npm init -y 初始化一个项目,再通过 npm i -D webpack webpack-cli webpack-dev-server 安装 webpack 以及服务热更新支持。

接下来构建一个简单的项目雏形,其目录如下:

├─dist
│  └─index.html
├─loader
│  └─txt-loader.js
├─node_modules
│  └─(...)
├─src
│  ├─data.txt
│  └─index.js
├─package-lock.json
├─package.json
└─webpack.config.js

其中 /dist/main.js 为 webpack 的输出文件, /src/index.js 为 webpack 的入口文件,即编译起点。

配置

webpack.config.js

编写 webpack.config.js 内容如下:

const path = require("path");

module.exports = {
  entry: "./src/index.js",
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "dist"),
  },
  mode: "development",
  module: {
    rules: [
      {
        test: /.txt$/,
        use: [
          {
            loader: path.resolve(__dirname, "loader/txt-loader.js"),
            options: {
              name: "auhnip",
              greetings: "How are you",
            },
          },
        ],
      },
    ],
  },
  // 下面为 webpack-dev-server 的配置
  // 非本文重点,不作过多解释
  devServer: {
    static: {
      directory: path.join(__dirname, "dist"),
    },
    compress: true,
    port: 4200,
    hot: true,
  },
};

其中,引用 loader 的语句在这一段:

{
  test: /.txt$/,
  use: [
    {
      loader: path.resolve(__dirname, "loader/txt-loader.js"),
      options: {
        name: "auhnip",
        greetings: "How are you",
      },
    },
  ],
},

这一 Rule 配置中, test 筛选后缀为 txt 的文件, use 指定对该文件应该使用的 loader 。这里通过 path.resolve 指定了我们自己编写的 loader ,其存放于 项目路径/loader/txt-loader.js 下。 options 属性传递了一些参数,稍后我们将在自己编写的 loader 中获取它。

dist/index.html

简单编写 html 模板如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Test Loader</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="main.js" ></script>
  </body>
</html>

在其 <script> 标签中引用了同目录下的 main.js ,即 webpack 打包后的 js 文件。

src/index.js

这是 webpack 的打包入口文件,我们编写如下测试代码:

import data from "./data.txt";

const root = document.getElementById("root");

const para = document.createElement("p");
para.innerHTML = data;

root.appendChild(para);

其从同目录下的 data.txt 文件中导入了一个对象 data ,并新建了一个 DOM 节点 <p> ,将其内容设置为 data ,最后将该新建的节点挂载到原 HTML 文件的 #root 节点下。

src/data.txt

对这一文件,简单填入一些测试数据:

Hello, [name]! [greetings].

其中形如 [xxx] 的片段都是我们希望 loader 替换的内容。回看 webpack.config.js 中的配置信息,我们向自己编写的 txt-loader 传入了如下 options 对象:

{
  name: "auhnip",
  greetings: "How are you",
}

其意为我们希望将文件中的 [name] 都替换为 auhnip ,将 [greetings] 替换为 How are you

编写 loader

loader/txt-loader

首先阅读官方文档,有如下描述:

When a single loader is applied to the resource, the loader is called with only one parameter – a string containing the content of the resource file.


当一个单独的 loader 被应用到资源文件上时,该 loader 将被使用唯一的参数(一个包括资源文件内容的字符串)进行调用。

我们可以获知,我们的 loader 需要导出的函数只需要接收一个参数 content ,其类型为字符串,其内容为资源文件——我们希望 loader 应用到的 *.txt 文件——的内容。

接着,关于返回值,官方文档有如下说明:

The loader is expected to give back one or two values. The first value is a resulting JavaScript code as string or buffer. The second optional value is a SourceMap as JavaScript object.


loader 应该返回一到两个值,第一个返回值应该为处理后的 JS 代码,作为 string 或 buffer 。第二个可选的返回值为一个 SourceMap ,其形式为一个 JS 对象。

在本例中,我们只需要返回一个值即可,其为一个字符串,字符串内容为合法的 JS 代码,该代码需要导出一个字符串字面量,供 src/index.js 导入使用。

最后一个门槛在于如何获取 loader 的 options ,即 loader 对应的配置。查阅 Loader API 文档,可以找到 this.getOptions(schema) 条目:

Extracts given loader options. Optionally, accepts JSON schema as an argument.

Tip:

Since webpack 5, this.getOptions is available in loader context. It substitutes getOptions method from loader-utils.


提取 loader 的 options 对象,该函数可选地接收一个 JSON 模式作为参数。

提示:

自从 webpack 5 , this.getOptions 将在 loader 上下文中可用。它代替了 loader-utils 包提供的 getOptions 方法。

这里提到的 this.getOptions() 接收的参数,其用以对 options 对象进行模式校验,以提示用户其是否存在错误的配置。

在了解了 loader 相关的基础内容后,可以编写 loader 代码如下:

module.exports = function (content) {
  // 获取 options 对象
  const options = this.getOptions();
  // 获取 options 对象中的每个 key
  for (const keyWord in options) {
    // 将资源文件( *.txt )的内容中符合 [key] 的内容
    // 都替换为 options 中对应的 value (即 options[key] )
    content = content.replace(`[${keyWord}]`, options[keyWord]);
  }
  // 返回 JS 代码,其默认导出一个字符串
  // 字符串内容为刚刚处理过的 *.txt 文件的内容
  return `export default \`${content}\``;
};

测试 loader

loader 写好后,在 package.jsonscripts 属性中,添加如下内容:

"scripts": {
  "start": "webpack-dev-server"
}

在控制台输入 npm start 后,启动浏览器访问 http://localhost:4200 观察结果。

webpack 中 loader 的简单实现

可以看见, 刚刚编写的 loader 成功将 data.txt 文件中的 [name] 转换成了 auhnip ,将 [greetings] 转换为了 How are you 。而后 src/index.js 文件中的 JS 代码逻辑负责导入该内容,并将其输出到网页上。

webpack 通过解析 src/index.js 的内容和依赖,打包生成 dist/main.js 文件。最后,在 dist/index.html 中,使用 <script> 引入了打包后生成的 dist/main.js ,其实现与 src/index.js 相同的逻辑,即将 data.txt 经过 txt-loader 处理后的内容输出到网页上。

总结

本文对 loader 的作用进行了概述,并根据官方文档逐步编写了一个简单的 loader 。

loader 可以对在 JS 代码中导入的文件进行预处理,由此可以实现许多功能,像是预处理 CSS 代码,实现 CSS 的模块化,以及对 TS 、 SASS 、 VUE 文件等新的文件格式提供支持。