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