クライアントアーキテクチャ
テーマエイリアス
テーマは、プラグインから渡されたデータをレンダリングするために、`Navbar`、`Layout`、`Footer`などのコンポーネントのセットをエクスポートすることで機能します。Docusaurusとユーザーは、`@theme` webpackエイリアスを使用してインポートすることでこれらのコンポーネントを使用します。
import Navbar from '@theme/Navbar';
エイリアス`@theme`は、次の優先順位でいくつかのディレクトリを参照できます。
- ユーザーの`website/src/theme`ディレクトリ。これは、より高い優先順位を持つ特別なディレクトリです。
- Docusaurusのテーマパッケージの`theme`ディレクトリ。
- Docusaurusコアによって提供されるフォールバックコンポーネント(通常は不要)。
これは*階層化アーキテクチャ*と呼ばれます。より優先度の高いレイヤーがコンポーネントを提供する場合、より優先度の低いレイヤーをシャドウイングすることで、スウィズリングが可能になります。次の構造が与えられた場合
website
├── node_modules
│ └── @docusaurus/theme-classic
│ └── theme
│ └── Navbar.js
└── src
└── theme
└── Navbar.js
`website/src/theme/Navbar.js`は、`@theme/Navbar`がインポートされるたびに優先されます。この動作はコンポーネントスウィズリングと呼ばれます。実行時に関数の実装を入れ替えることができるObjective Cに精通している場合は、`@theme/Navbar`が指すターゲットを変更するという点で、ここではまったく同じ概念です!
すでに`src/theme`の「ユーザーランドテーマ」が、`@theme-original`エイリアスを使用してテーマコンポーネントを再利用する方法について説明しました。1つのテーマパッケージは、`@theme-init`インポートを使用して、初期テーマからコンポーネントをインポートすることで、別のテーマのコンポーネントをラップすることもできます。
この機能を使用して、デフォルトテーマの`CodeBlock`コンポーネントを`react-live`プレイグラウンド機能で強化する例を次に示します。
import InitialCodeBlock from '@theme-init/CodeBlock';
import React from 'react';
export default function CodeBlock(props) {
return props.live ? (
<ReactLivePlayground {...props} />
) : (
<InitialCodeBlock {...props} />
);
}
詳細については、`@docusaurus/theme-live-codeblock`のコードを確認してください。
再利用可能な「テーマエンハンサー」(`@docusaurus/theme-live-codeblock`など)を公開したい場合を除き、おそらく`@theme-init`は必要ありません。
これらのエイリアスについて理解するのは非常に難しい場合があります。3つのテーマ/プラグインとサイト自体がすべて同じコンポーネントを定義しようとしている、非常に複雑なセットアップの次のケースを想像してみましょう。内部的には、Docusaurusはこれらのテーマを「スタック」としてロードします。
+-------------------------------------------------+
| `website/src/theme/CodeBlock.js` | <-- `@theme/CodeBlock` always points to the top
+-------------------------------------------------+
| `theme-live-codeblock/theme/CodeBlock/index.js` | <-- `@theme-original/CodeBlock` points to the topmost non-swizzled component
+-------------------------------------------------+
| `plugin-awesome-codeblock/theme/CodeBlock.js` |
+-------------------------------------------------+
| `theme-classic/theme/CodeBlock/index.js` | <-- `@theme-init/CodeBlock` always points to the bottom
+-------------------------------------------------+
この「スタック」のコンポーネントは、`preset plugins > preset themes > plugins > themes > site`の順序でプッシュされるため、`website/src/theme`のスウィズリングされたコンポーネントは、最後にロードされるため、常に最上位になります。
`@theme/*`は常に最上位のコンポーネントを指します。`CodeBlock`がスウィズリングされると、`@theme/CodeBlock`を要求する他のすべてのコンポーネントは、スウィズリングされたバージョンを受け取ります。
`@theme-original/*`は常に最上位の非スウィズリングコンポーネントを指します。そのため、スウィズリングされたコンポーネントで`@theme-original/CodeBlock`をインポートできます。これは、「コンポーネントスタック」の次のコンポーネント(テーマが提供するコンポーネント)を指します。プラグイン作成者は、コンポーネントが最上位のコンポーネントになり、自己インポートを引き起こす可能性があるため、これを使用しないでください。
`@theme-init/*`は常に最下位のコンポーネントを指します。通常、これはこのコンポーネントを最初に提供するテーマまたはプラグインからのものです。コードブロックを強化しようとする個々のプラグイン/テーマは、`@theme-init/CodeBlock`を安全に使用して、その基本バージョンを取得できます。サイト作成者は、通常、*最下位*のコンポーネントではなく、*最上位*のコンポーネントを強化したい可能性が高いため、これを使用しないでください。`@theme-init/CodeBlock`エイリアスが存在しない可能性もあります。Docusaurusは、`@theme-original/CodeBlock`とは異なるものを指す場合、つまり複数のテーマによって提供される場合にのみ、それを作成します。エイリアスを無駄にしません!
クライアントモジュール
クライアントモジュールは、テーマコンポーネントと同様に、サイトのバンドルの一部です。ただし、通常、副作用があります。クライアントモジュールは、Webpackで`import`できるもの(CSS、JSなど)です。JSスクリプトは通常、イベントリスナーの登録、グローバル変数の作成など、グローバルコンテキストで機能します。
これらのモジュールは、Reactが最初のUIをレンダリングする前にグローバルにインポートされます。
// How it works under the hood
import '@generated/client-modules';
プラグインとサイトの両方が、`getClientModules`と`siteConfig.clientModules`を使用して、クライアントモジュールを宣言できます。
クライアントモジュールはサーバーサイドレンダリング中にも呼び出されるため、クライアントサイドのグローバル変数にアクセスする前に、実行環境を確認してください。
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
if (ExecutionEnvironment.canUseDOM) {
// As soon as the site loads in the browser, register a global event listener
window.addEventListener('keydown', (e) => {
if (e.code === 'Period') {
location.assign(location.href.replace('.com', '.dev'));
}
});
}
クライアントモジュールとしてインポートされたCSSスタイルシートはグローバルです。
/* This stylesheet is global. */
.globalSelector {
color: red;
}
クライアントモジュールのライフサイクル
クライアントモジュールは、副作用の導入に加えて、オプションで2つのライフサイクル関数(`onRouteUpdate`と`onRouteDidUpdate`)をエクスポートできます。
Docusaurusはシングルページアプリケーションを構築するため、`script`タグはページが最初にロードされたときにのみ実行され、ページ遷移時に再実行されません。これらのライフサイクルは、新しいページがロードされるたびに実行される必要がある命令型JSロジック(DOM要素の操作、分析データの送信など)がある場合に役立ちます。
すべてのルート遷移には、いくつかの重要なタイミングがあります
- ユーザーがリンクをクリックすると、ルーターが現在の場所を変更します。
- Docusaurusは、現在のページの内容を表示したまま、次のルートのアセットをプリロードします。
- 次のルートのアセットがロードされました。
- 新しい場所のルートコンポーネントがDOMにレンダリングされます。
`onRouteUpdate`はイベント(2)で呼び出され、`onRouteDidUpdate`は(4)で呼び出されます。どちらも現在の場所と前の場所(これが最初の画面の場合は`null`になる可能性があります)を受け取ります。
`onRouteUpdate`はオプションで「クリーンアップ」コールバックを返すことができます。これは(3)で呼び出されます。たとえば、プログレスバーを表示する場合は、`onRouteUpdate`でタイムアウトを開始し、コールバックでタイムアウトをクリアできます。(クラシックテーマでは、この方法で`nprogress`統合がすでに提供されています。)
新しいページのDOMは、イベント(4)でのみ使用できることに注意してください。新しいページのDOMを操作する必要がある場合は、新しいページのDOMがマウントされるとすぐに起動する`onRouteDidUpdate`を使用するとよいでしょう。
export function onRouteDidUpdate({location, previousLocation}) {
// Don't execute if we are still on the same page; the lifecycle may be fired
// because the hash changes (e.g. when navigating between headings)
if (location.pathname !== previousLocation?.pathname) {
const title = document.getElementsByTagName('h1')[0];
if (title) {
title.innerText += '❤️';
}
}
}
export function onRouteUpdate({location, previousLocation}) {
if (location.pathname !== previousLocation?.pathname) {
const progressBarTimeout = window.setTimeout(() => {
nprogress.start();
}, delay);
return () => window.clearTimeout(progressBarTimeout);
}
return undefined;
}
または、TypeScriptを使用してコンテキスト型指定を活用する場合は
import type {ClientModule} from '@docusaurus/types';
const module: ClientModule = {
onRouteUpdate({location, previousLocation}) {
// ...
},
onRouteDidUpdate({location, previousLocation}) {
// ...
},
};
export default module;
両方のライフサイクルは最初のレンダリング時に発生しますが、サーバーサイドでは発生しないため、ライフサイクル内でブラウザのグローバル変数に安全にアクセスできます。
クライアントモジュールのライフサイクルは完全に命令型であり、Reactフックを使用したり、ライフサイクル内でReactコンテキストにアクセスしたりすることはできません。操作が状態駆動型である場合、または複雑なDOM操作が含まれる場合は、代わりにコンポーネントのスウィズリングを検討する必要があります。