mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-28 17:57:48 +02:00
184 lines
9.1 KiB
Text
184 lines
9.1 KiB
Text
---
|
|
description: How the Docusaurus client is structured
|
|
---
|
|
|
|
# Client architecture
|
|
|
|
## Theme aliases {#theme-aliases}
|
|
|
|
A theme works by exporting a set of components, e.g. `Navbar`, `Layout`, `Footer`, to render the data passed down from plugins. Docusaurus and users use these components by importing them using the `@theme` webpack alias:
|
|
|
|
```js
|
|
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`](#wrapping) 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.
|
|
|
|
```js
|
|
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.
|
|
|
|
:::warning
|
|
|
|
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".
|
|
|
|
```text
|
|
+-------------------------------------------------+
|
|
| `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!
|
|
|
|
## Client modules {#client-modules}
|
|
|
|
Client modules are part of your site's bundle, just like theme components. However, they are usually side-effect-ful. Client modules are anything that can be `import`ed by Webpack—CSS, JS, etc. JS scripts usually work on the global context, like registering event listeners, creating global variables...
|
|
|
|
These modules are imported globally before React even renders the initial UI.
|
|
|
|
```js title="@docusaurus/core/App.tsx"
|
|
// How it works under the hood
|
|
import '@generated/client-modules';
|
|
```
|
|
|
|
Plugins and sites can both declare client modules, through [`getClientModules`](../api/plugin-methods/lifecycle-apis.mdx#getClientModules) and [`siteConfig.clientModules`](../api/docusaurus.config.js.mdx#clientModules), respectively.
|
|
|
|
Client modules are called during server-side rendering as well, so remember to check the [execution environment](./ssg.mdx#escape-hatches) before accessing client-side globals.
|
|
|
|
```js title="mySiteGlobalJs.js"
|
|
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 stylesheets imported as client modules are [global](../styling-layout.mdx#global-styles).
|
|
|
|
```css title="mySiteGlobalCss.css"
|
|
/* This stylesheet is global. */
|
|
.globalSelector {
|
|
color: red;
|
|
}
|
|
```
|
|
|
|
### Client module lifecycles {#client-module-lifecycles}
|
|
|
|
Besides introducing side-effects, client modules can optionally export two lifecycle functions: `onRouteUpdate` and `onRouteDidUpdate`.
|
|
|
|
Because Docusaurus builds a single-page application, `script` tags will only be executed the first time the page loads, but will not re-execute on page transitions. These lifecycles are useful if you have some imperative JS logic that should execute every time a new page has loaded, e.g., to manipulate DOM elements, to send analytics data, etc.
|
|
|
|
For every route transition, there will be several important timings:
|
|
|
|
1. The user clicks a link, which causes the router to change its current location.
|
|
2. Docusaurus preloads the next route's assets, while keeping displaying the current page's content.
|
|
3. The next route's assets have loaded.
|
|
4. The new location's route component gets rendered to DOM.
|
|
|
|
`onRouteUpdate` will be called at event (2), and `onRouteDidUpdate` will be called at (4). They both receive the current location and the previous location (which can be `null`, if this is the first screen).
|
|
|
|
`onRouteUpdate` can optionally return a "cleanup" callback, which will be called at (3). For example, if you want to display a progress bar, you can start a timeout in `onRouteUpdate`, and clear the timeout in the callback. (The classic theme already provides an `nprogress` integration this way.)
|
|
|
|
Note that the new page's DOM is only available during event (4). If you need to manipulate the new page's DOM, you'll likely want to use `onRouteDidUpdate`, which will be fired as soon as the DOM on the new page has mounted.
|
|
|
|
```js title="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;
|
|
}
|
|
```
|
|
|
|
Or, if you are using TypeScript and you want to leverage contextual typing:
|
|
|
|
```ts title="myClientModule.ts"
|
|
import type {ClientModule} from '@docusaurus/types';
|
|
|
|
const module: ClientModule = {
|
|
onRouteUpdate({location, previousLocation}) {
|
|
// ...
|
|
},
|
|
onRouteDidUpdate({location, previousLocation}) {
|
|
// ...
|
|
},
|
|
};
|
|
export default module;
|
|
```
|
|
|
|
Both lifecycles will fire on first render, but they will not fire on server-side, so you can safely access browser globals in them.
|
|
|
|
:::tip Prefer using React
|
|
|
|
Client module lifecycles are purely imperative, and you can't use React hooks or access React contexts within them. If your operations are state-driven or involve complicated DOM manipulations, you should consider [swizzling components](../swizzling.mdx) instead.
|
|
|
|
:::
|