Skip to main content
Version: 3.5.1

客户端架构

¥Client architecture

主题别名

¥Theme aliases

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

¥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:

import Navbar from '@theme/Navbar';

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

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

  1. 用户的 website/src/theme 目录,是一个特殊的目录,具有较高的优先级。

    ¥A user's website/src/theme directory, which is a special directory that has the higher precedence.

  2. Docusaurus 主题包的 theme 目录。

    ¥A Docusaurus theme package's theme directory.

  3. Docusaurus 核心提供的后备组件(通常不需要)。

    ¥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

每当导入 @theme/Navbar 时,website/src/theme/Navbar.js 优先。这种行为称为组件调配。如果你熟悉 Objective C,其中函数的实现可以在运行时交换,那么这里的概念与更改 @theme/Navbar 指向的目标完全相同!

¥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!

我们已经讨论过 src/theme 中的 "用户态主题" 如何通过 @theme-original 别名重用主题组件。一个主题包还可以通过使用 @theme-init 导入从初始主题导入组件来封装另一个主题的组件。

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

以下是使用此功能通过 react-live Playground 功能增强默认主题 CodeBlock 组件的示例。

¥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} />
);
}

详情请查看 @docusaurus/theme-live-codeblock 代码。

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

警告

除非你想发布可重复使用的 "主题增强器"(如 @docusaurus/theme-live-codeblock),否则你可能不需要 @theme-init

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

很难理解这些别名。让我们想象一下以下情况,其中包含三个主题/插件的超级复杂设置,并且站点本身都试图定义相同的组件。在内部,Docusaurus 将这些主题加载为 "stack"。

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

"stack" 中的组件按 preset plugins > preset themes > plugins > themes > site 的顺序推送,因此 website/src/theme 中的混合组件始终位于顶部,因为它是最后加载的。

¥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/* 始终指向最顶层的组件 - 当 CodeBlock 被调配时,所有其他请求 @theme/CodeBlock 的组件都会收到调配后的版本。

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

@theme-original/* 始终指向最上面的非混合组件。这就是为什么你可以在 swizzled 组件中导入 @theme-original/CodeBlock — 它指向 "组件堆栈" 中的下一个组件,即主题提供的组件。插件作者不应尝试使用此组件,因为你的组件可能是最顶层的组件并导致自导入。

¥@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/* 始终指向最底层的组件 - 通常,这来自第一个提供该组件的主题或插件。尝试增强代码块的各个插件/主题可以安全地使用 @theme-init/CodeBlock 来获取其基本版本。网站创建者通常不应使用此组件,因为你可能想要增强最顶部而不是最底部的组件。也有可能 @theme-init/CodeBlock 别名根本不存在 - Docusaurus 仅当它指向与 @theme-original/CodeBlock 不同的别名时(即,当它由多个主题提供时)才会创建它。我们不会浪费别名!

¥@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

客户端模块是站点包的一部分,就像主题组件一样。然而,它们通常是有副作用的。客户端模块是 Webpack 可以实现 import 的任何内容 — CSS、JS 等。JS 脚本通常在全局上下文中工作,例如注册事件监听器、创建全局变量...

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

这些模块在 React 渲染初始 UI 之前就已全局导入。

¥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';

插件和站点都可以分别通过 getClientModulessiteConfig.clientModules 声明客户端模块。

¥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 样式表是 global

¥CSS stylesheets imported as client modules are global.

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

客户端模块生命周期

¥Client module lifecycles

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

¥Besides introducing side-effects, client modules can optionally export two lifecycle functions: onRouteUpdate and onRouteDidUpdate.

由于 Docusaurus 构建的是单页面应用,因此 script 标签只会在页面第一次加载时执行,但不会在页面转换时重新执行。如果你有一些必须在每次加载新页面时执行的命令式 JS 逻辑(例如,操作 DOM 元素、发送分析数据等),那么这些生命周期非常有用。

¥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 会预加载下一个路由的资源,同时保持显示当前页面的内容。

    ¥Docusaurus preloads the next route's assets, while keeping displaying the current page's content.

  3. 下一条路由的资源已加载。

    ¥The next route's assets have loaded.

  4. 新位置的路由组件被渲染到 DOM。

    ¥The new location's route component gets rendered to DOM.

onRouteUpdate 将在事件 (2) 处被调用,onRouteDidUpdate 将在事件 (4) 处被调用。它们都接收当前位置和先前位置(如果这是第一个屏幕,则可以是 null)。

¥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 可以选择返回 "cleanup" 回调,该回调将在 (3) 处调用。例如,如果要显示进度条,可以在 onRouteUpdate 开始超时,并在回调中清除超时。(经典主题已经以这种方式提供了 nprogress 集成。)

¥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.)

请注意,新页面的 DOM 仅在事件 (4) 期间可用。如果你需要操作新页面的 DOM,你可能需要使用 onRouteDidUpdate,它将在新页面上的 DOM 安装后立即触发。

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

或者,如果你正在使用 TypeScript 并且想要利用上下文输入:

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

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

¥Both lifecycles will fire on first render, but they will not fire on server-side, so you can safely access browser globals in them.

更喜欢使用 React

客户端模块生命周期是纯粹命令式的,你不能使用 React hooks 或访问其中的 React 上下文。如果你的操作是状态驱动的或涉及复杂的 DOM 操作,你应该考虑 混合组件

¥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 instead.