问题探索
有一个使用 webpack 构建的 react 项目在 macOS 和 Linux 下能正常使用 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
的时候没有把相关的包,例如 sass 和 sass-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"],
};
到这一步,基本上可以推断出问题是由于 Windows 和 Unix 的文件路径分隔符导致的。因为 Windows 下的路径分隔符为 \
而 Unix 下的文件路径分隔符为/
,上述的正则表达式当然会出现问题。
webpack loaders 机制
要想知道原理,调试是最方便的手段了,在设置好了调试环境之后,就可以对整个流程进行分析了。针对上面这个场景,以报错信息作为切入点是最方便的。经过全局查找之后,错误信息在 node_modules/webpack/lib/ModuleParseError.js
文件里。
在对应的行数打上断点之后,通过栈回溯,可以找到产生错误的位置。
可以看到,错误是在 NormalModule.js
里边产生的,进一步回溯,问题是在 NormalModule.js:doBuild
函数里边抛出的。
通过条件断点,可以进一步知道 parser
里边发生了什么。
通过断点单步,可以发现异常的第一现场位于 node_modules/webpack/lib/Parser.js:parse
函数里边,而 acorn parser 则是用 JavaScript 实现的 JavaScript 解析器,将 sass 的代码传递给 JavaScript 解析当然会产生异常!
正常的流程应该是现有 sass-loader 将 scss 代码编译成 css 代码,然后由 css-loader 将 css 代码编译成 CommonJS 代码,再由 style-loader 生成代码将代码插入到页面的 style 中,或者是由 mini-css-extract-plugin 将代码剥离成单独的 css 文件。
这里的正常流程是 style-loader 生成了用于将 css 插入到页面的胶水代码传递给了 acron parser 解析成了 AST 进一步处理。
最后回到 node_modules/loader-runner/lib/LoaderRunner.js:iterateNormalLoaders
正常的情况下 loaderContext.loaders
对 webpack 配置文件中的 rules
对应规则中定义的 loader
数组。
webpack 的 rules 匹配机制
在了解了 webpack 的 loader 机制之后,让我们把目光聚焦到 webpack 的 rules 匹配机制上。
要找到 rules 的蛛丝马迹,最高效的方式仍然是通过条件断点和栈回溯。
最终,在 node_modules/webpack/lib/NormalModuleFactory.js
中找到了 this.ruleSet = new RuleSet(options.defaultRules.concat(options.rules));
这一行代码。
通过断点,可以看到 options.rules
正是在 webpack 中定义的。而 webpack 的 rules 逻辑主要是在 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
函数中进行的。逻辑就是利用上一步生成的函数对文件名进行匹配。
可以看到由于在 Windows 下的 path separator 的差异即 Unix 下为 /
Windows 下为 \
,所以当正则执行时,在 Windows 下会导致不匹配。