# 「webpack 核心特性」loader

# 一、前言

webpack 是一个现代 JavaScript 应用的静态模块打包器。那么 webpack 是怎样实现不同种类资源模块加载的呢?

没错就是通过 loader。loader 用于对模块的源代码进行转换。loader 可以使你在 import 或加载模块时预处理文件。

我们带着下面几个问题,彻底吃透 loader ~

# 二、为什么要使用 loader

webpack 是如何加载资源模块的呢?我们先试着用 webpack 直接打包项目中的 css 文件。

初始化一个 webpack 项目,目录如下:

在 src 目录下新建了一个 index.css 文件,这里新建这个文件的目的就是以 css 文件为入口,尝试使用 webpack 单独打包它。

/* index.css */
body {
  margin: 0 auto;
  padding: 0 20px;
  width: 1000px;
  background-color: #ccc;
}

调整下 webpack 配置,让入口文件路径指定为 index.css 的路径。

// webpack.config.js
module.exports = {
  entry: "./src/index.css",
  output: {
    filename: "bundle.js",
  },
};

然后我们到终端运行 npx webpack 命令,你会发现命令行会提示 Module parse failed: Unexpected token (1:5) 模块解析错误。

细心的同学会发现后面还紧跟着一句解决方案:You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file.

大致的意思就是说,您可能需要适当的 loader 来处理此文件类型,目前没有配置 loader 来处理此文件。

这里,loader 的重要性就凸显出来了。

# 三、怎么配置 loader

还是接着刚才打包 index.css 报错的问题。想加载 css 文件,我们可以试试常用的 css-loader。

yarn add css-loader -D

webpack 配置也同步改下:

// webpack.config.js
module.exports = {
  mode: "none", // 避免不指定打包模式时弹出警告
  ...
  module: {
    rules: [
      {
        test: /\.css$/,
        use: "css-loader",
      },
    ],
  },
};

webpack 配置中 module 属性添加 rules 对象数组。这里面的 test 属性值为一个正则表达式,匹配当前 loader 对应处理文件的格式。use 属性值为 loader 名称。

再打包就不会报错了。

如果想要 index.css 模块在页面中生效,只需要额外添加一个 style-loader,样式就 OK 了。

style-loader 的作用可以理解为:建立了一个 style 标签,这个标签里面带入了 css 样式。标签最后追加到页面上。

注意配置多个 loader 时,执行顺序是从后往前执行的:

  • 最后的 loader 最早调用,将会传入原始资源内容
  • 第一个 loader 最后调用,期望值是传出 JavaScript 和 source map(可选)
  • 中间的 loader 执行时,会传入前一个 loader 传出的结果

所以 css-loader 放在最后。具体配置如下:

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

假如你还要用到 less-loader,同理可知 rules 中 use 属性值应该为:["style-loader", "css-loader", "less-loader"]

# 四、怎么写一个 loader

想要实现的大致流程:

接下来,我们尝试实现上图 css-loader 和 style-loader 的简单版本。

# 4.1 创建 loader

我们在项目根目录下创建好 css-loader.js 和 style-loader.js 文件。

主要代码如下:

├── src ····································· source dir
    │   ├── index.css ······················· css module
+   │   └── index.js ························ entry module
+   ├── css-loader.js ······················· css loader
    ├── package.json ························ package file
+   ├── style-loader.js ····················· style loader
    └── webpack.config.js ··················· webpack config file
/* index.css */
body {
  margin: 0 auto;
  padding: 0 20px;
  width: 1000px;
  background-color: #ccc;
}
// index.js
import "./index.css";
console.log("loader ok!");

每个 webpack 的 loader 都需要导出一个函数,这个函数就是我们这个 loader 对资源的处理过程,它的输入就是加载到的资源文件内容,输出就是我们加工后的结果。我们通过 source 参数接收输入,通过返回值输出。这里我们先尝试打印一下 source,然后在函数的内部直接返回一个字符串 hello webpack css-loader!,具体代码如下所示:

// css-loader.js
module.exports = (source) => {
  console.log(source);
  return "hello webpack css-loader!";
};

我们回到 webpack 配置文件中调整一下加载器规则,调整之后使用的加载器就是我们刚刚编写的这个 css-loader.js 模块,具体代码如下:

// webpack.config.js
module.exports = {
  mode: "none",
  // 入口改为 index.js
  entry: "./src/index.js",
  output: {
    filename: "bundle.js",
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        // 改下这里
        use: ["./css-loader"],
      },
    ],
  },
};

温馨提示:这里的 use 中不仅可以使用模块名称,还可以使用模块文件路径,这点与 Node 中的 require 函数相同。

配置完成后,我们再次打开命令行终端运行打包命令,如下图所示:

从报错信息可以看出,loader 函数的参数就是文件的内容。

错误提示说: You may need an additional loader to handle the result of these loaders. (我们可能还需要一个额外的加载器来处理当前加载器的结果)

温馨提示:其实 webpack 加载资源文件的过程最后的结果必须是一段标准的 JS 代码字符串。

正常流程:

我们现在应该想到是 css-loader 出了问题。

# 4.2 css-loader

css-loader 主要作用就是将多个 css 模块整合到一起。

module.exports = (source) => {
  // 匹配 url(xxx) 类似结构
  const reg = /url((.+?))/g;
  // 位置下标
  let pos = 0;
  // 当前匹配的代码片段
  let current;
  const arr = ["let list = []"];
  while ((current = reg.exec(source))) {
    const [matchUrl, g] = current;
    const lastPos = reg.lastIndex - matchUrl.length;
    arr.push(`list.push(${JSON.stringify(source.slice(pos, lastPos))})`);
    pos = reg.lastIndex;
    arr.push(`list.push('url(' + require('${g}') + ')')`);
  }
  arr.push(`list.push(${JSON.stringify(source.slice(pos))})`);
  arr.push(`module.exports = list.join('')`);
  // 每行代码之间增加一个回车
  return arr.join("\n");
};

大致思路:

  • 整个 css 代码片段以 url(xxx) 类似结构为节点分成多个部分
  • url 里的路径改为 require 引入
  • 用数组的形式将 css 代码拼凑起来最后形成一个整体

loader 打包结果如下图:

这是输出的 bundle.js 的片段,就是把我们刚刚返回的字符串直接拼接到了该模块中。这里也解释了刚才打包语法报错的问题(loader 处理完必须返回 JS 代码)。

# 4.3 style-loader

style-loader 会把整合的 css 部分挂载到 head 标签中。

代码如下:

module.exports = function(source) {
  return `
    const styleElement = document.createElement('style');
    styleElement.innerHTML = ${JSON.stringify(source)}
    document.head.appendChild(styleElement);
  `;
};

# 4.4 写 loader 之后的总结

loader 就是一个函数,一旦有模块被 import 或者 require 时它就会去拦截这些模块的源码,对其进行改造,然后输出到另一个模块中,循环往复,最终输出到入口文件中,形成最终的代码。

也正是有了 loader 机制,我们才能通过 webpack 去加载任何我们想要加载的资源。

# 五、感谢