ミリ秒単位が重要な世界では、サーバー側のレンダリングはフロントエンド アプリケーションにとって不可欠な機能となっています。
このガイドでは、React を使用して本番環境に対応した SSR を構築するための基本的なパターンを説明します。 SSR (Next.js など) が組み込まれた React ベースのフレームワークの背後にある原則を理解し、独自のカスタム ソリューションを作成する方法を学びます。
提供されたコードは本番環境に対応しており、Dockerfile を含むクライアント部分とサーバー部分の両方の完全なビルド プロセスを備えています。この実装では、Vite を使用してクライアントと SSR コードを構築しますが、任意の他のツールを使用することもできます。 Vite は、クライアントの開発モード中にホットリロードも行います。
Vite を含まないこのセットアップのバージョンに興味がある場合は、お気軽にお問い合わせください。
サーバーサイド レンダリング (SSR) は、サーバーが Web ページの HTML コンテンツを生成してからブラウザーに送信する Web 開発の手法です。 JavaScript が空の HTML シェルをロードした後にユーザーのデバイス上でコンテンツを構築する従来のクライアント側レンダリング (CSR) とは異なり、SSR は完全にレンダリングされた HTML をサーバーから直接配信します。
SSR の主な利点:
SSR を使用したアプリのフローは次の手順に従います:
私は pnpm と React-swc-ts Vite テンプレートを使用することを好みますが、他のセットアップを選択することもできます。
pnpm create vite react-ssr-app --template react-swc-ts
依存関係をインストールします:
pnpm create vite react-ssr-app --template react-swc-ts
典型的な React アプリケーションには、index.html の main.tsx エントリ ポイントが 1 つあります。 SSR では、サーバー用とクライアント用の 2 つのエントリ ポイントが必要です。
Node.js サーバーはアプリを実行し、React コンポーネントを文字列 (renderToString) にレンダリングすることで HTML を生成します。
pnpm install
ブラウザはサーバーで生成された HTML をハイドレートし、JavaScript と接続してページをインタラクティブにします。
ハイドレーション は、サーバーによってレンダリングされた静的 HTML にイベント リスナーやその他の動的動作をアタッチするプロセスです。
// ./src/entry-server.tsx import { renderToString } from 'react-dom/server' import App from './App' export function render() { return renderToString(<App />) }
プロジェクトのルートにあるindex.htmlファイルを更新します。 プレースホルダーは、サーバーが生成された HTML を挿入する場所です。
// ./src/entry-client.tsx import { hydrateRoot } from 'react-dom/client' import { StrictMode } from 'react' import App from './App' import './index.css' hydrateRoot( document.getElementById('root')!, <StrictMode> <App /> </StrictMode>, )
サーバーに必要なすべての依存関係は、クライアント バンドルに含まれないように、開発依存関係 (devDependency) としてインストールする必要があります。
次に、プロジェクトのルートに ./server という名前のフォルダーを作成し、次のファイルを追加します。
メインサーバーファイルを再エクスポートします。これにより、コマンドの実行がより便利になります。
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Vite + React + TS</title> </head> <body> <div> <h3> Create Server </h3> <p>First, install the dependencies:<br> </p> <pre class="brush:php;toolbar:false">pnpm install -D express compression sirv tsup vite-node nodemon @types/express @types/compression
HTML_KEY 定数は、index.html のプレースホルダー コメントと一致する必要があります。他の定数は環境設定を管理します。
// ./server/index.ts export * from './app'
開発環境と運用環境に異なる構成で Express サーバーをセットアップします。
// ./server/constants.ts export const NODE_ENV = process.env.NODE_ENV || 'development' export const APP_PORT = process.env.APP_PORT || 3000 export const PROD = NODE_ENV === 'production' export const HTML_KEY = `<!--app-html-->`
開発では、Vite のミドルウェアを使用してリクエストを処理し、ホットリロードでindex.html ファイルを動的に変換します。サーバーは React アプリケーションをロードし、リクエストごとに HTML にレンダリングします。
// ./server/app.ts import express from 'express' import { PROD, APP_PORT } from './constants' import { setupProd } from './prod' import { setupDev } from './dev' export async function createServer() { const app = express() if (PROD) { await setupProd(app) } else { await setupDev(app) } app.listen(APP_PORT, () => { console.log(`http://localhost:${APP_PORT}`) }) } createServer()
運用環境では、圧縮を使用してパフォーマンスを最適化し、sirv を使用して静的ファイルを提供し、事前構築されたサーバー バンドルを使用してアプリをレンダリングします。
// ./server/dev.ts import { Application } from 'express' import fs from 'fs' import path from 'path' import { HTML_KEY } from './constants' 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) { // Create a Vite development server in middleware mode const vite = await ( await import('vite') ).createServer({ root: process.cwd(), server: { middlewareMode: true }, appType: 'custom', }) // Use Vite middleware for serving files app.use(vite.middlewares) app.get('*', async (req, res, next) => { try { // Read and transform the HTML file let html = fs.readFileSync(HTML_PATH, 'utf-8') html = await vite.transformIndexHtml(req.originalUrl, html) // Load the entry-server.tsx module and render the app const { render } = await vite.ssrLoadModule(ENTRY_SERVER_PATH) const appHtml = await render() // Replace the placeholder with the rendered HTML html = html.replace(HTML_KEY, appHtml) res.status(200).set({ 'Content-Type': 'text/html' }).end(html) } catch (e) { // Fix stack traces for Vite and handle errors vite.ssrFixStacktrace(e as Error) console.error((e as Error).stack) next(e) } }) }
アプリケーションを構築するためのベスト プラクティスに従うには、不要なパッケージをすべて除外し、アプリケーションが実際に使用するもののみを含める必要があります。
ビルド プロセスを最適化し、SSR の依存関係を処理するには、Vite 構成を更新します。
// ./server/prod.ts import { Application } from 'express' import fs from 'fs' import path from 'path' import compression from 'compression' import sirv from 'sirv' import { HTML_KEY } from './constants' 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) { // Use compression for responses app.use(compression()) // Serve static files from the client build folder app.use(sirv(CLIENT_PATH, { extensions: [] })) app.get('*', async (_, res, next) => { try { // Read the pre-built HTML file let html = fs.readFileSync(HTML_PATH, 'utf-8') // Import the server-side render function and generate HTML const { render } = await import(ENTRY_SERVER_PATH) const appHtml = await render() // Replace the placeholder with the rendered HTML html = html.replace(HTML_KEY, appHtml) res.status(200).set({ 'Content-Type': 'text/html' }).end(html) } catch (e) { // Log errors and pass them to the error handler console.error((e as Error).stack) next(e) } }) }
tsconfig.json を更新してサーバー ファイルを含め、TypeScript を適切に構成します。
pnpm create vite react-ssr-app --template react-swc-ts
TypeScript バンドラーである tsup を使用してサーバー コードを構築します。 noExternal オプションは、サーバーにバンドルするパッケージを指定します。 サーバーが使用する追加パッケージを必ず含めてください。
pnpm install
// ./src/entry-server.tsx import { renderToString } from 'react-dom/server' import App from './App' export function render() { return renderToString(<App />) }
開発: 次のコマンドを使用して、ホットリロードでアプリケーションを起動します:
// ./src/entry-client.tsx import { hydrateRoot } from 'react-dom/client' import { StrictMode } from 'react' import App from './App' import './index.css' hydrateRoot( document.getElementById('root')!, <StrictMode> <App /> </StrictMode>, )
本番: アプリケーションをビルドし、本番サーバーを起動します:
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Vite + React + TS</title> </head> <body> <div> <h3> Create Server </h3> <p>First, install the dependencies:<br> </p> <pre class="brush:php;toolbar:false">pnpm install -D express compression sirv tsup vite-node nodemon @types/express @types/compression
SSR が動作していることを確認するには、サーバーへの最初のネットワーク リクエストを確認します。応答には、アプリケーションの完全にレンダリングされた HTML が含まれている必要があります。
アプリにさまざまなページを追加するには、ルーティングを適切に構成し、クライアントとサーバーの両方のエントリ ポイントで処理する必要があります。
// ./server/index.ts export * from './app'
クライアント側のルーティングを有効にするために、クライアント エントリ ポイントで BrowserRouter を使用してアプリケーションをラップします。
// ./server/constants.ts export const NODE_ENV = process.env.NODE_ENV || 'development' export const APP_PORT = process.env.APP_PORT || 3000 export const PROD = NODE_ENV === 'production' export const HTML_KEY = `<!--app-html-->`
サーバー側のルーティングを処理するには、サーバーのエントリ ポイントで StaticRouter を使用します。 URL をプロップとして渡し、リクエストに基づいて正しいルートをレンダリングします。
// ./server/app.ts import express from 'express' import { PROD, APP_PORT } from './constants' import { setupProd } from './prod' import { setupDev } from './dev' export async function createServer() { const app = express() if (PROD) { await setupProd(app) } else { await setupDev(app) } app.listen(APP_PORT, () => { console.log(`http://localhost:${APP_PORT}`) }) } createServer()
開発サーバーと運用サーバーの両方のセットアップを更新して、リクエスト URL をレンダリング関数に渡します。
// ./server/dev.ts import { Application } from 'express' import fs from 'fs' import path from 'path' import { HTML_KEY } from './constants' 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) { // Create a Vite development server in middleware mode const vite = await ( await import('vite') ).createServer({ root: process.cwd(), server: { middlewareMode: true }, appType: 'custom', }) // Use Vite middleware for serving files app.use(vite.middlewares) app.get('*', async (req, res, next) => { try { // Read and transform the HTML file let html = fs.readFileSync(HTML_PATH, 'utf-8') html = await vite.transformIndexHtml(req.originalUrl, html) // Load the entry-server.tsx module and render the app const { render } = await vite.ssrLoadModule(ENTRY_SERVER_PATH) const appHtml = await render() // Replace the placeholder with the rendered HTML html = html.replace(HTML_KEY, appHtml) res.status(200).set({ 'Content-Type': 'text/html' }).end(html) } catch (e) { // Fix stack traces for Vite and handle errors vite.ssrFixStacktrace(e as Error) console.error((e as Error).stack) next(e) } }) }
これらの変更により、React アプリで SSR と完全に互換性のあるルートを作成できるようになりました。ただし、この基本的なアプローチでは、遅延ロードされたコンポーネント (React.lazy) は処理されません。遅延ロードされたモジュールの管理については、下部にリンクされている私の他の記事 ストリーミングと動的データを使用した高度な React SSR テクニック を参照してください。
アプリケーションをコンテナ化するための Dockerfile は次のとおりです:
// ./server/prod.ts import { Application } from 'express' import fs from 'fs' import path from 'path' import compression from 'compression' import sirv from 'sirv' import { HTML_KEY } from './constants' 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) { // Use compression for responses app.use(compression()) // Serve static files from the client build folder app.use(sirv(CLIENT_PATH, { extensions: [] })) app.get('*', async (_, res, next) => { try { // Read the pre-built HTML file let html = fs.readFileSync(HTML_PATH, 'utf-8') // Import the server-side render function and generate HTML const { render } = await import(ENTRY_SERVER_PATH) const appHtml = await render() // Replace the placeholder with the rendered HTML html = html.replace(HTML_KEY, appHtml) res.status(200).set({ 'Content-Type': 'text/html' }).end(html) } catch (e) { // Log errors and pass them to the error handler console.error((e as Error).stack) next(e) } }) }
Docker イメージの構築と実行
// ./vite.config.ts import { defineConfig } from 'vite' import react from '@vitejs/plugin-react-swc' import { dependencies } from './package.json' export default defineConfig(({ mode }) => ({ plugins: [react()], ssr: { noExternal: mode === 'production' ? Object.keys(dependencies) : undefined, }, }))
{ "include": [ "src", "server", "vite.config.ts" ] }
このガイドでは、React を使用して本番環境に対応した SSR アプリケーションを作成するための強力な基盤を確立しました。プロジェクトのセットアップ、ルーティングの構成、Dockerfile の作成方法を学習しました。この設定は、ランディング ページや小さなアプリを効率的に作成するのに最適です。
これは、React を使用した SSR に関するシリーズの一部です。他の記事もお楽しみに!
フィードバック、コラボレーション、技術的なアイデアについての議論はいつでも受け付けています。お気軽にご連絡ください。
以上が本番環境に対応した SSR React アプリケーションの構築の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。