跳到主要内容
版本:3.6.3

客户端架构

Theme aliases

A theme works by exporting a set of components, e.g. Navbar, Layout, Footer, to render the data passed down from plugins. Docusaurus and users use these components by importing them using the @theme webpack alias:

import Navbar from '@theme/Navbar';

The alias @theme can refer to a few directories, in the following priority:

  1. A user's website/src/theme directory, which is a special directory that has the higher precedence.
  2. A Docusaurus theme package's theme directory.
  3. Docusaurus core 提供的原始组件(通常用不到)。

This is called a layered architecture: a higher-priority layer providing the component would shadow a lower-priority layer, making swizzling possible. 假设有以下文件结构:

website
├── node_modules
│ └── @docusaurus/theme-classic
│ └── theme
│ └── Navbar.js
└── src
└── theme
└── Navbar.js

website/src/theme/Navbar.js takes precedence whenever @theme/Navbar is imported. 这被称为 swizzle。 If you are familiar with Objective C where a function's implementation can be swapped during runtime, it's the exact same concept here with changing the target @theme/Navbar is pointing to!

We already talked about how the "userland theme" in src/theme can re-use a theme component through the @theme-original alias. One theme package can also wrap a component from another theme, by importing the component from the initial theme, using the @theme-init import.

Here's an example of using this feature to enhance the default theme CodeBlock component with a react-live playground feature.

import InitialCodeBlock from '@theme-init/CodeBlock';
import React from 'react';

export default function CodeBlock(props) {
return props.live ? (
<ReactLivePlayground {...props} />
) : (
<InitialCodeBlock {...props} />
);
}

Check the code of @docusaurus/theme-live-codeblock for details.

警告

Unless you want to publish a re-usable "theme enhancer" (like @docusaurus/theme-live-codeblock), you likely don't need @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
+-------------------------------------------------+

The components in this "stack" are pushed in the order of preset plugins > preset themes > plugins > themes > site, so the swizzled component in website/src/theme always comes out on top because it's loaded last.

@theme/* always points to the topmost component—when CodeBlock is swizzled, all other components requesting @theme/CodeBlock receive the swizzled version.

@theme-original/* always points to the topmost non-swizzled component. That's why you can import @theme-original/CodeBlock in the swizzled component—it points to the next one in the "component stack", a theme-provided one. 插件作者不能使用这个别名,因为你的组件可能是最顶端的组件,从而导致自己导入自己的情况。

@theme-init/* always points to the bottommost component—usually, this comes from the theme or plugin that first provides this component. Individual plugins / themes trying to enhance code block can safely use @theme-init/CodeBlock to get its basic version. Site creators should generally not use this because you likely want to enhance the topmost instead of the bottommost component. It's also possible that the @theme-init/CodeBlock alias does not exist at all—Docusaurus only creates it when it points to a different one from @theme-original/CodeBlock, i.e. when it's provided by more than one theme. 我们不会浪费别名的!

Client modules

客户端模块是网站包的一部分,就像主题组件一样。 然而,它们通常会引入副作用。 Client modules are anything that can be imported by Webpack—CSS, JS, etc. JS 脚本通常在全局环境中工作,比如注册事件监听器,创建全局变量……

这些模块是在 React 甚至还没开始渲染 UI 之前就导入的。

@docusaurus/core/App.tsx
// 它在底层是如何工作的
import '@generated/client-modules';

Plugins and sites can both declare client modules, through getClientModules and siteConfig.clientModules, respectively.

Client modules are called during server-side rendering as well, so remember to check the execution environment before accessing client-side globals.

mySiteGlobalJs.js
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 stylesheets imported as client modules are global.

mySiteGlobalCss.css
/* This stylesheet is global. */
.globalSelector {
color: red;
}

Client module lifecycles

Besides introducing side-effects, client modules can optionally export two lifecycle functions: onRouteUpdate and onRouteDidUpdate.

Because Docusaurus builds a single-page application, script tags will only be executed the first time the page loads, but will not re-execute on page transitions. 如果你有一些命令式的 JS 逻辑需要在每个新页面加载时执行,那么这些生命周期会很有用,比如操纵 DOM 元素,发送分析数据,等等。

每次路由变换,会有几个重要的时间节点:

  1. 用户点击链接,导致路由更改当前位置。
  2. Docusaurus 预加载下一路径的资源,同时保持显示当前页面的内容。
  3. 下一路径的资源加载完毕。
  4. 新路径的路由组件在 DOM 上渲染出来。

onRouteUpdate will be called at event (2), and onRouteDidUpdate will be called at (4). They both receive the current location and the previous location (which can be null, if this is the first screen).

onRouteUpdate can optionally return a "cleanup" callback, which will be called at (3). For example, if you want to display a progress bar, you can start a timeout in onRouteUpdate, and clear the timeout in the callback. (The classic theme already provides an nprogress integration this way.)

请注意,新页面的 DOM 仅在事件 (4) 中可用。 If you need to manipulate the new page's DOM, you'll likely want to use onRouteDidUpdate, which will be fired as soon as the DOM on the new page has mounted.

myClientModule.js
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,并且你想要利用上下文类型 (contextual type):

myClientModule.ts
import type {ClientModule} from '@docusaurus/types';

const module: ClientModule = {
onRouteUpdate({location, previousLocation}) {
// ...
},
onRouteDidUpdate({location, previousLocation}) {
// ...
},
};
export default module;

这两个生命周期都会在第一次渲染时被调用,但它们不会在服务端被调用,因此你可以安全地在函数中访问浏览器专有的全局变量。

Prefer using React

客户端模块的生命周期是纯命令式的,你不能在内部用 React 钩子函数或读取 React context。 If your operations are state-driven or involve complicated DOM manipulations, you should consider swizzling components instead.