Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

记一次 Next.js 样式异常的排查与 pr 修复 #46

Open
xiaoxiaojx opened this issue Nov 1, 2022 · 1 comment
Open

记一次 Next.js 样式异常的排查与 pr 修复 #46

xiaoxiaojx opened this issue Nov 1, 2022 · 1 comment
Labels
DEBUG debug webpack A bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting a

Comments

@xiaoxiaojx
Copy link
Owner

xiaoxiaojx commented Nov 1, 2022

问题简述


开发同学反馈 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 来依次处理, 看上去没有任何问题

image

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 的规则
image

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 字段
image

而 !!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 的定义
image

即便新的模块又命中了 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 文件就得排除, 更多见next.js/pull/42283webpack/pull/16477

            issuer: regexLikeCss,
            // Exclude extensions that webpack handles by default
            exclude: [
+              /\.(css|sass|scss)$/,
              /\.(js|mjs|jsx|ts|tsx)$/,
              /\.html$/,
              /\.json$/,
@xiaoxiaojx xiaoxiaojx added the DEBUG debug label Nov 1, 2022
@xiaoxiaojx xiaoxiaojx added the webpack A bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting a label Nov 30, 2022
@xiaoxiaojx
Copy link
Owner Author

xiaoxiaojx commented Apr 25, 2023

webpack/pull/16477 已经合并,发布于 webpack@5.78.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
DEBUG debug webpack A bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting a
Projects
None yet
Development

No branches or pull requests

1 participant