客户端架构
主题别名
主题通过导出一组组件来工作,例如 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
都优先。这种行为称为组件 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` 指向最顶部的非 swizzle 组件
+-------------------------------------------------+
| `plugin-awesome-codeblock/theme/CodeBlock.js` |
+-------------------------------------------------+
| `theme-classic/theme/CodeBlock/index.js` | <-- `@theme-init/CodeBlock` 总是指向底部
+-------------------------------------------------+
这个"堆栈"中的组件按 预设插件 > 预设主题 > 插件 > 主题 > 网站
的顺序推送,因此 website/src/theme
中被 swizzle 的组件总是位于顶部,因为它是最后加载的。
@theme/*
总是指向最顶部的组件——当 CodeBlock
被 swizzle 时,所有其他请求 @theme/CodeBlock
的组件都会收到 swizzle 后的版本。
@theme-original/*
总是指向最顶部的非 swizzle 组件。这就是为什么你可以在 swizzle 的组件中导入 @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}) {
// 如果我们仍在同一页面上,则不执行;生命周期可能会因为
// hash 更改而被触发(例如,在标题之间导航时)
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 hooks 或访问 React 上下文。如果你的操作是状态驱动的或涉及复杂的 DOM 操作,你应该考虑swizzling 组件。