调试

Webpack 的 loaders 和 rules 机制浅析

by Cheng, 2022-09-29


问题探索

有一个使用 webpack 构建的 react 项目在 macOSLinux 下能正常使用 webpack-dev-server 进行开发和调试,同时也能使用 webpack 进行构建,但是在 Windows 下开发和构建时候会报错,提示 scss 文件找不到 loader

ERROR in ./src/styles.scss 1:0
Module parse failed: Unexpected character '@' (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
> @import 'mixins.scss';
|
| .a-class-name {
 @ ./src/a-page/index.tsx 15:0-36 18:2-20:4 18:38-20:3 19:4-29 116:23-29 152:17-23 154:17-23 156:17-23 166:17-23 168:17-23 192:17-23 194:17-23 209:15-21 211:15-21
 @ ./src/App.tsx
 @ ./src/index.tsx
 @ multi ./node_modules/@pmmmwh/react-refresh-webpack-plugin/client/ReactRefreshEntry.js ./src

开始以为是 npm install 的时候没有把相关的包,例如 sasssass-loader 安装成功,于是重新执行 npm install -D sass sass-loader 并确保安装成功之后,依然显示上述错误。

❯ npm ls sass sass-loader webpack
the-project@ path-of-the-project
+-- [email protected]
| `-- [email protected] deduped
`-- [email protected]
+-- [email protected]
| `-- [email protected] deduped
+-- [email protected]
| +-- [email protected]
| | `-- [email protected] deduped
| `-- [email protected] deduped
+-- [email protected]
  `-- [email protected]
    `-- [email protected] deduped

在排除了包的安装问题之外,进一步推测,可能是 rule.test 匹配路径的时候出现了问题。项目中的关于 scss 的配置如下:

{
  test: /\/((src\/(patha|pathb|pathc))|(node_modules))\/(.*)\.(sa|sc|c)ss$/,
  use: ["css-loader", "postcss-loader", "sass-loader"],
};

由于原匹配的正则写得较为复杂,凭借着大道至简的原则,将上述正则改为下面的,项目果然可以正常调试和构建。

{
  test: /(.*)\.(sa|sc|c)ss$/,
  use: ["css-loader", "postcss-loader", "sass-loader"],
};

到这一步,基本上可以推断出问题是由于 WindowsUnix 的文件路径分隔符导致的。因为 Windows 下的路径分隔符为 \Unix 下的文件路径分隔符为/,上述的正则表达式当然会出现问题。

webpack loaders 机制

要想知道原理,调试是最方便的手段了,在设置好了调试环境之后,就可以对整个流程进行分析了。针对上面这个场景,以报错信息作为切入点是最方便的。经过全局查找之后,错误信息在 node_modules/webpack/lib/ModuleParseError.js 文件里。

Module Parse Error

在对应的行数打上断点之后,通过栈回溯,可以找到产生错误的位置。

Call Stack

可以看到,错误是在 NormalModule.js 里边产生的,进一步回溯,问题是在 NormalModule.js:doBuild 函数里边抛出的。

Parser

通过条件断点,可以进一步知道 parser里边发生了什么。

Conditional Breakpoint

Parser Exception

通过断点单步,可以发现异常的第一现场位于 node_modules/webpack/lib/Parser.js:parse 函数里边,而 acorn parser 则是用 JavaScript 实现的 JavaScript 解析器,将 sass 的代码传递给 JavaScript 解析当然会产生异常!

正常的流程应该是现有 sass-loaderscss 代码编译成 css 代码,然后由 css-loadercss 代码编译成 CommonJS 代码,再由 style-loader 生成代码将代码插入到页面的 style 中,或者是由 mini-css-extract-plugin 将代码剥离成单独的 css 文件。

Style Loader

这里的正常流程是 style-loader 生成了用于将 css 插入到页面的胶水代码传递给了 acron parser 解析成了 AST 进一步处理。

最后回到 node_modules/loader-runner/lib/LoaderRunner.js:iterateNormalLoaders 正常的情况下 loaderContext.loaders 对 webpack 配置文件中的 rules 对应规则中定义的 loader 数组。

webpack 的 rules 匹配机制

在了解了 webpackloader 机制之后,让我们把目光聚焦到 webpackrules 匹配机制上。

要找到 rules 的蛛丝马迹,最高效的方式仍然是通过条件断点和栈回溯。

Call Stack

最终,在 node_modules/webpack/lib/NormalModuleFactory.js 中找到了 this.ruleSet = new RuleSet(options.defaultRules.concat(options.rules)); 这一行代码。

Rules

通过断点,可以看到 options.rules 正是在 webpack 中定义的。而 webpackrules 逻辑主要是在 node_modules/webpack/lib/RuleSet.js 文件中实现的。RuleSet 的初始化逻辑是,遍历 webpack 配置中的 rules 并将每一个 test 字段都转成函数:

// node_modules/webpack/lib/RuleSet.js:normalizeCondition

if (typeof condition === "string") {
  return (str) => str.indexOf(condition) === 0;
}
if (typeof condition === "function") {
  return condition;
}
if (condition instanceof RegExp) {
  return condition.test.bind(condition);
}
if (Array.isArray(condition)) {
  const items = condition.map((c) => RuleSet.normalizeCondition(c));
  return orMatcher(items);
}

而匹配则是在 node_modules/webpack/lib/RuleSet.js:exec 函数中进行的。逻辑就是利用上一步生成的函数对文件名进行匹配。

Ruleset Execution

可以看到由于在 Windows 下的 path separator 的差异即 Unix 下为 / Windows 下为 \ ,所以当正则执行时,在 Windows 下会导致不匹配。

webpack

作者: Cheng

2025 © typecho & elise & Cheng