客户端架构
主题别名
主题通过导出组件集(例如 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
website/src/theme/Navbar.js
始终优先于 @theme/Navbar
的导入。这种行为称为组件 swizzling。如果您熟悉 Objective C,其中函数的实现可以在运行时进行交换,那么这里也是相同的概念,改变 @theme/Navbar
指向的目标!
我们已经讨论了 src/theme
中的“用户空间主题”如何通过 @theme-original
别名重用主题组件。一个主题包也可以包装另一个主题的组件,方法是从初始主题导入组件,使用 @theme-init
导入。
以下是如何使用此功能来增强默认主题 CodeBlock
组件的 react-live
游乐场功能的示例。
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
+-------------------------------------------------+
此“堆栈”中的组件按 预设插件 > 预设主题 > 插件 > 主题 > 站点
的顺序推送,因此 website/src/theme
中的 swizzling 组件始终位于最顶端,因为它最后加载。
@theme/*
始终指向最顶端的组件——当 CodeBlock
被 swizzling 时,所有其他请求 @theme/CodeBlock
的组件都会收到 swizzling 版本。
@theme-original/*
始终指向最顶端的非 swizzling 组件。这就是为什么您可以在 swizzling 组件中导入 @theme-original/CodeBlock
——它指向“组件堆栈”中的下一个组件,一个主题提供的组件。插件作者不应该尝试使用它,因为您的组件可能是最顶层的组件,会导致自导入。
@theme-init/*
始终指向最底层的组件——通常,它来自首先提供此组件的主题或插件。尝试增强代码块的单个插件/主题可以安全地使用 @theme-init/CodeBlock
来获取其基本版本。站点创建者通常不应该使用它,因为您可能希望增强最顶层而不是最底层组件。@theme-init/CodeBlock
别名也可能根本不存在——Docusaurus 仅在它指向与 @theme-original/CodeBlock
不同的别名时创建它,即当它由多个主题提供时。我们不浪费别名!
客户端模块
客户端模块是您站点包的一部分,就像主题组件一样。但是,它们通常是有副作用的。客户端模块是任何可以通过 Webpack import
的内容——CSS、JS 等。JS 脚本通常在全局上下文中工作,例如注册事件监听器、创建全局变量...
这些模块在 React 甚至渲染初始 UI 之前就被全局导入。
// How it works under the hood
import '@generated/client-modules';
插件和站点都可以通过 getClientModules
和 siteConfig.clientModules
分别声明客户端模块。
客户端模块在服务器端渲染期间也会被调用,因此请记住在访问客户端全局变量之前检查 执行环境。
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 样式表是 全局 的。
/* This stylesheet is global. */
.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}) {
// 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 并且想要利用上下文类型
import type {ClientModule} from '@docusaurus/types';
const module: ClientModule = {
onRouteUpdate({location, previousLocation}) {
// ...
},
onRouteDidUpdate({location, previousLocation}) {
// ...
},
};
export default module;
这两个生命周期都将在首次渲染时触发,但它们不会在服务器端触发,因此您可以在其中安全地访问浏览器全局变量。
客户端模块生命周期纯粹是命令式的,您无法在其中使用 React 钩子或访问 React 上下文。如果您的操作是状态驱动的或涉及复杂的 DOM 操作,您应该考虑 swizzling 组件。