coverPiccoverPic

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

问题

因为之前 Next 和 React 接连出现安全问题,于是把博客的依赖升级了一下,没想到就搞出问题了,如下图所示:

初次渲染时样式丢失,在客户端上会短暂展示 Antd 组件无样式界面,出现样式闪烁的情况。项目是 Next 14,React 18 的 App Router 项目,依赖版本:"@ant-design/nextjs-registry": "^1.3.0""antd": "^5.14.2"

解决思路

因为 Antd 是 CSS-in-js 的 UI 库,按照官方文档呢,我们需要一个 @ant-design/nextjs-registry 包裹整个页面,在 SSR 时收集所有组件的样式,并且通过 <script> 标签在客户端首次渲染时带上。

ts
  1. // src/app/layout.tsx
  2. import { AntdRegistry } from '@ant-design/nextjs-registry'
  3. export default async function RootLayout({
  4. children
  5. }: Readonly<{
  6. children: React.ReactNode
  7. }>) {
  8. return (
  9. <html lang="en">
  10. <head>
  11. {/* ... */}
  12. </head>
  13. <body>
  14. <AntdRegistry>
  15. {/* ... 假装这是页面代码 */}
  16. </AntdRegistry>
  17. </body>
  18. </html>
  19. )
  20. }

对照了一下官方文档也问了下 AI,没发现我的写法有什么问题。就在这个时候,我猛然间看见了 Antd 的 Pages Router 使用的注意事项:

我寻思,可能我遇到的情况和这里一样,是内部依赖版本 @ant-design/cssinj 不对引起的。

输入 npm ls @ant-design/cssinjs 看了一下,

text
  1. ├─┬ @ant-design/[email protected]
  2. │ └── @ant-design/[email protected]
  3. └─┬ [email protected]
  4. └── @ant-design/[email protected] deduped

@ant-design/nextjs-registry 内部也使用了 @ant-design/cssinj,而且它的版本和 antd 内置版本还不一样,这就是问题的所在了。

接下来把 @ant-design/nextjs-registry 的版本降到了 1.2.0,这时候版本对上了,bug 也就修复了。

text
  1. ├─┬ @ant-design/[email protected]
  2. │ └── @ant-design/[email protected]
  3. └─┬ [email protected]
  4. └── @ant-design/[email protected] deduped

@ant-design/nextjs-registry 的内部发生了什么

AntdRegistry

这勾起了我的好奇心,就让我们来看看 @ant-design/nextjs-registry 干了些什么:

https://github.com/ant-design/nextjs-registry

tsx
  1. // /src/AntdRegistry.tsx
  2. 'use client';
  3. import type { StyleProviderProps } from '@ant-design/cssinjs';
  4. import type { FC } from 'react';
  5. import { createCache, extractStyle, StyleProvider } from '@ant-design/cssinjs';
  6. import { useServerInsertedHTML } from 'next/navigation';
  7. import React, { useState } from 'react';
  8. type AntdRegistryProps = Omit<StyleProviderProps, 'cache'>;
  9. const AntdRegistry: FC<AntdRegistryProps> = (props) => {
  10. const [cache] = useState(() => createCache());
  11. useServerInsertedHTML(() => {
  12. const styleText = extractStyle(cache, { plain: true, once: true });
  13. if (styleText.includes('.data-ant-cssinjs-cache-path{content:"";}')) {
  14. return null;
  15. }
  16. return (
  17. <style
  18. id="antd-cssinjs"
  19. // to make sure this style is inserted before Ant Design's style generated by client
  20. data-rc-order="prepend"
  21. data-rc-priority="-1000"
  22. dangerouslySetInnerHTML={{ __html: styleText }}
  23. />
  24. );
  25. });
  26. return <StyleProvider {...props} cache={cache} />;
  27. };
  28. export default AntdRegistry;

除了用 Next 的 API useServerInsertedHTML 把样式字符串插到页面上之外,和 Pages Router 中 Antd 收集首屏样式的写法几乎是一样的。

@ant-design/cssinjs

首先来看上文 const [cache] = useState(() => createCache()) 这一行。

@ant-design/cssinjs 部分仓库在 https://github.com/ant-design/cssinjs

它干了几件事:

  1. 生成唯一实例 ID。
  2. (仅客户端)将 body 中的样式移到 head 中,并且去重。
ts
  1. export function createCache() {
  2. const cssinjsInstanceId = Math.random().toString(12).slice(2);
  3. // Tricky SSR: Move all inline style to the head.
  4. // PS: We do not recommend tricky mode.
  5. if (typeof document !== 'undefined' && document.head && document.body) {
  6. const styles = document.body.querySelectorAll(`style[${ATTR_MARK}]`) || [];
  7. const { firstChild } = document.head;
  8. Array.from(styles).forEach((style) => {
  9. (style as any)[CSS_IN_JS_INSTANCE] =
  10. (style as any)[CSS_IN_JS_INSTANCE] || cssinjsInstanceId;
  11. // Not force move if no head
  12. if ((style as any)[CSS_IN_JS_INSTANCE] === cssinjsInstanceId) {
  13. document.head.insertBefore(style, firstChild);
  14. }
  15. });
  16. // Deduplicate of moved styles
  17. const styleHash: Record<string, boolean> = {};
  18. Array.from(document.querySelectorAll(`style[${ATTR_MARK}]`)).forEach(
  19. (style) => {
  20. const hash = style.getAttribute(ATTR_MARK)!;
  21. if (styleHash[hash]) {
  22. if ((style as any)[CSS_IN_JS_INSTANCE] === cssinjsInstanceId) {
  23. style.parentNode?.removeChild(style);
  24. }
  25. } else {
  26. styleHash[hash] = true;
  27. }
  28. },
  29. );
  30. }
  31. return new CacheEntity(cssinjsInstanceId);
  32. }
  1. 返回一个类包裹的 Map 结构,在 StyleProvider 中由后代组件把首屏所需样式传回。结构如下所示:
tsx
  1. export type KeyType = string | number;
  2. type ValueType = [number, any];
  3. /** Connect key with `SPLIT` */
  4. export declare function pathKey(keys: KeyType[]): string;
  5. declare class Entity {
  6. instanceId: string;
  7. constructor(instanceId: string);
  8. /** @private Internal cache map. Do not access this directly */
  9. cache: Map<string, ValueType>;
  10. extracted: Set<string>;
  11. get(keys: KeyType[]): ValueType | null;
  12. /** A fast get cache with `get` concat. */
  13. opGet(keyPathStr: string): ValueType | null;
  14. update(keys: KeyType[], valueFn: (origin: ValueType | null) => ValueType | null): void;
  15. /** A fast get cache with `get` concat. */
  16. opUpdate(keyPathStr: string, valueFn: (origin: ValueType | null) => ValueType | null): void;
  17. }
  18. export default Entity;

至于 StyleProvider,除了整合上层 StyleProvider 注入的样式外,它基本上是一个普通的 Context.Provider,作用也很好猜,把 createCache 返回的 Map 结构注入到下层组件中。

tsx
  1. const StyleContext = React.createContext<StyleContextProps>({
  2. hashPriority: 'low',
  3. cache: createCache(),
  4. defaultCache: true,
  5. autoPrefix: false,
  6. })
  7. export const StyleProvider: React.FC<StyleProviderProps> = (props) => {
  8. // ...
  9. return (
  10. <StyleContext.Provider value={context}>{children}</StyleContext.Provider>
  11. );
  12. };

Antd 组件的调用路径

具体源码就不细看了,以按钮组件 Button 为例,调用路径大致如下:

flowchart TD
    subgraph CSSInJS 底层机制
        genStyleUtils[@ant-design/cssinjs-utils
genStyleUtils] genStyleHooks[@ant-design/cssinjs-utils
genStyleHooks] genComponentStyleHook[@ant-design/cssinjs-utils
genComponentStyleHook] useStyleRegister[@ant-design/cssinjs
useStyleRegister] useGlobalCache[@ant-design/cssinjs
useGlobalCache] end subgraph Antd 组件层 useStyleAntd[useStyle(antd)] Button[Button组件(antd)] JSX[写入到JSX并返回] end genStyleUtils -->|生成| genStyleHooks genStyleHooks -->|调用| genComponentStyleHook genComponentStyleHook -->|调用| useStyleRegister useStyleRegister -->|调用| useGlobalCache genStyleHooks -->|返回| useStyleAntd Button -->|调用| useStyleAntd useStyleAntd -->|样式注入| JSX

在 useGlobalCache 中 调用 React.useContext(StyleContext)cache.onUpdate方法更新缓存。

总结

这次碰到的问题其实挺典型的:升级了依赖,结果页面出问题了。解决方法很简单——把 @ant-design/nextjs-registry 从 1.3.0 降级到 1.2.0,让它跟 antd 用的 @ant-design/cssinjs 内部版本对上就行了。

以后要是用 Next.js App Router 配 Ant Design 遇到类似情况,可以先看看这两个包的版本是不是兼容。有时候问题没看起来那么复杂,可能就是版本没对上。

出于好奇,我还顺便看了一下 AntdRegistry 内部的实现——发现它主要是通过 StyleProvider 在服务端收集样式,然后通过 useServerInsertedHTML 在客户端首次渲染时注入到 style 标签中,这样就能避免样式闪烁的问题。

0 条评论未登录用户
Ctrl or + Enter 评论
© 2023-2025 LittleRangiferTarandus, All rights reserved.
🌸 Run