Aller au contenu principal
Version : 2.4.3

Architecture client

Alias du thème

Un thème fonctionne en exportant un ensemble de composants, par exemple Navbar, Layout, Footer, pour rendre les données transmises à partir de plugins. Docusaurus et les utilisateurs utilisent ces composants en les important en utilisant l'alias de webpack @theme :

import Navbar from '@theme/Navbar';

L'alias @theme peut faire référence à quelques répertoires, selon la priorité suivante :

  1. Un répertoire website/src/theme d'un utilisateur, qui est un répertoire spécial qui a la priorité supérieure.
  2. Un répertoire theme des paquets du thème de Docusaurus.
  3. Des composants de secours fournis par le noyau de Docusaurus (généralement pas nécessaire).

C'est ce qu'on appelle une architecture en couches : une couche de priorité supérieure fournissant le composant ferait de l'ombre à une couche de priorité inférieure, rendant le swizzling possible. Considérons la structure suivante :

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

website/src/theme/Navbar.js est prioritaire lorsque @theme/Navbar est importé. Ce comportement est appelé « swizzling de composant ». Si vous êtes familier avec Objective C où l'implémentation d'une fonction peut être permutée pendant l'exécution, c'est exactement le même concept ici avec le changement de la cible vers laquelle @theme/Navbar pointe !

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.

attention

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

Il peut être assez difficile de se faire une idée de ces alias. Imaginons le cas suivant avec une configuration super alambiquée où trois thèmes/plugins et le site lui-même essaient tous de définir le même composant. En interne, Docusaurus charge ces thèmes comme une « pile ».

+-------------------------------------------------+
| `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. Les auteurs de plugins ne devraient pas essayer de l'utiliser car votre composant pourrait être le composant le plus haut et provoquer un auto-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. Nous ne gaspillons pas les alias !

Client modules

Les modules clients font partie de l'offre groupée de votre site, tout comme les composants du thème. Cependant, ils sont généralement accompagnés d'effets secondaires. Client modules are anything that can be imported by Webpack—CSS, JS, etc. Les scripts JS travaillent généralement sur le contexte global, comme l'enregistrement d'écouteurs d'événements, la création de variables globales...

Ces modules sont importés globalement avant même que React ne rende l'interface utilisateur initiale.

@docusaurus/core/App.tsx
// Comment ça marche sous le capot
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) {
// 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.

mySiteGlobalCss.css
/* This stylesheet is global. */
.globalSelector {
color: red;
}

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. Ces cycles de vie sont utiles si vous avez une logique JS impérative qui doit s'exécuter chaque fois qu'une nouvelle page est chargée, par exemple pour manipuler des éléments DOM, pour envoyer des données analytiques, etc.

Pour chaque transition de route, il y aura plusieurs moments importants :

  1. L'utilisateur clique sur un lien, ce qui fait que le routeur change son emplacement actuel.
  2. Docusaurus précharge les ressources de la route suivante, tout en conservant l'affichage du contenu de la page courante.
  3. Les ressources de la route suivante ont été chargées.
  4. Le composant de la route du nouvel emplacement est rendu dans le 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.)

Notez que le DOM de la nouvelle page n'est disponible que pendant l'événement (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.

myClientModule.js
import type {Location} from 'history';

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;
}

Ou, si vous utilisez TypeScript et que vous souhaitez tirer parti du typage contextuel :

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

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

Les deux cycles de vie se déclencheront au premier rendu, mais pas du côté serveur, de sorte que vous pouvez y accéder sans risque aux globaux du navigateur.

Prefer using React

Les cycles de vie des modules clients sont purement impératifs, et vous ne pouvez pas utiliser les hooks React ou accéder aux contextes React en leur sein. If your operations are state-driven or involve complicated DOM manipulations, you should consider swizzling components instead.