はじめに
React が提供する SSR API は 2 つの部分に分かれています。一部はサーバー側向け(react-dom/server)、もう一部はクライアント側で実行されます(react-dom)
[caption id="attachment_2317" align="alignnone" width="625"]
react ssr[/caption]
一.ReactDOMServer
ReactDOMServer 関連 API はサーバー側で React コンポーネントを静的な(HTML)タグにレンダリングできます:
The ReactDOMServer object enables you to render components to static markup.
コンポーネントツリーを対応する HTML タグにレンダリングする作業はブラウザ環境でも完了できるため、サーバー側向け React DOM API も 2 つの類別に分かれます:
-
Node.js、ブラウザ環境を跨いで実行可能な String API:renderToString()、renderToStaticMarkup()
-
Node.js 環境でのみ実行可能な Stream API:renderToNodeStream()、renderToStaticNodeStream()
renderToString
ReactDOMServer.renderToString(element)
最も基本的な SSR API で、React コンポーネント(正確には ReactElement)を入力し、HTML 文字列を出力します。その後クライアント側の hydrate API がサーバー側から返されたビュー構造にインタラクション動作を付加し、ページレンダリングを完了します:
If you call ReactDOM.hydrate() on a node that already has this server-rendered markup, React will preserve it and only attach event handlers.
renderToStaticMarkup
ReactDOMServer.renderToStaticMarkup(element)
renderToString と類似していますが、API 設計上の違いとして、renderToStaticMarkup は純粋な表示(イベントインタラクションなし、hydrate 不要)のシーンのみに使用されます:
This is useful if you want to use React as a simple static page generator, as stripping away the extra attributes can save some bytes. If you plan to use React on the client to make the markup interactive, do not use this method. Instead, use renderToString on the server and ReactDOM.hydrate() on the client.
したがって renderToStaticMarkup はクリーンな HTML のみを生成し、追加の DOM 属性(data-reactroot など)を帯びず、応答体積上でわずかな優位性があります
体積優位性がわずかな理由としては、React 16 之前、SSR は 文字列チェックサム(string checksum)に基づく HTML ノード再利用方式 を採用しており、文字対文字で厳密に一貫性を検証し、一旦不匹配を発見するとサーバー側レンダリング結果を完全に破棄し、クライアント側で再レンダリングしていました:
If for any reason there's a mismatch, React raises a warning in development mode and replaces the entire tree of server-generated markup with HTML that has been generated on the client.
大量の追加属性を生成していました:
// renderToString
<div data-reactroot="" data-reactid="1"
data-react-checksum="122239856">
<!-- react-text: 2 -->This is some <!-- /react-text -->
<span data-reactid="3">server-generated</span>
<!-- react-text: 4--> <!-- /react-text -->
<span data-reactid="5">HTML.</span>
</div>
この時 renderToStaticMarkup が生成するクリーンで爽やかな HTML にはまだ不小的体積優位性がありました:
// renderToStaticMarkup
<div data-reactroot="">
This is some <span>server-generated</span> <span>HTML.</span>
</div>
React 16 は単ノード検証に切り替え(サーバー側から返された)HTML ノードを再利用 し、data-reactid、data-react-checksum などの体積占有大户を生成しなくなったため、2 つの API のレンダリング結果の体積差異は微々たるものになりました。例えば、React コンポーネントに対して:
class MyComponent extends React.Component {
state = {
title: 'Welcome to React SSR!',
}
render() {
return (
<div>
<h1 className="here">
{this.state.title} Hello There!
</h1>
</div>
);
}
}
二者のレンダリング結果はそれぞれ:
// renderToString
<div data-reactroot=""><h1 class="here">Welcome to React SSR!<!-- --> Hello There!</h1></div>
// renderToStaticMarkup
<div><h1 class="here">Welcome to React SSR! Hello There!</h1></div>
つまり、現在(2020/11/8、React 17.0.1)renderToStaticMarkup と renderToString の実際の差異は主に:
-
renderToStaticMarkupはdata-reactrootを生成しない -
renderToStaticMarkupは隣接テキストノード間に<!-- -->を生成しない(テキストノードを合併したのに相当し、ノード再利用を考慮せず、静的レンダリング向けの追加最適化措置と算是)
renderToNodeStream
ReactDOMServer.renderToNodeStream(element)
renderToString に対応する Stream API で、renderToString が生成する HTML 文字列を Node.js Readable stream 形式で返します
P.S. デフォルトでutf-8 エンコードのバイトストリームを返し、他のエンコード形式は自行変換が必要です
P.S. この API の実装は Node.js の Stream 特性 に依存するため、ブラウザ環境では使用できません
renderToStaticNodeStream
ReactDOMServer.renderToStaticNodeStream(element)
renderToStaticMarkup に対応する Stream API で、renderToStaticMarkup が生成するクリーンな HTML 文字列を Node.js Readable stream 形式で返します
P.S. 同样 utf-8 エンコードで、ブラウザ環境では使用できません
二.ReactDOM
hydrate()
ReactDOM.hydrate(element, container[, callback])
よく使用される render() 関数シグネチャと完全に一致します:
ReactDOM.render(element, container[, callback])
hydrate() は SSR と配合して使用し、render() との違いはレンダリング過程中にサーバー側から返された既存の HTML ノードを再利用でき、インタラクション動作(イベントリスナーなど)のみを付加し、DOM ノードを再作成しないことです:
React will attempt to attach event listeners to the existing markup.
注意すべきは、サーバー側から返された HTML とクライアント側レンダリング結果が一致しない時、性能考慮のため、hydrate() はテキストノード外の SSR レンダリング結果を訂正せず、そのまま間違えることです:
There are no guarantees that attribute differences will be patched up in case of mismatches. This is important for performance reasons because in most apps, mismatches are rare, and so validating all markup would be prohibitively expensive.
development モードでのみこれらの不一致の問題に Warning を報告するため、SSR HydrationWarning を重視し、Error として逐个解決する必要があります:
This performance optimization means that you will need to make extra sure that you fix any markup mismatch warnings you see in your app in development mode.
特別なことに、予想内の不一致問題、例えばタイムスタンプについては、suppressHydrationWarning={true} 属性を通じて該要素の HydrationWarning を明示的に無視できます(警告のみを無視し、訂正はしないため、仍サーバー側レンダリング結果を保持します)。もしどうしてもサーバー側とクライアント側でそれぞれ異なる内容をレンダリングしたい場合、まず初回レンダリング内容を一致させ、その後更新を通じて完了することを推奨します(もちろん、性能は少し悪くなります)。例えば:
class MyComponent extends React.Component {
state = {
isClient: false
}
render() {
return this.state.isClient ? 'レンダリング...クライアント内容' : 'レンダリング...サーバー側内容';
}
componentDidMount() {
this.setState({
isClient: true
});
}
}
三.SSR 関連の API 制限
大部分のライフサイクル関数はサーバー側で実行されない
SSR モードでは、サーバー側は 3 つのライフサイクル関数のみを実行します:
constructorgetDerivedStateFromPropsrender
その他あらゆるライフサイクルはサーバー側で実行されず、getDerivedStateFromError、componentDidCatch などのエラー処理 API を含みます
[caption id="attachment_2319" align="alignnone" width="625"]
react ssr lifecycle[/caption]
P.S. すでに廃棄された componentWillMount、UNSAFE_componentWillMount は getDerivedStateFromProps、getSnapshotBeforeUpdate と互斥し、後者グループの新 API の任意 1 つが存在すれば、前者 2 つの旧 API は呼び出されません
Error Boundary と Portal をサポートしない
With streaming rendering it's impossible to "call back" markup that has already been sent, and we opted to keep renderToString and renderToNodeStream's output identical.
ストリーミングレンダリングをサポートし、同時に String API と Stream API の出力内容の一貫性を保持するため、レンダリングバックトラックを引き起こす 2 つの特性を犠牲にしました:
-
Error Boundary:子孫コンポーネントのランタイムエラーをキャプチャし、ダウングレード UI をレンダリング可能
-
Portal:コンポーネントを指定された任意の DOM ノードにレンダリング可能で、同時にイベントがコンポーネント階層に従ってバブルすることを保持
理解しやすい���とで、ストリーミングはレンダリングしながら応答し、(バックトラックして)すでに送信した内容を変更できないため、その他類似のシーンもサポートされません。例えばレンダリング過程中に動的に head 内に style や script タグを挿入するなど
P.S. SSR Error Boundary に関するより多くの議論については、componentDidCatch doesn't work in React 16's renderToString を参照
コメントはまだありません