首页 >web前端 >js教程 >在 SSR React 应用程序中设置主题

在 SSR React 应用程序中设置主题

Patricia Arquette
Patricia Arquette原创
2025-01-06 00:56:38580浏览

Setting Up Themes in SSR React Applications

想象一下,访问一个可以无缝适应您的偏好的网站,轻松在浅色、深色和基于系统的主题之间切换。

本文是我关于使用 React 进行 SSR 的系列文章的继续。在基础知识文章中,我们探索了生产就绪的配置,而在先进技术中,我们解决了水合错误等挑战。现在,我们将更进一步,实施与 SSR 无缝集成的强大主题支持。

目录

  • 主题和SSR
  • 执行
    • 安装依赖项
    • 将 cookie 添加到服务器构建
    • 在服务器上应用主题
    • 处理客户端上的主题
  • 结论

主题和 SSR

主要问题是初始闪现不正确的主题(FOIT)

本质上,主题只是改变 CSS 变量。在大多数情况下,您将使用三个主题:

  • Light:默认的 CSS 变量集。
  • 深色:当 时应用。标签的类别为 dark。
  • 系统:根据用户的系统偏好自动切换,使用(prefers-color-scheme:dark) 媒体查询以确定主题应该是深色还是浅色。

默认情况下,服务器会渲染浅色主题的 HTML 并将其发送到浏览器。如果用户更喜欢深色主题,他们会在第一页加载时看到明显的主题更改,这会破坏用户体验。

解决这个问题主要有两种方法:

  • 添加一个在服务器上的 HTML 中标记并在客户端动态设置类。
  • 使用cookie来存储用户的主题偏好并在服务器上设置类。

第一个解决方案是下一个主题包的工作原理(2025 年 1 月)。在本文中,您将实现基于 cookie 的方法,以确保 SSR 应用程序中的无缝主题处理。

执行

要实现主题,您将使用两个 cookie:

  1. serverTheme - 用于将正确的类应用于 标签。
  2. clientTheme - 用于处理水合错误。

客户端始终设置这两个 cookie,确保服务器可以在下一个请求时正确呈现适当的主题。

本指南基于上一篇文章中介绍的概念,构建生产就绪的 SSR React 应用程序,您可以在底部找到链接。为简单起见,此处未创建共享常量和类型,但您可以在示例存储库中找到它们的实现。

安装依赖项

安装 cookie 处理所需的软件包:

pnpm add cookie js-cookie

js-cookie 的安装类型:

pnpm add -D @types/js-cookie

如果你的应用中没有使用react-router,你可以使用cookie包作为devDependency。

将 cookie 添加到服务器构建中

更新你的 tsup 配置文件:

// ./tsup.config.ts
import { defineConfig } from 'tsup'

export default defineConfig({
  entry: ['server'],
  outDir: 'dist/server',
  target: 'node22',
  format: ['cjs'],
  clean: true,
  minify: true,
  external: ['lightningcss', 'esbuild', 'vite'],
  noExternal: [
    'express',
    'sirv',
    'compression',
    'cookie', // Include the cookie in the server build
  ],
})

在服务器上应用主题

定义主题常量

// ./server/constants.ts
export const CLIENT_THEME_COOKIE_KEY = 'clientTheme'
export const SERVER_THEME_COOKIE_KEY = 'serverTheme'

export enum Theme {
  Light = 'light',
  Dark = 'dark',
  System = 'system'
}

将主题类应用到标签

创建一个实用函数以将正确的主题类应用到 中基于服务器主题 cookie 的标签:

// ./server/lib/applyServerTheme.ts
import { parse } from 'cookie'
import { Request } from 'express'
import { SERVER_THEME_COOKIE_KEY, Theme } from '../constants'

export function applyServerTheme(req: Request, html: string): string {
  const cookies = parse(req.headers.cookie || '')
  const theme = cookies?.[SERVER_THEME_COOKIE_KEY]

  if (theme === Theme.Dark) {
    return html.replace('<html lang="en">', `<html lang="en">



<h4>
  
  
  Retrieve the Client Theme Cookie
</h4>

<p>Create a utility function to retrieve the clientTheme cookie<br>
</p>

<pre class="brush:php;toolbar:false">// ./server/getClientTheme.ts
import { parse } from 'cookie'
import { Request } from 'express'
import { CLIENT_THEME_COOKIE_KEY, Theme } from '../constants'

export function getClientTheme(req: Request) {
  const cookies = parse(req.headers.cookie || '')

  return cookies?.[CLIENT_THEME_COOKIE_KEY] as Theme | undefined
}

更新主题的服务器配置

开发配置:

// ./server/dev.ts
import fs from 'fs'
import path from 'path'
import { Application } from 'express'
import { HTML_KEY } from './constants'
import { applyServerTheme } from './lib/applyServerTheme'
import { getClientTheme } from './lib/getClientTheme'

const HTML_PATH = path.resolve(process.cwd(), 'index.html')
const ENTRY_SERVER_PATH = path.resolve(process.cwd(), 'src/entry-server.tsx')

export async function setupDev(app: Application) {
  const vite = await (
    await import('vite')
  ).createServer({
    root: process.cwd(),
    server: { middlewareMode: true },
    appType: 'custom',
  })

  app.use(vite.middlewares)

  app.get('*', async (req, res, next) => {
    try {
      let html = fs.readFileSync(HTML_PATH, 'utf-8')
      html = await vite.transformIndexHtml(req.originalUrl, html)

      const { render } = await vite.ssrLoadModule(ENTRY_SERVER_PATH)
      // send Client Theme from cookie to render
      const appHtml = await render(getClientTheme(req))

      // Apply Server theme on template html
      html = applyServerTheme(req, html)
      html = html.replace(HTML_KEY, appHtml)

      res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
    } catch (e) {
      vite.ssrFixStacktrace(e as Error)
      console.error((e as Error).stack)
      next(e)
    }
  })
}

生产配置:

// ./server/prod.ts
import fs from 'fs'
import path from 'path'
import compression from 'compression'
import { Application } from 'express'
import sirv from 'sirv'
import { HTML_KEY } from './constants'
import { applyServerTheme } from './lib/applyServerTheme'
import { getClientTheme } from './lib/getClientTheme'

const CLIENT_PATH = path.resolve(process.cwd(), 'dist/client')
const HTML_PATH = path.resolve(process.cwd(), 'dist/client/index.html')
const ENTRY_SERVER_PATH = path.resolve(process.cwd(), 'dist/ssr/entry-server.js')

export async function setupProd(app: Application) {
  app.use(compression())
  app.use(sirv(CLIENT_PATH, { extensions: [] }))

  app.get('*', async (req, res, next) => {
    try {
      let html = fs.readFileSync(HTML_PATH, 'utf-8')

      const { render } = await import(ENTRY_SERVER_PATH)
      // send Client Theme from cookie to render
      const appHtml = await render(getClientTheme(req))

      // Apply Server theme on template html
      html = applyServerTheme(req, html)
      html = html.replace(HTML_KEY, appHtml)

      res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
    } catch (e) {
      console.error((e as Error).stack)
      next(e)
    }
  })
}

处理客户端上的主题

定义常量

复制常量供客户端使用或将它们移动到共享文件夹

// ./src/constants.ts
export const SSR = import.meta.env.SSR

export const CLIENT_THEME_COOKIE_KEY = 'clientTheme'
export const SERVER_THEME_COOKIE_KEY = 'serverTheme'

export enum Theme {
  Light = 'light',
  Dark = 'dark',
  System = 'system',
}

创建主题上下文

设置React上下文来管理主题状态并提供主题管理方法:

// ./src/theme/context.ts
import { createContext, useContext } from 'react'
import { Theme } from '../constants'

export type ThemeContextState = {
  theme: Theme
  setTheme: (theme: Theme) => void
}

export const ThemeContext = createContext<ThemeContextState>({
  theme: Theme.System,
  setTheme: () => null,
})

export const useThemeContext = () => useContext(ThemeContext)

实施主题实用程序

// ./src/theme/lib.ts
import Cookies from 'js-cookie'
import { CLIENT_THEME_COOKIE_KEY, SERVER_THEME_COOKIE_KEY, SSR, Theme } from '../constants'

// Resolve the system theme using the `prefers-color-scheme` media query
export function resolveSystemTheme() {
  if (SSR) return Theme.Light
  return window.matchMedia('(prefers-color-scheme: dark)').matches ? Theme.Dark : Theme.Light
}

// Update the theme cookies and set appropriate class to <html>
export function updateTheme(theme: Theme) {
  if (SSR) return

  const resolvedTheme = theme === Theme.System ? resolveSystemTheme() : theme

  Cookies.set(CLIENT_THEME_COOKIE_KEY, theme)
  Cookies.set(SERVER_THEME_COOKIE_KEY, resolvedTheme)

  window.document.documentElement.classList.toggle('dark', resolvedTheme === Theme.Dark)
}

// Get the default theme from cookies
export function getDefaultTheme(): Theme {
  if (SSR) return Theme.System
  const theme = (Cookies.get(CLIENT_THEME_COOKIE_KEY) as Theme) || Theme.System

  updateTheme(theme)
  return theme
}

创建主题提供者

// ./src/theme/Provider.tsx
import { PropsWithChildren, useState } from 'react'
import { Theme } from '../constants'
import { ThemeContext } from './context'
import { getDefaultTheme, updateTheme } from './lib'

type Props = PropsWithChildren & {
  defaultTheme?: Theme // Handle theme for SSR
}

export function ThemeProvider({ children, defaultTheme }: Props) {
  const [theme, setTheme] = useState<Theme>(defaultTheme || getDefaultTheme())

  const handleSetTheme = (theme: Theme) => {
    setTheme(theme)
    updateTheme(theme)
  }

  return <ThemeContext value={{ theme, setTheme: handleSetTheme }}>{children}</ThemeContext>
}
// ./src/theme/index.ts
export { ThemeProvider } from './Provider'
export { useThemeContext } from './context'

在组件中使用主题上下文

// ./src/App.tsx
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import Card from './Card'
import { Theme } from './constants'
import { ThemeProvider } from './theme'

import './App.css'

// Theme from Server Entry
type AppProps = {
  theme?: Theme
}

function App({ theme }: AppProps) {
  return (
    <ThemeProvider defaultTheme={theme}>
      <div>
        <a href="https://vite.dev" target="_blank" rel="noreferrer">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev" target="_blank" rel="noreferrer">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <Card />
      <p className="read-the-docs">Click on the Vite and React logos to learn more</p>
    </ThemeProvider>
  )
}

export default App

创建卡片组件

// ./src/Card.tsx
import { useState } from 'react'
import { Theme } from './constants'
import { useThemeContext } from './theme'

function Card() {
  const { theme, setTheme } = useThemeContext()
  const [count, setCount] = useState(0)

  return (
    <div className='card'>
      <button onClick={() => setCount((count) => count + 1)}>count is {count}</button>
      <p>
        Edit <code>src/App.tsx</code> and save to test HMR
      </p>
      <div>
        Themes:{' '}
        <select value={theme} onChange={(event) => setTheme(event.target.value as Theme)}>
          <option value={Theme.System}>System</option>
          <option value={Theme.Light}>Light</option>
          <option value={Theme.Dark}>Dark</option>
        </select>
      </div>
    </div>
  )
}

export default Card

解决水合错误

将主题传递给服务器渲染方法,以确保服务器生成的 HTML 与客户端渲染匹配:

import { renderToString } from 'react-dom/server'
import App from './App'
import { Theme } from './constants'

export function render(theme: Theme) {
  return renderToString(<App theme={theme} />)
}

添加样式

:root {
    color: #242424;
    background-color: rgba(255, 255, 255, 0.87);
}

:root.dark {
    color: rgba(255, 255, 255, 0.87);
    background-color: #242424;
}

结论

在本文中,我们解决了在 SSR React 应用程序中实现无缝主题的挑战。通过使用 cookie 并集成客户端和服务器端逻辑,我们创建了一个强大的系统,支持浅色、深色和基于系统的主题,而不会出现水合作用错误或用户体验中断。

探索代码

  • 示例:react-ssr-themes-example
  • 以SSR落地:专业落地

相关文章

这是我的 React SSR 系列的一部分。更多文章敬请期待!

  • 构建生产就绪的 SSR React 应用程序
  • 使用流和动态数据的高级 React SSR 技术
  • 在 SSR React 应用程序中设置主题

保持联系

我始终乐于接受反馈、合作或讨论技术想法 - 请随时与我们联系!

  • 投资组合:maxh1t.xyz
  • 电子邮件:m4xh17@gmail.com

以上是在 SSR React 应用程序中设置主题的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn