在 SSR React 应用程序中设置主题
想象一下,访问一个可以无缝适应您的偏好的网站,轻松在浅色、深色和基于系统的主题之间切换。
本文是我关于使用 React 进行 SSR 的系列文章的继续。在基础知识文章中,我们探索了生产就绪的配置,而在先进技术中,我们解决了水合错误等挑战。现在,我们将更进一步,实施与 SSR 无缝集成的强大主题支持。
目录
- 主题和SSR
-
执行
- 安装依赖项
- 将 cookie 添加到服务器构建
- 在服务器上应用主题
- 处理客户端上的主题
- 结论
主题和 SSR
主要问题是初始闪现不正确的主题(FOIT)。
本质上,主题只是改变 CSS 变量。在大多数情况下,您将使用三个主题:
- Light:默认的 CSS 变量集。
- 深色:当 时应用。标签的类别为 dark。
- 系统:根据用户的系统偏好自动切换,使用(prefers-color-scheme:dark) 媒体查询以确定主题应该是深色还是浅色。
默认情况下,服务器会渲染浅色主题的 HTML 并将其发送到浏览器。如果用户更喜欢深色主题,他们会在第一页加载时看到明显的主题更改,这会破坏用户体验。
解决这个问题主要有两种方法:
- 添加一个在服务器上的 HTML 中标记并在客户端动态设置类。
- 使用cookie来存储用户的主题偏好并在服务器上设置类。
第一个解决方案是下一个主题包的工作原理(2025 年 1 月)。在本文中,您将实现基于 cookie 的方法,以确保 SSR 应用程序中的无缝主题处理。
执行
要实现主题,您将使用两个 cookie:
- serverTheme - 用于将正确的类应用于 标签。
- 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中文网其他相关文章!

热AI工具

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

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

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

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

热门文章

热工具

记事本++7.3.1
好用且免费的代码编辑器

SublimeText3汉化版
中文版,非常好用

禅工作室 13.0.1
功能强大的PHP集成开发环境

Dreamweaver CS6
视觉化网页开发工具

SublimeText3 Mac版
神级代码编辑软件(SublimeText3)

JavaScript是现代Web开发的基石,它的主要功能包括事件驱动编程、动态内容生成和异步编程。1)事件驱动编程允许网页根据用户操作动态变化。2)动态内容生成使得页面内容可以根据条件调整。3)异步编程确保用户界面不被阻塞。JavaScript广泛应用于网页交互、单页面应用和服务器端开发,极大地提升了用户体验和跨平台开发的灵活性。

Python和JavaScript开发者的薪资没有绝对的高低,具体取决于技能和行业需求。1.Python在数据科学和机器学习领域可能薪资更高。2.JavaScript在前端和全栈开发中需求大,薪资也可观。3.影响因素包括经验、地理位置、公司规模和特定技能。

学习JavaScript不难,但有挑战。1)理解基础概念如变量、数据类型、函数等。2)掌握异步编程,通过事件循环实现。3)使用DOM操作和Promise处理异步请求。4)避免常见错误,使用调试技巧。5)优化性能,遵循最佳实践。

实现视差滚动和元素动画效果的探讨本文将探讨如何实现类似资生堂官网(https://www.shiseido.co.jp/sb/wonderland/)中�...

JavaScript的最新趋势包括TypeScript的崛起、现代框架和库的流行以及WebAssembly的应用。未来前景涵盖更强大的类型系统、服务器端JavaScript的发展、人工智能和机器学习的扩展以及物联网和边缘计算的潜力。

如何在JavaScript中将具有相同ID的数组元素合并到一个对象中?在处理数据时,我们常常会遇到需要将具有相同ID�...

深入探讨console.log输出差异的根源本文将分析一段代码中console.log函数输出结果的差异,并解释其背后的原因。�...
