记一次 Next.js 样式异常排查与 pr 修复(涉及 Nodejs 环境)
记一次 Next.js 样式异常排查与 pr 修复(涉及 Nodejs 环境)
原文地址: https://github.com/xiaoxiaojx/blog/issues/46
问题简述
开发同学反馈 Next.js 项目 next dev 命令启动后浏览器访问页面 css 样式有丢失,表现为 style 标签的内容不是 css 样式而是一个 url 😨
<style>/_next/static/media/globals.23a96686.css</style>
面对这个略显奇葩的问题该如何排查了 ?
问题排查
Step1 确认是否配置了错误的 loader
- 分析: 给 css 文件设置错了 loader 造成结果不符合预期也说得过去
- 结论: ❌ 没有发现可疑的 loader, 经过查看 webpackConfig 发现会命中红圈 oneOf 数组索引为 6 的规则, 即 css 文件会依次被 postcss-loader > css-loader > next-style-loader 来依次处理, 看上去没有任何问题
Rule.oneOf : An array of Rules from which only the first matching Rule is used when the Rule matches.
同时根据 Rule.oneOf 的定义确认了只会从 oneOf 数组中找到一个匹配到的规则就会停止, 那么 loader 应该是都被正确设置了
Step2 处理 css 文件的 loader 有 bug ?
- 分析: 确认了没有奇怪的 loader 掺杂进来, 那么是不是命中的 loader 有 bug 了
- 结论: ✅ 果然有 bug
loader 正常是按从下到上, 从右到左的顺序执行, 即如上配置会按从 postcss-loader > css-loader > next-style-loader 来依次处理。
// packages/next/build/webpack/loaders/next-style-loader/index.js
import path from ‘path’
import isEqualLocals from ‘./runtime/isEqualLocals’
import { stringifyRequest } from ‘…/…/stringify-request’
const loaderApi = () => {}
loaderApi.pitch = function loader(request) {
// …
}
module.exports = loaderApi
但是由于 next-style-loader 导出了 pitch 属性, loader 的顺序将会变成首先运行 pitch 函数, 相关文档见 Pitching Loader
|- next-style-loader `pitch`
|- requested module is picked up as a dependency
|- postcss-loader normal execution
|- css-loader normal execution
|- next-style-loader normal execution
那么我们先从 next-style-loader 的 pitch 函数开始排查, 然后发现了如下语句
// packages/next/build/webpack/loaders/next-style-loader/index.js
var content = require(${stringifyRequest(this, !!${request}
)});
上面语句补充上变量的值后相当于如下
var content = require('!!xxx/css-loader/src/index.js??xxx/postcss-loader/src/index.js??ruleSet[1].rules[3].oneOf[6].use[2]!./globals.css')
♻️ 这里的依赖关系先简单理一下, 比如 _app.tsx 引用了 globals.css
// pages/_app.tsx
import ‘…/styles/globals.css’
globals.css 模块的内容被 next-style-loader 处理后的内容类似于下面这样
// styles/globals.css
var content = require(’!!xxx/css-loader/src/index.js??xxx/postcss-loader/src/index.js??ruleSet[1].rules[3].oneOf[6].use[2]!./globals.css’)
const style = document.createElement(‘style’);
style.appendChild(document.createTextNode(content));
document.head.append(style)
所以 style 标签里面的 content 的源头其实是 require 的 !!xxx/css-loader/src/index.js??xxx/postcss-loader/src/index.js??ruleSet[1].rules[3].oneOf[6].use[2]!./globals.css 模块提供
// !!xxx/css-loader/src/index.js??xxx/postcss-loader/src/index.js??ruleSet[1].rules[3].oneOf[6].use[2]!./globals.css
module.exports = ?
那么对于 !!xxx/css-loader/src/index.js??xxx/postcss-loader/src/index.js??ruleSet[1].rules[3].oneOf[6].use[2]!./globals.css 这个路径上带有 loader 的模块其文件后缀依然为 .css, 那么不还得命中 oneOf 数组索引为 6 的规则么 🤔 ?
经过 debug webpack 的代码发现结果是意外的 ❌, 它命中的是 oneOf 数组索引为 8 的规则
Rule.issuer: A Condition to match against the module that issued the request. In the following example, the issuer for the a.js request would be the path to the index.js file.
关于 Rule.issuer: 比如 pages/_app.tsx 文件里面 import 或者 require 了 globals.css, 那么对于 globals.css 而言, 它的 issuer 为 pages/_app.tsx
让我们回头查看一下为什么没有命中索引为 6 的规则, 发现索引为 6 规则非常重要的一点是要求引用该文件的 issuer 必须只能是 pages/_app.tsx, 见下图红圈的 issuer.and 字段
而 !!xxx/css-loader/src/index.js??xxx/postcss-loader/src/index.js??ruleSet[1].rules[3].oneOf[6].use[2]!./globals.css 模块其实是被 next-style-loader 代理后的 globals.css 所 require, 所以它的 issuer 竟然为 styles/globals.css 🤯
至此 !!xxx/css-loader/src/index.js??xxx/postcss-loader/src/index.js??ruleSet[1].rules[3].oneOf[6].use[2]!./globals.css 模块经过 postcss-loader > css-loader 处理后, 最后还要被 webpack 内置的文件类型 asset/resource 处理(相当于 file-loader ), 最终处理完成它的 module.exports 其实为如下👇
// '!!xxx/css-loader/src/index.js??xxx/postcss-loader/src/index.js??ruleSet[1].rules[3].oneOf[6].use[2]!./globals.css'
module.exports = “/_next/static/media/globals.23a96686.css”
所以被 next-style-loader 处理后的 styles/globals.css 模块最后 append 了一个 url 字符串到了 style 标签中造成了本次 css 样式的异常 !!!
// styles/globals.css
var content = require(’!!xxx/css-loader/src/index.js??xxx/postcss-loader/src/index.js??ruleSet[1].rules[3].oneOf[6].use[2]!./globals.css’)
const style = document.createElement(‘style’);
style.appendChild(document.createTextNode(content));
document.head.append(style)
疑惑解答
为什么 Rule.oneOf 貌似 pick 了多次规则?
对于 styles/globals.css 模块而言只选择了一次即 oneOf 数组索引为 6 的规则, 而经过 next-style-loader 的代理修改后的 require 语句又产生了新的模块 !!xxx/css-loader/src/index.js??xxx/postcss-loader/src/index.js??ruleSet[1].rules[3].oneOf[6].use[2]!./globals.css, 于是新的模块又进行了一次规则 pick, 所以并不算违背了 Rule.oneOf 的定义
即便新的模块又命中了 oneOf 数组索引为 6 的规则, 由于新的模块路径前缀已经包含了 postcss-loader 与 css-loader 也会因为如上图红圈的 if 条件判断不满足而不会被添加重复的 loader
Next.js 的 oneOf 数组索引为 8 的规则真实意图是干什么的?
根据如下 Next.js 对应的代码可知, Next.js 的预期是 issuer 为 *.css 的模块将要被 asset/resource (类似于 file-loader) 给处理, 比如 globals.css 有一行代码为 background-image: url("./xxx.png"), 那么此时 xxx.png 就要被 asset/resource 给处理
// next/dist/build/webpack/config/blocks/css/index.js
markRemovable({
// This should only be applied to CSS files
issuer: regexLikeCss,
// Exclude extensions that webpack handles by default
exclude: [
/.(js|mjs|jsx|ts|tsx)$/,
/.html$/,
/.json$/,
/.webpack[[^]]+]$/,
],
// asset/resource
always emits a URL reference, where asset
// might inline the asset as a data URI
type: ‘asset/resource’
}),
问题解决
Next.js 没想到被 next-style-loader 修改后还存在 .css 文件 require .css 文件的情况, .css 文件被 asset/resource 处理后返回一个 url 就导致了本次的 bug 🐛
其实 .css 中 require 的图片、字体等文件被 asset/resource 处理是符合预期, 如果被 require 的是 .css 文件就得排除, 更多见 pr
issuer: regexLikeCss,
// Exclude extensions that webpack handles by default
exclude: [
+ /\.(css|sass|scss)$/,
/\.(js|mjs|jsx|ts|tsx)$/,
/\.html$/,
/\.json$/,
在排查 Next.js 样式异常时,我们通常会遇到 CSS 加载顺序、CSS-in-JS 库冲突、或服务器与客户端渲染不一致等问题。以下是一个基本的排查步骤和可能的 PR 修复示例,假设问题出在 CSS 加载顺序上。
排查步骤
-
检查 CSS 文件引入顺序: 确保全局样式文件在组件样式之前引入。
-
确认 CSS 隔离: 如果使用 CSS Modules 或 styled-components,确保样式被正确隔离。
-
查看构建日志: 检查
next build
和next start
的构建日志,查找可能的警告或错误。 -
调试工具: 使用 Chrome DevTools 的 Elements 和 Sources 面板,检查实际应用的样式。
示例修复(假设问题出在 CSS 加载顺序)
修改 _app.js
:
// pages/_app.js
import '../styles/globals.css'; // 确保全局样式首先加载
import App from 'next/app';
import { Provider } from 'react-redux';
import store from '../store';
function MyApp({ Component, pageProps }) {
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
);
}
export default MyApp;
PR 修复:
- 在 PR 描述中详细说明问题现象和排查过程。
- 附上修改前后的对比图或代码片段。
- 提醒测试团队验证修复效果,特别是在不同环境下的表现。
通过以上步骤,通常可以定位并解决大多数 Next.js 样式异常问题。