Skip to main content

クライアントアーキテクチャ

テーマのエイリアス

テーマは、例えば NavbarLayoutFooter などの一連のコンポーネントをエクスポートすることで、プラグインから渡されたデータをレンダリングして動作します。 Docusaurus and users use these components by importing them using the @theme webpack alias:

import Navbar from '@theme/Navbar';

The alias @theme can refer to a few directories, in the following priority:

  1. A user's website/src/theme directory, which is a special directory that has the higher precedence.
  2. A Docusaurus theme package's theme directory.
  3. Fallback components provided by Docusaurus core (usually not needed).

This is called a layered architecture: a higher-priority layer providing the component would shadow a lower-priority layer, making swizzling possible. Given the following structure:

website
├── node_modules
│ └── @docusaurus/theme-classic
│ └── theme
│ └── Navbar.js
└── src
└── theme
└── Navbar.js

website/src/theme/Navbar.js takes precedence whenever @theme/Navbar is imported. This behavior is called component swizzling. If you are familiar with Objective C where a function's implementation can be swapped during runtime, it's the exact same concept here with changing the target @theme/Navbar is pointing to!

We already talked about how the "userland theme" in src/theme can re-use a theme component through the @theme-original alias. One theme package can also wrap a component from another theme, by importing the component from the initial theme, using the @theme-init import.

Here's an example of using this feature to enhance the default theme CodeBlock component with a react-live playground feature.

import InitialCodeBlock from '@theme-init/CodeBlock';
import React from 'react';

export default function CodeBlock(props) {
return props.live ? (
<ReactLivePlayground {...props} />
) : (
<InitialCodeBlock {...props} />
);
}

Check the code of @docusaurus/theme-live-codeblock for details.

警告

Unless you want to publish a re-usable "theme enhancer" (like @docusaurus/theme-live-codeblock), you likely don't need @theme-init.

It can be quite hard to wrap your mind around these aliases. Let's imagine the following case with a super convoluted setup with three themes/plugins and the site itself all trying to define the same component. Internally, Docusaurus loads these themes as a "stack".

+-------------------------------------------------+
| `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
+-------------------------------------------------+

The components in this "stack" are pushed in the order of preset plugins > preset themes > plugins > themes > site, so the swizzled component in website/src/theme always comes out on top because it's loaded last.

@theme/* always points to the topmost component—when CodeBlock is swizzled, all other components requesting @theme/CodeBlock receive the swizzled version.

@theme-original/* always points to the topmost non-swizzled component. That's why you can import @theme-original/CodeBlock in the swizzled component—it points to the next one in the "component stack", a theme-provided one. Plugin authors should not try to use this because your component could be the topmost component and cause a self-import.

@theme-init/* always points to the bottommost component—usually, this comes from the theme or plugin that first provides this component. Individual plugins / themes trying to enhance code block can safely use @theme-init/CodeBlock to get its basic version. Site creators should generally not use this because you likely want to enhance the topmost instead of the bottommost component. It's also possible that the @theme-init/CodeBlock alias does not exist at all—Docusaurus only creates it when it points to a different one from @theme-original/CodeBlock, i.e. when it's provided by more than one theme. We don't waste aliases!

クライアントモジュール

クライアントモジュールは、テーマコンポーネントのようにサイトのバンドルに含まれています。 しかし、いつも副作用を持ち得ます。 クライアントモジュールは、Webpack によって import される CSS や JS などのあらゆるものです。 JS のスクリプトは通常、イベントリスナの登録、グローバル変数の作成など、グローバルコンテキストで動作します。

These modules are imported globally before React even renders the initial UI.

@docusaurus/core/App.tsx
// さて、一体どうやって動いているのでしょうか
import '@generated/client-modules';

Plugins and sites can both declare client modules, through getClientModules and siteConfig.clientModules, respectively.

Client modules are called during server-side rendering as well, so remember to check the execution environment before accessing client-side globals.

mySiteGlobalJs.js
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';

if (ExecutionEnvironment.canUseDOM) {
// サイトがブラウザにロードされたらすぐにグローバルのイベントリスナを登録
window.addEventListener('keydown', (e) => {
if (e.code === 'Period') {
location.assign(location.href.replace('.com', '.dev'));
}
});
}

CSS stylesheets imported as client modules are global.

mySiteGlobalCss.css
/* グローバルなスタイルシート */
.globalSelector {
color: red;
}

クライアントモジュールのライフサイクル

副作用をもたらすだけでなく、クライアントモジュールは必要に応じて onRouteUpdateonRouteDidUpdate という2つのライフサイクル関数をエクスポートできます。

Docusaurus はシングルページのアプリケーションをビルドするので、 script タグはページが読み込まれたときのみ実行され、ページ遷移時には再実行されません。 これらのライフサイクルは、DOM 要素の操作や分析データの送信など、新しいページがロードされるたびに実行される命令型の JS ロジックがある場合に便利です。

ルートが遷移するごとに、重要なタイミングがいくつかあります。

  1. ユーザがリンクをクリックすると、ルータは現在のロケーションを変更します。
  2. Docusaurus は現在のページの内容を表示し続けながら、次のルートのアセットをプリロードします。
  3. 次のルートのアセットがロードされました。
  4. 新しいロケーションのルートコンポーネントが DOM にレンダリングされます。

onRouteUpdate は (2) のタイミングで呼び出され、 onRouteDidUpdate は (4) で呼び出されます。 どちらも現在と直前のロケーションの情報を受け取ります(最初の画面だった場合は直前のロケーションは null になります)。

onRouteUpdate は "cleanup" コールバックを返すことができ、これは (3) で呼び出されます。 たとえば、プログレスバーを表示したい場合は onRouteUpdate で開始し、コールバックで解除できます。 (従来のテーマはすでにこのように nprogress インテグレーションを提供しています)。

新しいページの DOM は、(4) のタイミングでのみ利用可能であることに注意してください。 新しいページの DOM を操作する必要がある場合は、onRouteDidUpdate をお勧めします。 これは新しいページの DOM がマウントされるとすぐに呼び出されます。

myClientModule.js
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 を使用していて、コンテキスト型の型を活用したい場合は

myClientModule.ts
import type {ClientModule} from '@docusaurus/types';

const module: ClientModule = {
onRouteUpdate({location, previousLocation}) {
// ...
},
onRouteDidUpdate({location, previousLocation}) {
// ...
},
};
export default module;

どちらのライフサイクルも最初のレンダリング時に呼び出されますが、サーバ側では呼び出されないので安全にブラウザのグローバル変数にアクセスできます。

React を好むなら

クライアントモジュールのライフサイクルは純粋に命令型であり、React フックを使用したり、React コンテキストにアクセスすることはできません。 If your operations are state-driven or involve complicated DOM manipulations, you should consider swizzling components instead.