Rumah >hujung hadapan web >tutorial js >Menyediakan Tema dalam Aplikasi SSR React
Bayangkan melawati tapak web yang menyesuaikan dengan pilihan anda dengan lancar—bertukar antara tema terang, gelap dan berasaskan sistem dengan lancar.
Artikel ini meneruskan siri saya tentang SSR dengan React. Dalam artikel asas, kami meneroka konfigurasi sedia pengeluaran, manakala dalam teknik lanjutan, kami menangani cabaran seperti ralat penghidratan. Kini, kami akan melangkah lebih jauh dengan melaksanakan sokongan tema teguh yang disepadukan dengan lancar dengan SSR.
Isu utama ialah Denyar Awal Tema Salah (FOIT).
Pada asasnya, tema hanyalah tentang menukar pembolehubah CSS. Dalam kebanyakan kes, anda akan menggunakan tiga tema:
Secara lalai, pelayan akan memaparkan HTML dengan tema ringan dan menghantarnya ke penyemak imbas. Jika pengguna lebih suka tema gelap, mereka akan melihat perubahan tema yang boleh dilihat pada pemuatan halaman pertama, yang mengganggu pengalaman pengguna.
Terdapat dua cara utama untuk menyelesaikan isu ini:
Penyelesaian pertama ialah cara pakej tema seterusnya berfungsi (Jan 2025). Dalam artikel ini, anda akan melaksanakan pendekatan berasaskan kuki untuk memastikan pengendalian tema yang lancar dalam aplikasi SSR anda.
Untuk melaksanakan tema, anda akan menggunakan dua kuki:
Pelanggan sentiasa menetapkan kedua-dua kuki, memastikan pelayan dapat memaparkan tema yang sesuai dengan betul pada permintaan seterusnya.
Panduan ini dibina berdasarkan konsep yang diperkenalkan dalam artikel sebelumnya, Membina Aplikasi Reaksi SSR Sedia Pengeluaran, yang boleh anda temui dipautkan di bahagian bawah. Untuk kesederhanaan, pemalar dan jenis yang dikongsi tidak dibuat di sini, tetapi anda boleh menemui pelaksanaannya dalam repositori contoh.
Pasang pakej yang diperlukan untuk pengendalian kuki:
pnpm add cookie js-cookie
Jenis pemasangan untuk js-cookie:
pnpm add -D @types/js-cookie
Jika anda tidak menggunakan penghala tindak balas dalam apl anda, anda boleh menggunakan pakej kuki sebagai devDependencies.
Kemas kini fail konfigurasi tsup anda:
// ./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' }
Buat fungsi utiliti untuk menggunakan kelas tema yang betul pada tag berdasarkan kuki Tema pelayan:
// ./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 }
Tatarajah Pembangunan:
// ./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) } }) }
Konfigurasi Pengeluaran:
// ./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) } }) }
Pemalar pendua untuk kegunaan pelanggan atau alihkannya ke folder kongsi
// ./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', }
Sediakan konteks React untuk mengurus keadaan tema dan menyediakan kaedah pengurusan tema:
// ./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
Lepaskan tema kepada kaedah pemaparan pelayan untuk memastikan HTML yang dijana pelayan sepadan dengan pemaparan sebelah klien:
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; }
Dalam artikel ini, kami menangani cabaran untuk melaksanakan tema lancar dalam aplikasi SSR React. Dengan menggunakan kuki dan menyepadukan kedua-dua logik sisi klien dan sisi pelayan, kami mencipta sistem yang teguh yang menyokong tema terang, gelap dan berasaskan sistem tanpa ralat penghidratan atau gangguan pengalaman pengguna.
Ini adalah sebahagian daripada siri saya tentang SSR dengan React. Nantikan lebih banyak artikel!
Saya sentiasa terbuka untuk maklum balas, kerjasama atau membincangkan idea teknologi — jangan ragu untuk menghubungi kami!
Atas ialah kandungan terperinci Menyediakan Tema dalam Aplikasi SSR React. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!