跳到主要内容
版本:Canary 🚧

客户端架构

主题的别名

主题通过导出一组组件来工作,例如:Navbar, Layout, Footer,渲染从插件传递下来的数据。 Docusaurus 和用户通过使用@theme webpack 别名导入这些组件来使用它们:

import Navbar from "@theme/Navbar";

别名@theme可以引用几个目录,优先级如下:

  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!

客户端模块

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 imported 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.

@docusaurus/core/App.tsx
// How it works under the hood
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;
}

客户端模块生命周期

除了引入副作用,客户端模块还可以选择导出两个生命周期函数:onRouteUpdateonroutediduupdate

因为 Docusaurus 构建的是单页面应用程序,所以script标签只会在页面第一次加载时执行,而不会在页面转换时重新执行。这些生命周期是有用的,如果你有一些命令式的 JS 逻辑,应该在每次加载新页面时执行,例如,操作 DOM 元素,发送分析数据等。

对于每个路线转换,将有几个重要的时间安排:

  1. 用户单击链接,导致路由器改变其当前位置。
  2. Docusaurus 预加载下一个路由的资源,同时保持显示当前页面的内容。
  3. 下一条路由的资源已经加载完毕。
  4. 新位置的路由组件被渲染到 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.

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:

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

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

这两个生命周期都将在第一次渲染时触发,但不会在服务器端触发,因此您可以安全地在其中访问浏览器全局变量。

Prefer using React

客户端模块的生命周期是完全命令式的,你不能在其中使用 React 钩子或访问 React 上下文。如果您的操作是状态驱动的或涉及复杂的 DOM 操作,则应该考虑使用搅拌组件