このページは PageTurner AI で翻訳されました(ベータ版)。プロジェクト公式の承認はありません。 エラーを見つけましたか? 問題を報告 →
サーバーサイドレンダリング
サーバーサイドレンダリングの最も一般的なユースケースは、ユーザー(または検索エンジンのクローラー)が初めてアプリをリクエストした際の_初期レンダリング_を処理することです。サーバーはリクエストを受信すると、必要なコンポーネントをHTML文字列にレンダリングし、クライアントへレスポンスとして送信します。その後はクライアントがレンダリングを引き継ぎます。
以下の例ではReactを使用しますが、同じテクニックはサーバー上でレンダリング可能な他のビューフレームワークでも適用できます。
サーバーサイドにおけるRedux
Reduxをサーバーサイドレンダリングで使用する場合、アプリケーションの状態もレスポンスと共に送信する必要があります。これによりクライアントはその状態を初期状態として利用できます。HTML生成前にデータをプリロードする場合、クライアントもそのデータにアクセスできるようにすることが重要です。そうしないと、クライアントで生成されるマークアップがサーバーのマークアップと一致せず、クライアントは再度データをロードする必要が生じます。
データをクライアントに送信するには、以下の手順が必要です:
-
各リクエストごとに新規のReduxストアインスタンスを作成する
-
(オプションで)いくつかのアクションをディスパッチする
-
ストアから状態を取り出す
-
その状態をクライアントに渡す
クライアントサイドでは、サーバーから提供された状態で初期化された新しいReduxストアが作成されます。 サーバーサイドにおけるReduxの唯一の役割は、アプリの初期状態を提供することです。
セットアップ
次のレシピでは、サーバーサイドレンダリングの設定方法を説明します。シンプルなカウンターアプリをガイドとして使用し、リクエストに基づいてサーバーが事前に状態をレンダリングする方法を示します。
パッケージのインストール
この例では、シンプルなWebサーバーとしてExpressを使用します。また、ReactバインディングはReduxにデフォルトで含まれていないため、別途インストールする必要があります。
npm install express react-redux
サーバーサイドの実装
以下はサーバーサイドの実装概要です。Expressミドルウェアをapp.useで設定し、サーバーへのすべてのリクエストを処理します。Expressやミドルウェアに慣れていない場合でも、handleRender関数がサーバーがリクエストを受信するたびに呼び出されることを理解してください。
さらに、現代的なJSとJSX構文を使用するため、Babel(NodeサーバーとBabelの使用例を参照)とReactプリセットでコンパイルする必要があります。
server.js
import path from 'path'
import Express from 'express'
import React from 'react'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import counterApp from './reducers'
import App from './containers/App'
const app = Express()
const port = 3000
// Serve static files
app.use('/static', Express.static('static'))
// This is fired every time the server side receives a request
app.use(handleRender)
// We are going to fill these out in the sections to follow
function handleRender(req, res) {
/* ... */
}
function renderFullPage(html, preloadedState) {
/* ... */
}
app.listen(port)
リクエストの処理
各リクエストで最初に行う必要があるのは、新しいReduxストアインスタンスの作成です。このストアインスタンスの唯一の目的は、アプリケーションの初期状態を提供することです。
レンダリング時には、ルートコンポーネントである<App />を<Provider>でラップし、コンポーネントツリー内のすべてのコンポーネントがストアを利用できるようにします。これは"Redux Fundamentals" Part 5: UI and Reactで見た方法と同じです。
サーバーサイドレンダリングの重要なステップは、クライアントサイドに送信する_前に_コンポーネントの初期HTMLをレンダリングすることです。これにはReactDOMServer.renderToString()を使用します。
次に、Reduxストアからstore.getState()を使用して初期状態を取得します。この状態をrenderFullPage関数でどのように受け渡すかは後ほど説明します。
import { renderToString } from 'react-dom/server'
function handleRender(req, res) {
// Create a new Redux store instance
const store = createStore(counterApp)
// Render the component to a string
const html = renderToString(
<Provider store={store}>
<App />
</Provider>
)
// Grab the initial state from our Redux store
const preloadedState = store.getState()
// Send the rendered page back to the client
res.send(renderFullPage(html, preloadedState))
}
初期コンポーネントHTMLと状態の注入
サーバーサイドでの最終ステップは、初期コンポーネントHTMLとアプリケーション状態をクライアントサイドでレンダリングするテンプレートに注入することです。状態を渡すために、preloadedState を window.__PRELOADED_STATE__ にアタッチする <script> タグを追加します。
これにより preloadedState はクライアントサイドで window.__PRELOADED_STATE__ にアクセスすることで利用可能になります。
また、クライアントサイドアプリケーション用のバンドルファイルをスクリプトタグ経由で含めます。これはバンドリングツールがクライアントエントリポイント向けに生成する出力(静的ファイルまたはホットリロード開発サーバーのURL)です。
function renderFullPage(html, preloadedState) {
return `
<!doctype html>
<html>
<head>
<title>Redux Universal Example</title>
</head>
<body>
<div id="root">${html}</div>
<script>
// WARNING: See the following for security issues around embedding JSON in HTML:
// https://redux.js.org/usage/server-rendering#security-considerations
window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(
/</g,
'\\u003c'
)}
</script>
<script src="/static/bundle.js"></script>
</body>
</html>
`
}
クライアントサイド
クライアントサイドの処理は非常にシンプルです。window.__PRELOADED_STATE__ から初期状態を取得し、それを createStore() 関数の初期状態として渡すだけです。
新しいクライアントファイルを見てみましょう:
client.js
import React from 'react'
import { hydrate } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './containers/App'
import counterApp from './reducers'
// Create Redux store with state injected by the server
const store = createStore(counterApp, window.__PRELOADED_STATE__)
// Allow the passed state to be garbage-collected
delete window.__PRELOADED_STATE__
hydrate(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
WebpackやBrowserifyなどお好みのビルドツールで、static/bundle.js にバンドルファイルをコンパイルするように設定できます。
ページ読み込み時、バンドルファイルが起動され ReactDOM.hydrate() がサーバーでレンダリングされたHTMLを再利用します。これにより新しく起動したReactインスタンスがサーバー側の仮想DOMに接続されます。Reduxストアの初期状態とすべてのビューコンポーネントのコードが同一であるため、結果として同じ実際のDOMが得られます。
以上です!これがサーバーサイドレンダリングを実装するために必要な全てです。
ただし結果は基本的なもので、動的なコードから静的ビューをレンダリングしているに過ぎません。次に必要なのは、レンダリングされたビューを動的にするために初期状態を動的に構築することです。
window.__PRELOADED_STATE__ を直接 createStore に渡し、プリロード状態への追加参照(例:const preloadedState = window.__PRELOADED_STATE__)の作成を避けることを推奨します。これによりガベージコレクションが可能になります。
初期状態の準備
クライアントサイドは継続的にコードを実行するため、空の初期状態から開始し必要に応じて段階的に状態を取得できます。一方サーバーサイドのレンダリングは同期的で、ビューのレンダリングは1回限りです。リクエスト処理中に初期状態を構築する必要があり、入力に対応し外部状態(APIやデータベースなど)を取得しなければなりません。
リクエストパラメータの処理
サーバーサイドコードへの唯一の入力は、アプリケーションのページをブラウザで読み込む際のリクエストです。サーバーの起動時(開発環境と本番環境の違いなど)に設定を行うことはできますが、その設定は静的なものです。
リクエストにはURL情報(React Routerのようなライブラリを使用する際に有用なクエリパラメータを含む)が含まれます。またCookieや認証情報などのヘッダーやPOSTボディデータも含まれます。クエリパラメータに基づいてカウンターの初期状態を設定する方法を見てみましょう。
server.js
import qs from 'qs' // Add this at the top of the file
import { renderToString } from 'react-dom/server'
function handleRender(req, res) {
// Read the counter from the request, if provided
const params = qs.parse(req.query)
const counter = parseInt(params.counter, 10) || 0
// Compile an initial state
let preloadedState = { counter }
// Create a new Redux store instance
const store = createStore(counterApp, preloadedState)
// Render the component to a string
const html = renderToString(
<Provider store={store}>
<App />
</Provider>
)
// Grab the initial state from our Redux store
const finalState = store.getState()
// Send the rendered page back to the client
res.send(renderFullPage(html, finalState))
}
このコードはサーバーミドルウェアに渡されるExpressの Request オブジェクトから読み取ります。パラメータは数値に変換された後、初期状態に設定されます。http://localhost:3000/?counter=100 にアクセスすると、カウンターが100から始まります。レンダリングされたHTMLではカウンター出力が100になり、__PRELOADED_STATE__ 変数にもカウンター値が設定されています。
非同期状態の取得
サーバーサイドレンダリングで最も一般的な課題は、非同期で到着する状態の扱いです。サーバーでのレンダリングは本質的に同期的であるため、非同期フェッチを同期的な操作に変換する必要があります。
最も簡単な方法は、コールバック関数を同期的なコードに渡すことです。ここではレスポンスオブジェクトを参照し、レンダリングされたHTMLをクライアントに送信する関数を使用します。心配ありません、見た目ほど複雑ではありません。
この例では、カウンターの初期値を保持する外部データストア(CaaS: Counter As A Service)があると想定します。モックAPI呼び出しを作成し、その結果から初期状態を構築します。まずAPI呼び出しの実装から始めましょう:
api/counter.js
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min
}
export function fetchCounter(callback) {
setTimeout(() => {
callback(getRandomInt(1, 100))
}, 500)
}
これはモックAPIなので、setTimeoutを使用して500ミリ秒で応答するネットワークリクエストをシミュレートします(実際のAPIではもっと高速になるはずです)。非同期で乱数を返すコールバックを渡しています。PromiseベースのAPIクライアントを使用する場合は、thenハンドラー内でこのコールバックを実行します。
サーバー側では、既存のコードをfetchCounterでラップし、コールバックで結果を受け取ります:
server.js
// Add this to our imports
import { fetchCounter } from './api/counter'
import { renderToString } from 'react-dom/server'
function handleRender(req, res) {
// Query our mock API asynchronously
fetchCounter(apiResult => {
// Read the counter from the request, if provided
const params = qs.parse(req.query)
const counter = parseInt(params.counter, 10) || apiResult || 0
// Compile an initial state
let preloadedState = { counter }
// Create a new Redux store instance
const store = createStore(counterApp, preloadedState)
// Render the component to a string
const html = renderToString(
<Provider store={store}>
<App />
</Provider>
)
// Grab the initial state from our Redux store
const finalState = store.getState()
// Send the rendered page back to the client
res.send(renderFullPage(html, finalState))
})
}
コールバック内でres.send()を呼び出すため、サーバーは接続を保持し、コールバックが実行されるまでデータを送信しません。新しいAPI呼び出しにより、各リクエストに500msの遅延が追加されます。より高度な実装では、不正なレスポンスやタイムアウトなどのエラーを適切に処理する必要があります。
セキュリティに関する考慮事項
ユーザー生成コンテンツ(UGC)や入力に依存するコードが増えたことで、アプリケーションの攻撃対象領域が拡大しました。クロスサイトスクリプティング(XSS)攻撃やコードインジェクションを防ぐため、入力値を適切にサニタイズすることが重要です。
この例では、基本的なセキュリティ対策を採用しています。リクエストからパラメータを取得する際、counterパラメータにparseIntを使用して数値であることを保証しています。これを行わない場合、リクエストにスクリプトタグを渡すことで危険なデータをレンダリングされたHTMLに挿入できてしまいます(例: ?counter=</script><script>doSomethingBad();</script>)。
この単純な例では、入力を数値に変換するだけで十分安全です。自由入力テキストなどより複雑な入力を扱う場合は、xss-filtersなどの適切なサニタイズ関数を通す必要があります。
さらに、状態出力のサニタイズによりセキュリティ層を追加できます。JSON.stringifyはスクリプトインジェクションの対象となる可能性があります。これを防ぐには、JSON文字列からHTMLタグや危険な文字を除去します。単純な文字列置換(例: JSON.stringify(state).replace(/</g, '\\u003c'))か、serialize-javascriptのような高度なライブラリを使用できます。
次のステップ
PromiseやThunkなどの非同期プリミティブを使用したReduxでの非同期フローの表現について学ぶには、Redux Fundamentals Part 6: Async Logic and Data Fetchingを参照してください。ここで学ぶ内容はユニバーサルレンダリングにも適用できます。
React Routerを使用する場合、データ取得の依存関係をルートハンドラーコンポーネントの静的fetchData()メソッドとして表現できます。これらはThunkを返すことができるため、handleRender関数がルートを対応するコンポーネントクラスにマッチングし、各コンポーネントのfetchData()結果をディスパッチし、Promiseが解決した後にのみレンダリングできます。この方法により、異なるルートに必要なAPI呼び出しをルートハンドラー定義と同じ場所に配置できます。クライアント側でも同じ手法を使用し、データが読み込まれるまでページ遷移を防ぐことができます。