静的サイト生成(SSG)
アーキテクチャで、テーマはWebpackで実行されると述べました。しかし注意してください。それは常にブラウザのグローバル変数にアクセスできるという意味ではありません!テーマは2回ビルドされます
- サーバーサイドレンダリング中、テーマはReact DOM Serverというサンドボックスでコンパイルされます。これは「ヘッドレスブラウザ」と見なすことができ、そこには
window
やdocument
はなく、Reactのみが存在します。SSRは静的なHTMLページを生成します。 - クライアントサイドレンダリング中、テーマは最終的にブラウザで実行されるJavaScriptにコンパイルされるため、ブラウザ変数にアクセスできます。
サーバーサイドレンダリングと静的サイト生成は異なる概念ですが、ここではこれらを同じ意味で使用します。
厳密に言えば、Docusaurusは静的サイトジェネレーターです。サーバー側のランタイムはなく、リクエストごとに動的にプリレンダリングするのではなく、CDNにデプロイされるHTMLファイルに静的にレンダリングするからです。これはNext.jsの動作モデルとは異なります。
したがって、process
のようなNodeグローバル変数(または使えるのか?)や'fs'
モジュールにアクセスしてはいけないことはご存知でしょうが、ブラウザのグローバル変数にも自由にアクセスすることはできません。
import React from 'react';
export default function WhereAmI() {
return <span>{window.location.href}</span>;
}
これは慣用的なReactのコードのように見えますが、docusaurus build
を実行すると、エラーが発生します。
ReferenceError: window is not defined
これは、サーバーサイドレンダリング中、Docusaurusアプリは実際にはブラウザで実行されておらず、window
が何であるかを知らないためです。
process.env.NODE_ENV
はどうですか?
「Nodeグローバル変数なし」というルールの例外の1つは、process.env.NODE_ENV
です。実際、Webpackはこの変数をグローバル変数として注入するため、Reactで使用できます。
import React from 'react';
export default function expensiveComp() {
if (process.env.NODE_ENV === 'development') {
return <>This component is not shown in development</>;
}
const res = someExpensiveOperationThatLastsALongTime();
return <>{res}</>;
}
Webpackビルド中、process.env.NODE_ENV
は'development'
または'production'
のいずれかの値に置き換えられます。その後、デッドコードエリミネーション後に異なるビルド結果が得られます。
- 開発
- 本番
import React from 'react';
export default function expensiveComp() {
if ('development' === 'development') {
+ return <>This component is not shown in development</>;
}
- const res = someExpensiveOperationThatLastsALongTime();
- return <>{res}</>;
}
import React from 'react';
export default function expensiveComp() {
- if ('production' === 'development') {
- return <>This component is not shown in development</>;
- }
+ const res = someExpensiveOperationThatLastsALongTime();
+ return <>{res}</>;
}
SSRを理解する
Reactは動的なUIランタイムであるだけでなく、テンプレートエンジンでもあります。Docusaurusサイトのほとんどは静的なコンテンツで構成されているため、JavaScript(Reactが実行する)なしで、プレーンなHTML / CSSのみで動作できるはずです。そして、それがサーバーサイドレンダリングが提供するものです。動的なコンテンツなしで、ReactコードをHTMLに静的にレンダリングします。HTMLファイルにはクライアント状態の概念がない(純粋にマークアップ)ため、ブラウザAPIに依存すべきではありません。
これらのHTMLファイルは、URLにアクセスしたときにユーザーのブラウザ画面に最初に到達するものです(ルーティングを参照)。その後、ブラウザはサイトの「動的」部分を提供する他のJSコード(JavaScriptで実装されたもの)をフェッチして実行します。ただし、それ以前に、ページのメインコンテンツはすでに表示されており、読み込みが高速化されます。
CSRのみのアプリでは、すべてのDOM要素がReactを使用してクライアント側で生成され、HTMLファイルにはReactがDOMをマウントするためのルート要素が1つだけ含まれています。SSRでは、Reactは完全に構築されたHTMLページにすでに直面しており、モデル内の仮想DOMとDOM要素を関連付けるだけです。このステップは「ハイドレーション」と呼ばれます。Reactが静的なマークアップをハイドレーションした後、アプリは通常のReactアプリとして動作を開始します。
Docusaurusは最終的にはシングルページアプリケーションであるため、静的サイト生成は単なる最適化(いわゆるプログレッシブエンハンスメント)にすぎませんが、その機能はこれらのHTMLファイルに完全には依存していません。これは、すべてのファイルが静的にマークアップに変換され、<script>
タグでリンクされた外部JavaScriptを介してインタラクティビティが追加される、JekyllやDocusaurus v1のようなサイトジェネレーターとは対照的です。ビルド出力を調べると、build/assets/js
の下にJSアセットが表示されます。これらは実際にはDocusaurusの中核となるものです。
エスケープハッチ
ブラウザAPIに依存する動的なコンテンツを画面にレンダリングする場合は、たとえば
- ブラウザのJSランタイムで実行されるライブコードブロック
- ユーザーのカラースキームを検出して異なる画像を表示するテーマ付き画像
- スタイリングに
window
グローバル変数を使用するデバッグパネルのJSONビューア
クライアントの状態を知らずに静的なHTMLでは何も役に立たないものを表示できないため、SSRからエスケープする必要がある場合があります。
最初のクライアントサイドレンダリングがサーバーサイドレンダリングとまったく同じDOM構造を生成することが重要です。そうしないと、Reactは仮想DOMを間違ったDOM要素と関連付けます。
したがって、if (typeof window !== 'undefined) {/* 何かをレンダリングする */}
という安易な試みは、ブラウザとサーバーの検出として適切に機能しません。最初のクライアントレンダリングでは、サーバーで生成されたものとは異なるマークアップがすぐにレンダリングされるためです。
この落とし穴の詳細については、The Perils of Rehydrationをお読みください。
SSRからエスケープするための、より信頼性の高い方法をいくつか提供します。
<BrowserOnly>
コンポーネントをブラウザでのみレンダリングする必要がある場合(たとえば、コンポーネントが機能するためにブラウザ固有のものに依存している場合)、一般的なアプローチの1つは、<BrowserOnly>
でコンポーネントをラップして、SSR中は非表示にし、CSRでのみレンダリングされるようにすることです。
import BrowserOnly from '@docusaurus/BrowserOnly';
function MyComponent(props) {
return (
<BrowserOnly fallback={<div>Loading...</div>}>
{() => {
const LibComponent =
require('some-lib-that-accesses-window').LibComponent;
return <LibComponent {...props} />;
}}
</BrowserOnly>
);
}
<BrowserOnly>
の子はJSX要素ではなく、要素を返す関数であることに注意することが重要です。これは設計上の決定です。次のコードを考えてください
import BrowserOnly from '@docusaurus/BrowserOnly';
function MyComponent() {
return (
<BrowserOnly>
{/* DON'T DO THIS - doesn't actually work */}
<span>page url = {window.location.href}</span>
</BrowserOnly>
);
}
BrowserOnly
はサーバーサイドレンダリング中に子を隠すことを期待するかもしれませんが、実際には隠すことはできません。ReactレンダラーがこのJSXツリーをレンダリングしようとすると、{window.location.href}
変数がこのツリーのノードとして認識され、実際には使用されていなくてもレンダリングしようとします。関数を使用することで、必要な場合にのみレンダラーにブラウザ専用のコンポーネントを表示させることができます。
useIsBrowser
useIsBrowser()
フックを使用して、コンポーネントが現在ブラウザ環境にあるかどうかをテストすることもできます。SSRではfalse
を返し、最初のクライアントレンダリング後のCSRではtrue
を返します。クライアント側で特定の条件付き操作のみを実行する必要があり、まったく異なるUIをレンダリングする必要がない場合は、このフックを使用します。
import useIsBrowser from '@docusaurus/useIsBrowser';
function MyComponent() {
const isBrowser = useIsBrowser();
const location = isBrowser ? window.location.href : 'fetching location...';
return <span>{location}</span>;
}
useEffect
最後に、useEffect()
にロジックを記述して、最初のCSRまで実行を遅らせることができます。これは、副作用のみを実行していて、クライアントの状態からデータを取得していない場合に最も適切です。
function MyComponent() {
useEffect(() => {
// Only logged in the browser console; nothing is logged during server-side rendering
console.log("I'm now in the browser");
}, []);
return <span>Some content...</span>;
}
ExecutionEnvironment
ExecutionEnvironment
名前空間にはいくつかの値が含まれており、canUseDOM
はブラウザ環境を検出するための効果的な方法です。
これは、内部的にはtypeof window !== 'undefined'
をチェックしていることに注意してください。したがって、レンダリング関連のロジックには使用せず、Webリクエストを送信してユーザー入力に応答したり、DOMがまったく更新されないライブラリを動的にインポートしたりするなど、命令的なコードのみに使用する必要があります。
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
if (ExecutionEnvironment.canUseDOM) {
document.title = "I'm loaded!";
}