跳到主要内容
版本:3.8.1

客户端架构

主题别名

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

import Navbar from '@theme/Navbar';

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

  1. 用户的 website/src/theme 目录,这是一个具有更高优先级的特殊目录。
  2. Docusaurus 主题包的 theme 目录。
  3. Docusaurus 核心提供的后备组件(通常不需要)。

这被称为分层架构:提供组件的较高优先级层会遮蔽较低优先级层,从而实现组件替换(swizzling)。给定以下结构:

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

每当导入 @theme/Navbar 时,website/src/theme/Navbar.js 都会优先。这种行为称为组件替换 (component swizzling)。如果你熟悉 Objective C 中函数实现可以在运行时互换的概念,那么这里改变 @theme/Navbar 指向目标的概念与此完全相同!

我们已经讨论了 src/theme 中的“用户态主题”如何通过 @theme-original 别名复用主题组件。一个主题包也可以通过导入原始主题中的组件来包装另一个主题的组件,这需要使用 @theme-init 导入。

这是一个使用此功能增强默认主题 CodeBlock 组件,使其具备 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 的代码。

警告

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

要完全理解这些别名可能会很困难。让我们想象一个非常复杂的场景,其中有三个主题/插件和网站本身都尝试定义相同的组件。在内部,Docusaurus 将这些主题作为“堆栈”加载。

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

这个“堆栈”中的组件按照 preset plugins > preset themes > plugins > themes > site 的顺序被推入,因此 website/src/theme 中的组件替换(swizzled component)总是处于最顶层,因为它最后加载。

@theme/* 总是指向最顶层的组件——当 CodeBlock 被替换(swizzled)时,所有请求 @theme/CodeBlock 的其他组件都会收到被替换的版本。

@theme-original/* 总是指向最顶层未被替换(non-swizzled)的组件。这就是为什么你可以在被替换的组件中导入 @theme-original/CodeBlock——它指向“组件堆栈”中的下一个,一个由主题提供的组件。插件作者不应该尝试使用这个,因为你的组件可能就是最顶层的组件,从而导致自我导入。

@theme-init/* 总是指向最底层的组件——通常,它来自最初提供此组件的主题或插件。尝试增强代码块的各个插件/主题可以安全地使用 @theme-init/CodeBlock 来获取其基本版本。网站创建者通常不应使用此别名,因为您可能希望增强最顶层而不是最底层的组件。此外,@theme-init/CodeBlock 别名也可能根本不存在——Docusaurus 仅在它指向与 @theme-original/CodeBlock 不同的组件时才创建它,即当它由多个主题提供时。我们不会浪费别名!

客户端模块

客户端模块是您网站捆绑包的一部分,就像主题组件一样。但是,它们通常具有副作用。客户端模块是任何可以被 Webpack import 的东西——CSS、JS 等。JS 脚本通常在全局上下文中工作,例如注册事件监听器、创建全局变量……

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

@docusaurus/core/App.tsx
// How it works under the hood
import '@generated/client-modules';

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

客户端模块在服务器端渲染期间也会被调用,因此在访问客户端全局变量之前,请记住检查执行环境

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 样式表是全局的

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

客户端模块生命周期

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

由于 Docusaurus 构建的是单页应用,script 标签只会在页面首次加载时执行,而不会在页面切换时重新执行。如果您有一些命令式 JS 逻辑需要在每次新页面加载时执行,例如操作 DOM 元素、发送分析数据等,这些生命周期就非常有用。

对于每次路由切换,都会有几个重要的时间点:

  1. 用户点击链接,导致路由器更改当前位置。
  2. Docusaurus 预加载下一个路由的资源,同时继续显示当前页面的内容。
  3. 下一个路由的资源已加载。
  4. 新位置的路由组件渲染到 DOM。

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

onRouteUpdate 可以选择返回一个“清理”回调函数,该函数将在 (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 Hooks 或访问 React 上下文。如果您的操作是状态驱动或涉及复杂的 DOM 操作,您应该考虑替换组件(swizzling components)