客户端架构
主题别名
主题通过导出组件集(例如 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 导入来包装另一个主题的组件。
以下是如何使用此功能来增强默认主题 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` 始终指向顶部
+-------------------------------------------------+
| `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 别名也可能根本不存在——只有当它指向与 @theme-original/CodeBlock 不同的组件时(即,当它由多个主题提供时),Docusaurus 才会创建它。我们不会浪费别名!
客户端模块
客户端模块是您站点包的一部分,就像主题组件一样。但是,它们通常是有副作用的。客户端模块是可以被 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 hook 或访问 React 上下文。如果您的操作是状态驱动的或涉及复杂的 DOM 操作,您应该考虑改为 混淆组件 。