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

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('', `



<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)
</themecontextstate>

实施主题实用程序

// ./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 
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>
}
</theme>
// ./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="%7BviteLogo%7D" classname="logo" alt="Vite logo">
        </a>
        <a href="https://react.dev" target="_blank" rel="noreferrer">
          <img src="%7BreactLogo%7D" classname="logo react" alt="React logo">
        </a>
      </div>
      <h1 id="Vite-React">Vite + React</h1>
      <card></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}"></app>)
}

添加样式

: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
Python vs. JavaScript:选择合适的工具Python vs. JavaScript:选择合适的工具May 08, 2025 am 12:10 AM

选择Python还是JavaScript取决于项目类型:1)数据科学和自动化任务选择Python;2)前端和全栈开发选择JavaScript。Python因其在数据处理和自动化方面的强大库而备受青睐,而JavaScript则因其在网页交互和全栈开发中的优势而不可或缺。

Python和JavaScript:了解每个的优势Python和JavaScript:了解每个的优势May 06, 2025 am 12:15 AM

Python和JavaScript各有优势,选择取决于项目需求和个人偏好。1.Python易学,语法简洁,适用于数据科学和后端开发,但执行速度较慢。2.JavaScript在前端开发中无处不在,异步编程能力强,Node.js使其适用于全栈开发,但语法可能复杂且易出错。

JavaScript的核心:它是在C还是C上构建的?JavaScript的核心:它是在C还是C上构建的?May 05, 2025 am 12:07 AM

javascriptisnotbuiltoncorc; saninterpretedlanguagethatrunsonenginesoftenwritteninc.1)javascriptwasdesignedAsalightweight,解释edganguageforwebbrowsers.2)Enginesevolvedfromsimpleterterterpretpreterterterpretertestojitcompilerers,典型地提示。

JavaScript应用程序:从前端到后端JavaScript应用程序:从前端到后端May 04, 2025 am 12:12 AM

JavaScript可用于前端和后端开发。前端通过DOM操作增强用户体验,后端通过Node.js处理服务器任务。1.前端示例:改变网页文本内容。2.后端示例:创建Node.js服务器。

Python vs. JavaScript:您应该学到哪种语言?Python vs. JavaScript:您应该学到哪种语言?May 03, 2025 am 12:10 AM

选择Python还是JavaScript应基于职业发展、学习曲线和生态系统:1)职业发展:Python适合数据科学和后端开发,JavaScript适合前端和全栈开发。2)学习曲线:Python语法简洁,适合初学者;JavaScript语法灵活。3)生态系统:Python有丰富的科学计算库,JavaScript有强大的前端框架。

JavaScript框架:为现代网络开发提供动力JavaScript框架:为现代网络开发提供动力May 02, 2025 am 12:04 AM

JavaScript框架的强大之处在于简化开发、提升用户体验和应用性能。选择框架时应考虑:1.项目规模和复杂度,2.团队经验,3.生态系统和社区支持。

JavaScript,C和浏览器之间的关系JavaScript,C和浏览器之间的关系May 01, 2025 am 12:06 AM

引言我知道你可能会觉得奇怪,JavaScript、C 和浏览器之间到底有什么关系?它们之间看似毫无关联,但实际上,它们在现代网络开发中扮演着非常重要的角色。今天我们就来深入探讨一下这三者之间的紧密联系。通过这篇文章,你将了解到JavaScript如何在浏览器中运行,C 在浏览器引擎中的作用,以及它们如何共同推动网页的渲染和交互。JavaScript与浏览器的关系我们都知道,JavaScript是前端开发的核心语言,它直接在浏览器中运行,让网页变得生动有趣。你是否曾经想过,为什么JavaScr

node.js流带打字稿node.js流带打字稿Apr 30, 2025 am 08:22 AM

Node.js擅长于高效I/O,这在很大程度上要归功于流。 流媒体汇总处理数据,避免内存过载 - 大型文件,网络任务和实时应用程序的理想。将流与打字稿的类型安全结合起来创建POWE

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

mPDF

mPDF

mPDF是一个PHP库,可以从UTF-8编码的HTML生成PDF文件。原作者Ian Back编写mPDF以从他的网站上“即时”输出PDF文件,并处理不同的语言。与原始脚本如HTML2FPDF相比,它的速度较慢,并且在使用Unicode字体时生成的文件较大,但支持CSS样式等,并进行了大量增强。支持几乎所有语言,包括RTL(阿拉伯语和希伯来语)和CJK(中日韩)。支持嵌套的块级元素(如P、DIV),

EditPlus 中文破解版

EditPlus 中文破解版

体积小,语法高亮,不支持代码提示功能

SecLists

SecLists

SecLists是最终安全测试人员的伙伴。它是一个包含各种类型列表的集合,这些列表在安全评估过程中经常使用,都在一个地方。SecLists通过方便地提供安全测试人员可能需要的所有列表,帮助提高安全测试的效率和生产力。列表类型包括用户名、密码、URL、模糊测试有效载荷、敏感数据模式、Web shell等等。测试人员只需将此存储库拉到新的测试机上,他就可以访问到所需的每种类型的列表。

SublimeText3 英文版

SublimeText3 英文版

推荐:为Win版本,支持代码提示!

PhpStorm Mac 版本

PhpStorm Mac 版本

最新(2018.2.1 )专业的PHP集成开发工具