记一次 Next.js 样式异常排查与 pr 修复(涉及 Nodejs 环境)

发布于 1周前 作者 htzhanglong 来自 nodejs/Nestjs

记一次 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$/,

1 回复

在排查 Next.js 样式异常时,我们通常会遇到 CSS 加载顺序、CSS-in-JS 库冲突、或服务器与客户端渲染不一致等问题。以下是一个基本的排查步骤和可能的 PR 修复示例,假设问题出在 CSS 加载顺序上。

排查步骤

  1. 检查 CSS 文件引入顺序: 确保全局样式文件在组件样式之前引入。

  2. 确认 CSS 隔离: 如果使用 CSS Modules 或 styled-components,确保样式被正确隔离。

  3. 查看构建日志: 检查 next buildnext start 的构建日志,查找可能的警告或错误。

  4. 调试工具: 使用 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 样式异常问题。

回到顶部