客户端架构
主题别名
主题通过导出一组组件(例如 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 操作,您应该考虑交换组件。