静的サイトの生成 (SSG)
In architecture, we mentioned that the theme is run in Webpack. しかし、それは常にブラウザのグローバルにアクセスできるという意味ではないことに注意が必要です。 以下のように、テーマは2回ビルドされます。
- During server-side rendering, the theme is compiled in a sandbox called React DOM Server. You can see this as a "headless browser", where there is no
windowordocument, only React. SSR は静的な HTML ページを生成します。 - During client-side rendering, the theme is compiled to JavaScript that gets eventually executed in the browser, so it has access to browser variables.
Server-side rendering and static site generation can be different concepts, but we use them interchangeably.
厳密に言えば、Docusaurus は静的サイトジェネレータです。 サーバサイドのランタイムがないため、リクエストごとに動的に事前にレンダリングするのではなく、CDN にデプロイされる HTML ファイルに静的にレンダリングします。 This differs from the working model of Next.js.
Therefore, while you probably know not to access Node globals like process (or can we?) or the 'fs' module, you can't freely access browser globals either.
import React from 'react';
export default function WhereAmI() {
return <span>{window.location.href}</span>;
}
This looks like idiomatic React, but if you run docusaurus build, you will get an error:
ReferenceError: window is not defined
This is because during server-side rendering, the Docusaurus app isn't actually run in browser, and it doesn't know what window is.
What about process.env.NODE_ENV?
One exception to the "no Node globals" rule is process.env.NODE_ENV. 実際、この変数は React で使用できます。Webpack がこの変数をグローバルなものとして注入するためです。
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}</>;
}
During Webpack build, the process.env.NODE_ENV will be replaced with the value, either 'development' or 'production'. デッドコードの除去後、それぞれに応じたビルド結果を得るでしょう。
- 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 サイトはたいてい静的なコンテンツを含んでいるため、(React を実行するような) JavaScript なしで、プレーンな HTML/CSS のみで動作する必要があります。 React コードを動的なコンテンツなしで静的に HTML にレンダリングすることができるのも、サーバサイドレンダリングによるものです。 HTML ファイルはクライアントの状態という概念を持たないため (つまり純粋なマークアップです)、ブラウザの API に依存するべきではありません。
These HTML files are the first to arrive at the user's browser screen when a URL is visited (see routing). ブラウザはその後、他のJavaScriptコードを取得して実行し、サイトの「動的」な部分—JavaScriptで実装されたすべての機能—を提供します。 しかし、その前にページの主要なコンテンツはすでに表示されているため、より高速な読み込みが可能になっています。
CSR専用のアプリでは、すべてのDOM要素がクライアント側でReactによって生成され、HTMLファイルにはReactがマウントするためのルート要素のみが含まれます。一方SSRでは、Reactは既に構築された完全なHTMLページを受け取り、そのDOM要素を自身の仮想DOMと照合するだけで済みます。 この処理は「ハイドレーション」と呼ばれます。 Reactが静的マークアップのハイドレーションを完了すると、アプリは通常のReactアプリとして動作を開始します。
Note that Docusaurus is ultimately a single-page application, so static site generation is only an optimization (progressive enhancement, as it's called), but our functionality does not fully depend on those HTML files. This is contrary to site generators like Jekyll and Docusaurus v1, where all files are statically transformed to markup, and interactiveness is added through external JavaScript linked with <script> tags. If you inspect the build output, you will still see JS assets under build/assets/js, which are, really, the core of Docusaurus.
エスケープハッチ(抜け道・回避策)
もし、画面上でブラウザAPIに依存した動的コンテンツをレンダリングしたい場合、例えば:
- Our live codeblock, which runs in the browser's JS runtime
- Our themed image that detects the user's color scheme to display different images
- The JSON viewer of our debug panel which uses the
windowglobal for styling
クライアントの状態を把握できなければ、静的なHTMLでは有用な表示ができないため、SSR(サーバーサイドレンダリング)から抜け出す必要がある場合があります。
最初のクライアント側レンダリングがサーバーサイドレンダリングとまったく同じDOM構造を生成することが重要です。そうでないと、Reactが仮想DOMと誤ったDOM要素を関連付けてしまいます。
Therefore, the naïve attempt of if (typeof window !== 'undefined) {/* render something */} won't work appropriately as a browser vs. server detection, because the first client render would instantly render different markup from the server-generated one.
You can read more about this pitfall in The Perils of Rehydration.
SSRから抜け出すための、より信頼性の高い方法をいくつか用意しています。
<BrowserOnly>
If you need to render some component in browser only (for example, because the component relies on browser specifics to be functional at all), one common approach is to wrap your component with <BrowserOnly> to make sure it's invisible during SSR and only rendered in 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>
);
}
It's important to realize that the children of <BrowserOnly> is not a JSX element, but a function that returns an element. これは設計上の判断です。 以下のコードを考えてみましょう:
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>
);
}
While you may expect that BrowserOnly hides away the children during server-side rendering, it actually can't. When the React renderer tries to render this JSX tree, it does see the {window.location.href} variable as a node of this tree and tries to render it, although it's actually not used! 関数を使うことで、レンダラーにブラウザ専用コンポーネントを必要なときだけ認識させることができます。
useIsBrowser
You can also use the useIsBrowser() hook to test if the component is currently in a browser environment. It returns false in SSR and true is CSR, after first client render. このフックは、クライアント側でのみ特定の条件付き処理を行いたい場合に使い、まったく異なるUIをレンダリングする必要がないときに適しています。
import useIsBrowser from '@docusaurus/useIsBrowser';
function MyComponent() {
const isBrowser = useIsBrowser();
const location = isBrowser ? window.location.href : 'fetching location...';
return <span>{location}</span>;
}
useEffect
Lastly, you can put your logic in useEffect() to delay its execution until after first CSR. This is most appropriate if you are only performing side-effects but don't get data from the client state.
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
The ExecutionEnvironment namespace contains several values, and canUseDOM is an effective way to detect browser environment.
Beware that it essentially checked typeof window !== 'undefined' under the hood, so you should not use it for rendering-related logic, but only imperative code, like reacting to user input by sending web requests, or dynamically importing libraries, where DOM isn't updated at all.
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
if (ExecutionEnvironment.canUseDOM) {
document.title = "ロード完了!";
}