推诿扯皮 發表於 2025-12-15 17:12:00

Antd 在 Next.js 项目中,初次渲染样式丢失

<h1 id="问题">问题</h1>
<p>因为之前 Next 和 React 接连出现安全问题,于是把博客的依赖升级了一下,没想到就搞出问题了,如下图所示:</p>
<p><img src="https://img2024.cnblogs.com/blog/3550914/202512/3550914-20251215171317499-2092419109.jpg"></p>
<p>初次渲染时样式丢失,在客户端上会短暂展示 Antd 组件无样式界面,出现样式闪烁的情况。项目是 Next 14,React 18 的 App Router 项目,依赖版本:<code>"@ant-design/nextjs-registry": "^1.3.0"</code>,<code>"antd": "^5.14.2"</code>。</p>
<h1 id="解决思路">解决思路</h1>
<p>因为 Antd 是 CSS-in-js 的 UI 库,按照官方文档呢,我们需要一个 @ant-design/nextjs-registry 包裹整个页面,在 SSR 时收集所有组件的样式,并且通过 <code>&lt;script&gt;</code> 标签在客户端首次渲染时带上。</p>
<pre><code class="language-ts">// src/app/layout.tsx

import { AntdRegistry } from '@ant-design/nextjs-registry'

export default async function RootLayout({
children
}: Readonly&lt;{
children: React.ReactNode
}&gt;) {
return (
    &lt;html lang="en"&gt;
      &lt;head&gt;
      {/* ... */}
      &lt;/head&gt;
      &lt;body&gt;
      &lt;AntdRegistry&gt;
          {/* ... 假装这是页面代码 */}
      &lt;/AntdRegistry&gt;
      &lt;/body&gt;
    &lt;/html&gt;
)
}
</code></pre>
<p>对照了一下官方文档也问了下 AI,没发现我的写法有什么问题。就在这个时候,我猛然间看见了 Antd 的 Pages Router 使用的注意事项:</p>
<p><img src="https://img2024.cnblogs.com/blog/3550914/202512/3550914-20251215171333622-1194230188.png"></p>
<p>我寻思,可能我遇到的情况和这里一样,是内部依赖版本 <code>@ant-design/cssinj</code> 不对引起的。</p>
<p>输入 <code>npm ls @ant-design/cssinjs</code> 看了一下,</p>
<pre><code class="language-text">├─┬ @ant-design/nextjs-registry@1.3.0
│ └── @ant-design/cssinjs@2.0.1
└─┬ antd@5.14.2
└── @ant-design/cssinjs@1.24.0 deduped
</code></pre>
<p><code>@ant-design/nextjs-registry</code> 内部也使用了 <code>@ant-design/cssinjs</code>,而且它的版本和 <code>antd</code> 内置版本还不一样,这就是问题的所在了。</p>
<p>接下来把 <code>@ant-design/nextjs-registry</code> 的版本降到了 1.2.0,这时候版本对上了,bug 也就修复了。</p>
<pre><code class="language-text">├─┬ @ant-design/nextjs-registry@1.2.0
│ └── @ant-design/cssinjs@1.24.0
└─┬ antd@5.14.2
└── @ant-design/cssinjs@1.24.0 deduped
</code></pre>
<h1 id="ant-designnextjs-registry-的内部发生了什么">@ant-design/nextjs-registry 的内部发生了什么</h1>
<h2 id="antdregistry">AntdRegistry</h2>
<p>这勾起了我的好奇心,就让我们来看看 <code>@ant-design/nextjs-registry</code> 干了些什么:</p>
<p>https://github.com/ant-design/nextjs-registry</p>
<pre><code class="language-tsx">// /src/AntdRegistry.tsx
'use client';

import type { StyleProviderProps } from '@ant-design/cssinjs';
import type { FC } from 'react';
import { createCache, extractStyle, StyleProvider } from '@ant-design/cssinjs';
import { useServerInsertedHTML } from 'next/navigation';
import React, { useState } from 'react';

type AntdRegistryProps = Omit&lt;StyleProviderProps, 'cache'&gt;;

const AntdRegistry: FC&lt;AntdRegistryProps&gt; = (props) =&gt; {
const = useState(() =&gt; createCache());

useServerInsertedHTML(() =&gt; {
    const styleText = extractStyle(cache, { plain: true, once: true });

    if (styleText.includes('.data-ant-cssinjs-cache-path{content:"";}')) {
      return null;
    }

    return (
      &lt;style
      id="antd-cssinjs"
      // to make sure this style is inserted before Ant Design's style generated by client
      data-rc-order="prepend"
      data-rc-priority="-1000"
      dangerouslySetInnerHTML={{ __html: styleText }}
      /&gt;
    );
});

return &lt;StyleProvider {...props} cache={cache} /&gt;;
};

export default AntdRegistry;
</code></pre>
<p>除了用 Next 的 API <code>useServerInsertedHTML</code> 把样式字符串插到页面上之外,和 Pages Router 中 Antd 收集首屏样式的写法几乎是一样的。</p>
<h2 id="ant-designcssinjs">@ant-design/cssinjs</h2>
<p>首先来看上文 <code>const = useState(() =&gt; createCache())</code> 这一行。</p>
<p>@ant-design/cssinjs 部分仓库在 https://github.com/ant-design/cssinjs</p>
<p>它干了几件事:</p>
<ol>
<li>生成唯一实例 ID。</li>
<li>(仅客户端)将 body 中的样式移到 head 中,并且去重。</li>
</ol>
<pre><code class="language-ts">export function createCache() {
const cssinjsInstanceId = Math.random().toString(12).slice(2);

// Tricky SSR: Move all inline style to the head.
// PS: We do not recommend tricky mode.
if (typeof document !== 'undefined' &amp;&amp; document.head &amp;&amp; document.body) {
    const styles = document.body.querySelectorAll(`style[${ATTR_MARK}]`) || [];
    const { firstChild } = document.head;

    Array.from(styles).forEach((style) =&gt; {
      (style as any) =
      (style as any) || cssinjsInstanceId;

      // Not force move if no head
      if ((style as any) === cssinjsInstanceId) {
      document.head.insertBefore(style, firstChild);
      }
    });

    // Deduplicate of moved styles
    const styleHash: Record&lt;string, boolean&gt; = {};
    Array.from(document.querySelectorAll(`style[${ATTR_MARK}]`)).forEach(
      (style) =&gt; {
      const hash = style.getAttribute(ATTR_MARK)!;
      if (styleHash) {
          if ((style as any) === cssinjsInstanceId) {
            style.parentNode?.removeChild(style);
          }
      } else {
          styleHash = true;
      }
      },
    );
}

return new CacheEntity(cssinjsInstanceId);
}
</code></pre>
<ol start="3">
<li>返回一个类包裹的 <code>Map</code> 结构,在 <code>StyleProvider</code> 中由后代组件把首屏所需样式传回。结构如下所示:</li>
</ol>
<pre><code class="language-tsx">export type KeyType = string | number;
type ValueType = ;
/** Connect key with `SPLIT` */
export declare function pathKey(keys: KeyType[]): string;
declare class Entity {
    instanceId: string;
    constructor(instanceId: string);
    /** @private Internal cache map. Do not access this directly */
    cache: Map&lt;string, ValueType&gt;;
    extracted: Set&lt;string&gt;;
    get(keys: KeyType[]): ValueType | null;
    /** A fast get cache with `get` concat. */
    opGet(keyPathStr: string): ValueType | null;
    update(keys: KeyType[], valueFn: (origin: ValueType | null) =&gt; ValueType | null): void;
    /** A fast get cache with `get` concat. */
    opUpdate(keyPathStr: string, valueFn: (origin: ValueType | null) =&gt; ValueType | null): void;
}
export default Entity;
</code></pre>
<p>至于 <code>StyleProvider</code>,除了整合上层 <code>StyleProvider</code> 注入的样式外,它基本上是一个普通的 <code>Context.Provider</code>,作用也很好猜,把 <code>createCache</code> 返回的 <code>Map</code> 结构注入到下层组件中。</p>
<pre><code class="language-tsx">const StyleContext = React.createContext&lt;StyleContextProps&gt;({
hashPriority: 'low',
cache: createCache(),
defaultCache: true,
autoPrefix: false,
})

export const StyleProvider: React.FC&lt;StyleProviderProps&gt; = (props) =&gt; {
// ...
return (
    &lt;StyleContext.Provider value={context}&gt;{children}&lt;/StyleContext.Provider&gt;
);
};

</code></pre>
<h2 id="antd-组件的调用路径">Antd 组件的调用路径</h2>
<p>具体源码就不细看了,以按钮组件 Button 为例,调用路径大致如下:</p>
<div class="mermaid">flowchart TD
    subgraph CSSInJS 底层机制
      genStyleUtils["@ant-design/cssinjs-utils&lt;br/&gt;genStyleUtils"]
      genStyleHooks["@ant-design/cssinjs-utils&lt;br/&gt;genStyleHooks"]
      genComponentStyleHook["@ant-design/cssinjs-utils&lt;br/&gt;genComponentStyleHook"]
      useStyleRegister["@ant-design/cssinjs&lt;br/&gt;useStyleRegister"]
      useGlobalCache["@ant-design/cssinjs&lt;br/&gt;useGlobalCache"]
    end

    subgraph Antd 组件层
      useStyleAntd
      Button
      JSX[写入到JSX并返回]
    end

    genStyleUtils --&gt;|生成| genStyleHooks
    genStyleHooks --&gt;|调用| genComponentStyleHook
    genComponentStyleHook --&gt;|调用| useStyleRegister
    useStyleRegister --&gt;|调用| useGlobalCache
   
    genStyleHooks --&gt;|返回| useStyleAntd
   
    Button --&gt;|调用| useStyleAntd
    useStyleAntd --&gt;|样式注入| JSX
</div><p>在 useGlobalCache 中 调用 <code>React.useContext(StyleContext)</code> 的 <code>cache.onUpdate</code>方法更新缓存。</p>
<h1 id="总结">总结</h1>
<p>这次碰到的问题其实挺典型的:升级了依赖,结果页面出问题了。解决方法很简单——把 @ant-design/nextjs-registry 从 1.3.0 降级到 1.2.0,让它跟 antd 用的 @ant-design/cssinjs 内部版本对上就行了。</p>
<p>以后要是用 Next.js App Router 配 Ant Design 遇到类似情况,可以先看看这两个包的版本是不是兼容。有时候问题没看起来那么复杂,可能就是版本没对上。</p>
<p>出于好奇,我还顺便看了一下 AntdRegistry 内部的实现——发现它主要是通过 <code>StyleProvider</code> 在服务端收集样式,然后通过 <code>useServerInsertedHTML</code> 在客户端首次渲染时注入到 <code>style</code> 标签中,这样就能避免样式闪烁的问题。</p>
<blockquote>
<p>大家的阅读是我发帖的动力,本文首发于我的博客:deer.shika-blog.xyz,欢迎大家来玩<s>喵</s>, 转载请注明出处。</p>
</blockquote><br><br>
来源:https://www.cnblogs.com/deerblog/p/19353638
頁: [1]
查看完整版本: Antd 在 Next.js 项目中,初次渲染样式丢失