NextJS App Router 使用antd最佳实践(组件服务端渲染、主题切换等)

1,351 阅读2分钟

解决问题

  • Next Server Component使用andt组件
  • antd主题切换
  • antd初次渲染样式未注入导致的闪烁问题

code

github.com/huangyangte…

可以直接下载代码跑一下,然后直接看代码实现。

依赖

"@ant-design/nextjs-registry": "^1.0.1",
"ant-design": "^1.0.0",
"antd": "^5.21.1",
"cookies-next": "^4.2.1",
"next": "14.2.13",
"next-themes": "^0.3.0",

概述

防止样式闪烁

样式闪烁是因为antd的样式加载较慢,解决方式是提前在server端注入样式。

使用库:@ant-design/nextjs-registry,使用AntdRegistry包裹一下组件即可。

usage

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

<AntdRegistry>{children</AntdRegistry>

主题切换

  • 使用next-themes库实现主题切换
  • 为了防止水合问题,需要把theme放在cookie里面,这样在server端可以取到

主题切换包括两部分,自定义的主题样式和UI库的主题样式。

自定义的主题样式使用CSS变量,根据不同的主题使用不同的变量。

亮色和暗色的变量定义如下:

:root {
    --background: #ffffff;
    --foreground: #171717;
}

[data-theme='dark'] {
    --background: #0a0a0a;
    --foreground: #ededed;
}

ant design v5提供了简单的方式设置主题,即修改ConfigProvider的theme属性

  • defaultAlgorithm 亮色主题
  • darkAlgorithm 暗色主题
import { ConfigProvider, theme } from 'antd'

function AntDesignProvider({ defaultTheme, children }: Props) {
    const { defaultAlgorithm, darkAlgorithm } = theme
    const { theme: currentTheme = defaultTheme } = useTheme()

    return (
        <ConfigProvider
            theme={{
                algorithm:
                    currentTheme === 'light' ? defaultAlgorithm : darkAlgorithm
            }}
        >
            {children}
        </ConfigProvider>
    )
}

代码实现

App Router 主题切换的逻辑要写在layout.tsx中

  • layout.tsx
    • 这里需要注意我们首先从cookie中取主题,因为cookie在server端就可以拿到,保证antd不会因为在server端渲染拿不到theme出现水合错误

import type { Metadata } from 'next'
import './globals.css'
import { cookies } from 'next/headers'


export const metadata: Metadata = {
    title: 'Create Next App',
    description: 'Generated by create next app'
}
import Providers from './Providers'

export default function RootLayout({
    children
}: Readonly<{
    children: React.ReactNode
}>) {
    const cookieStore=cookies()
    const defaultTheme=cookieStore.get('theme')?.value||'dark'
    console.log('🐔🐔🐔',defaultTheme)
    return (
        <html lang="en" suppressHydrationWarning={true}>
            <body>
                <Providers defaultTheme={defaultTheme}>{children}</Providers>
            </body>
        </html>
    )
}


  • Providers.tsx
    • 作用:切换主题库的样式
    • 在server端,从cookie中取theme
    • 在client端,从next-themes中取theme(其实是在localStrage中)
'use client'

import { ReactNode } from 'react'
import { ThemeProvider } from 'next-themes'
import { AntdRegistry } from '@ant-design/nextjs-registry'
import { ConfigProvider, theme } from 'antd'
import { useTheme } from 'next-themes'
interface Props {
    defaultTheme: string
    children: ReactNode
}
export default function Providers({ defaultTheme, children }: Props) {
    return (
        <AntdRegistry>
            <ThemeProvider>
                <AntDesignProvider defaultTheme={defaultTheme}>
                    {children}
                </AntDesignProvider>
            </ThemeProvider>
        </AntdRegistry>
    )
}

function AntDesignProvider({ defaultTheme, children }: Props) {
    const { defaultAlgorithm, darkAlgorithm } = theme
    const { theme: currentTheme = defaultTheme } = useTheme()

    return (
        <ConfigProvider
            theme={{
                algorithm:
                    currentTheme === 'light' ? defaultAlgorithm : darkAlgorithm
            }}
        >
            {children}
        </ConfigProvider>
    )
}

  • 主题切换组件 ThemeSwitch.tsx
    • 注意该组件只能在客户端运行,因为next-themes基于localStrage,在server端取不到theme
'use client'
import { useState, useEffect } from 'react'
import { useTheme } from 'next-themes'
import { setCookie } from 'cookies-next'

const ThemeSwitch = () => {
    const [mounted, setMounted] = useState(false)
    const { theme, setTheme } = useTheme()
    // useEffect only runs on the client, so now we can safely show the UI
    useEffect(() => {
        setMounted(true)
    }, [])

    if (!mounted) {
        return null
    }
    const handleSetTheme = (theme: string) => {
        setTheme(theme)
        setCookie('theme', theme)
    }

    return (
        <select value={theme} onChange={(e) => handleSetTheme(e.target.value)}>
            <option value="system">System</option>
            <option value="dark">Dark</option>
            <option value="light">Light</option>
        </select>
    )
}
export default ThemeSwitch

注意事项

需要给html标签添加suppressHydrationWarning属性,不然控制台会有警告

 <html lang="en" suppressHydrationWarning={true}>