客户端架构
主题别名
主题通过导出一组组件(例如 Navbar、Layout、Footer)来渲染从插件传递下来的数据。Docusaurus 和用户通过使用 @theme webpack 别名来使用这些组件:
import Navbar from '@theme/Navbar';
@theme 别名可以引用几个目录,按以下优先级:
- 用户的
website/src/theme目录,这是一个具有更高优先级的特殊目录。 - Docusaurus 主题包的
theme目录。 - 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 导入来包装另一个主题的组件。
下面是一个使用此功能通过 react-live 游乐场功能增强默认主题 CodeBlock 组件的示例。
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` 始终指向顶部
+-------------------------------------------------+
| `theme-live-codeblock/theme/CodeBlock/index.js` | <-- `@theme-original/CodeBlock` 指向最顶层未交换的组件
+-------------------------------------------------+
| `plugin-awesome-codeblock/theme/CodeBlock.js` |
+-------------------------------------------------+
| `theme-classic/theme/CodeBlock/index.js` | <-- `@theme-init/CodeBlock` 始终指向底部
+-------------------------------------------------+
这个"堆栈"中的组件按 预设插件 > 预设主题 > 插件 > 主题 > 站点 的顺序推送,因此 website/src/theme 中的交换组件总是因为最后加载而位于顶部。
@theme/* 始终指向最顶层的组件 - 当 CodeBlock 被交换时,所有请求 @theme/CodeBlock 的其他组件都会收到交换版本。
@theme-original/* 始终指向最顶层未交换的组件。这就是为什么您可以在交换的组件中导入 @theme-original/CodeBlock - 它指向"组件堆栈"中的下一个,一个主题提供的组件。插件作者不应尝试使用此功能,因为您的组件可能是最顶层组件,并导致自我导入。
@theme-init/* 始终指向最底层的组件 - 通常,这来自首次提供此组件的主题或插件。单个插件/主题尝试增强代码块可以安全地使用 @theme-init/CodeBlock 获取其基本版本。站点创建者通常不应使用此功能,因为您可能希望增强_最顶层_而不是_最底层_的组件。还有可能 @theme-init/CodeBlock 别名根本不存在 - Docusaurus 仅在它指向与 @theme-original/CodeBlock 不同的组件时创建它,即当它由多个主题提供时。我们不浪费别名!
客户端模块
客户端模块是站点捆绑包的一部分,就像主题组件一样。但是,它们通常具有副作用。客户端模块是可以被 Webpack import 的任何内容 - CSS、JS 等。JS 脚本通常在全局上下文中工作,如注册事件监听器、创建全局变量...
这些模块在 React 甚至渲染初始 UI 之前全局导入。
// 底层工作原理
import '@generated/client-modules';
插件和站点都可以通过 getClientModules 和 siteConfig.clientModules 声明客户端模块。
客户端模块在服务器端渲染期间也会被调用,因此请记住在访问客户端全局变量之前检查执行环境。
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
if (ExecutionEnvironment.canUseDOM) {
// 站点加载到浏览器后立即注册全局事件监听器
window.addEventListener('keydown', (e) => {
if (e.code === 'Period') {
location.assign(location.href.replace('.com', '.dev'));
}
});
}
作为客户端模块导入的 CSS 样式表是全局的。
/* 这是全局样式表。 */
.globalSelector {
color: red;
}
客户端模块生命周期
除了引入副作用外,客户端模块还可以可选地导出两个生命周期函数:onRouteUpdate 和 onRouteDidUpdate。
因为 Docusaurus 构建了一个单页应用,script 标签只会在页面第一次加载时执行,但不会在页面切换时重新执行。这些生命周期在您有一些命令式 JS 逻辑需要在每次新页面加载时执行时很有用,例如操作 DOM 元素、发送分析数据等。
对于每个路由转换,都会有几个重要的时间点:
- 用户点击链接,导致路由器改变其当前位置。
- Docusaurus 预加载下一个路由的资源,同时继续显示当前页面的内容。
- 下一个路由的资源已加载。
- 新位置的路由组件被渲染到 DOM。
onRouteUpdate 将在事件 (2) 时被调用,onRouteDidUpdate 将在事件 (4) 时被调用。它们都接收当前位置和前一个位置(如果这是第一个屏幕,则可以为 null)。
onRouteUpdate 可以可选地返回一个在事件 (3) 时调用的"清理"回调。例如,如果您想显示进度条,可以在 onRouteUpdate 中启动一个超时,并在回调中清除超时。(经典主题已经通过这种方式提供了 nprogress 集成。)
请注意,新页面的 DOM 仅在事件 (4) 期间可用。如果您需要操作新页面的 DOM,您可能需要使用 onRouteDidUpdate,它将在新页面的 DOM 挂载后立即触发。
export function onRouteDidUpdate({location, previousLocation}) {
// 如果我们仍在同一页面,则不执行;生命周期可能是因为哈希值变化(例如在标题之间导航)而触发
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 并希望利用上下文类型:
import type {ClientModule} from '@docusaurus/types';
const module: ClientModule = {
onRouteUpdate({location, previousLocation}) {
// ...
},
onRouteDidUpdate({location, previousLocation}) {
// ...
},
};
export default module;
两个生命周期都将在第一次渲染时触发,但它们不会在服务器端触发,因此您可以安全地在其中访问浏览器全局变量。
客户端模块生命周期是纯命令式的,您无法在其中使用 React 钩子或访问 React 上下文。如果您的操作是状态驱动的或涉及复杂的 DOM 操作,您应该考虑交换组件。