Skip to main content
Version: 3.6.1

静态站点生成(SSG)

¥Static site generation (SSG)

architecture 中,我们提到主题是在 Webpack 中运行的。但要注意:这并不意味着它总是可以访问浏览器全局变量!主题构建两次:

¥In architecture, we mentioned that the theme is run in Webpack. But beware: that doesn't mean it always has access to browser globals! The theme is built twice:

  • 在服务器端渲染期间,主题在名为 React DOM 服务器 的沙箱中编译。你可以将其视为 "无头浏览器",其中没有 windowdocument,只有 React。SSR 生成静态 HTML 页面。

    ¥During server-side rendering, the theme is compiled in a sandbox called React DOM Server. You can see this as a "headless browser", where there is no window or document, only React. SSR produces static HTML pages.

  • 在客户端渲染期间,主题被编译为 JavaScript,最终在浏览器中执行,因此它可以访问浏览器变量。

    ¥During client-side rendering, the theme is compiled to JavaScript that gets eventually executed in the browser, so it has access to browser variables.

SSR 还是 SSG?

服务器端渲染和静态站点生成可能是不同的概念,但我们可以互换使用它们。

¥Server-side rendering and static site generation can be different concepts, but we use them interchangeably.

严格来说,Docusaurus 是一个静态站点生成器,因为没有服务器端运行时 - 我们静态渲染部署在 CDN 上的 HTML 文件,而不是根据每个请求动态预渲染。这与 Next.js 的工作模式不同。

¥Strictly speaking, Docusaurus is a static site generator, because there's no server-side runtime—we statically render to HTML files that are deployed on a CDN, instead of dynamically pre-rendering on each request. This differs from the working model of Next.js.

因此,虽然你可能知道不要访问 Node 全局变量,例如 process (或者我们可以吗?) 或 'fs' 模块,但你也无法自由访问浏览器全局变量。

¥Therefore, while you probably know not to access Node globals like process (or can we?) or the 'fs' module, you can't freely access browser globals either.

import React from 'react';

export default function WhereAmI() {
return <span>{window.location.href}</span>;
}

这看起来像惯用的 React,但如果你运行 docusaurus build,你会得到一个错误:

¥This looks like idiomatic React, but if you run docusaurus build, you will get an error:

ReferenceError: window is not defined

这是因为在服务器端渲染期间,Docusaurus 应用实际上并未在浏览器中运行,并且它不知道 window 是什么。

¥This is because during server-side rendering, the Docusaurus app isn't actually run in browser, and it doesn't know what window is.

What about process.env.NODE_ENV?

"没有节点全局变量" 规则的一个例外是 process.env.NODE_ENV。事实上,你可以在 React 中使用它,因为 Webpack 将此变量作为全局变量注入:

¥One exception to the "no Node globals" rule is process.env.NODE_ENV. In fact, you can use it in React, because Webpack injects this variable as a global:

import React from 'react';

export default function expensiveComp() {
if (process.env.NODE_ENV === 'development') {
return <>This component is not shown in development</>;
}
const res = someExpensiveOperationThatLastsALongTime();
return <>{res}</>;
}

在 Webpack 构建期间,process.env.NODE_ENV 将替换为值 'development''production'。消除死代码后,你将得到不同的构建结果:

¥During Webpack build, the process.env.NODE_ENV will be replaced with the value, either 'development' or 'production'. You will then get different build results after dead code elimination:

import React from 'react';

export default function expensiveComp() {
if ('development' === 'development') {
+ return <>This component is not shown in development</>;
}
- const res = someExpensiveOperationThatLastsALongTime();
- return <>{res}</>;
}

了解 SSR

¥Understanding SSR

React 不仅仅是一个动态 UI 运行时,它还是一个模板引擎。由于 Docusaurus 网站主要包含静态内容,因此它应该能够在没有任何 JavaScript(React 运行于其中)的情况下工作,而只需要纯 HTML/CSS。这就是服务器端渲染所提供的:将 React 代码静态渲染为 HTML,没有任何动态内容。HTML 文件没有客户端状态的概念(它纯粹是标记),因此它不应该依赖于浏览器 API。

¥React is not just a dynamic UI runtime—it's also a templating engine. Because Docusaurus sites mostly contain static contents, it should be able to work without any JavaScript (which React runs in), but only plain HTML/CSS. And that's what server-side rendering offers: statically rendering your React code into HTML, without any dynamic content. An HTML file has no concept of client state (it's purely markup), hence it shouldn't rely on browser APIs.

当访问 URL 时,这些 HTML 文件首先到达用户的浏览器屏幕(参见 routing)。然后,浏览器获取并运行其他 JS 代码以提供站点的 "dynamic" 部分 - 任何使用 JavaScript 实现的内容。但是,在此之前,页面的主要内容已经可见,可以加快加载速度。

¥These HTML files are the first to arrive at the user's browser screen when a URL is visited (see routing). Afterwards, the browser fetches and runs other JS code to provide the "dynamic" parts of your site—anything implemented with JavaScript. However, before that, the main content of your page is already visible, allowing faster loading.

在仅限 CSR 的应用中,所有 DOM 元素都是在客户端使用 React 生成的,并且 HTML 文件只包含一个根元素供 React 挂载 DOM;在 SSR 中,React 已经面对一个完全构建的 HTML 页面,它只需要将 DOM 元素与其模型中的虚拟 DOM 关联起来。该步骤称为 "hydration"。React 水合静态标记后,应用开始像任何正常的 React 应用一样工作。

¥In CSR-only apps, all DOM elements are generated on client side with React, and the HTML file only ever contains one root element for React to mount DOM to; in SSR, React is already facing a fully built HTML page, and it only needs to correlate the DOM elements with the virtual DOM in its model. This step is called "hydration". After React has hydrated the static markup, the app starts to work as any normal React app.

请注意,Docusaurus 最终是一个单页应用,因此静态站点生成只是一种优化(所谓的渐进增强),但我们的功能并不完全依赖于这些 HTML 文件。这与 JekyllDocusaurus v1 等网站生成器相反,其中所有文件都静态转换为标记,并且通过与 <script> 标签链接的外部 JavaScript 添加交互性。如果你检查构建输出,你仍然会在 build/assets/js 下看到 JS 资源,这确实是 Docusaurus 的核心。

¥Note that Docusaurus is ultimately a single-page application, so static site generation is only an optimization (progressive enhancement, as it's called), but our functionality does not fully depend on those HTML files. This is contrary to site generators like Jekyll and Docusaurus v1, where all files are statically transformed to markup, and interactiveness is added through external JavaScript linked with <script> tags. If you inspect the build output, you will still see JS assets under build/assets/js, which are, really, the core of Docusaurus.

应急方案

¥Escape hatches

如果你想在屏幕上渲染任何依赖浏览器 API 运行的动态内容,例如:

¥If you want to render any dynamic content on your screen that relies on the browser API to be functional at all, for example:

  • 我们的 实时代码块,运行在浏览器的 JS 运行时

    ¥Our live codeblock, which runs in the browser's JS runtime

  • 我们的 主题图片 可以检测用户的配色方案以显示不同的图片

    ¥Our themed image that detects the user's color scheme to display different images

  • 我们的调试面板的 JSON 查看器使用 window 全局进行样式设置

    ¥The JSON viewer of our debug panel which uses the window global for styling

你可能需要转义 SSR,因为静态 HTML 在不知道客户端状态的情况下无法显示任何有用的内容。

¥You may need to escape from SSR since static HTML can't display anything useful without knowing the client state.

警告

对于第一个客户端渲染来说,生成与服务器端渲染完全相同的 DOM 结构非常重要,否则,React 会将虚拟 DOM 与错误的 DOM 元素关联起来。

¥It is important for the first client-side render to produce the exact same DOM structure as server-side rendering, otherwise, React will correlate virtual DOM with the wrong DOM elements.

因此,if (typeof window !== 'undefined) {/* render something */} 的幼稚尝试无法正常用作浏览器与服务器检测,因为第一个客户端渲染将立即渲染与服务器生成的标记不同的标记。

¥Therefore, the naïve attempt of if (typeof window !== 'undefined) {/* render something */} won't work appropriately as a browser vs. server detection, because the first client render would instantly render different markup from the server-generated one.

你可以在 补液的危险 中阅读有关此陷阱的更多信息。

¥You can read more about this pitfall in The Perils of Rehydration.

我们提供了几种更可靠的方法来规避 SSR。

¥We provide several more reliable ways to escape SSR.

<BrowserOnly>

如果你需要仅在浏览器中渲染某些组件(例如,因为该组件完全依赖于浏览器的具体功能),一种常见的方法是使用 <BrowserOnly> 封装你的组件,以确保它在 SSR 期间不可见,并且仅在 CSR 中渲染 。

¥If you need to render some component in browser only (for example, because the component relies on browser specifics to be functional at all), one common approach is to wrap your component with <BrowserOnly> to make sure it's invisible during SSR and only rendered in CSR.

import BrowserOnly from '@docusaurus/BrowserOnly';

function MyComponent(props) {
return (
<BrowserOnly fallback={<div>Loading...</div>}>
{() => {
const LibComponent =
require('some-lib-that-accesses-window').LibComponent;
return <LibComponent {...props} />;
}}
</BrowserOnly>
);
}

重要的是要认识到 <BrowserOnly> 的子元素不是 JSX 元素,而是返回元素的函数。这是一个设计决定。考虑这段代码:

¥It's important to realize that the children of <BrowserOnly> is not a JSX element, but a function that returns an element. This is a design decision. Consider this code:

import BrowserOnly from '@docusaurus/BrowserOnly';

function MyComponent() {
return (
<BrowserOnly>
{/* DON'T DO THIS - doesn't actually work */}
<span>page url = {window.location.href}</span>
</BrowserOnly>
);
}

虽然你可能期望 BrowserOnly 在服务器端渲染期间隐藏子级,但实际上不能。当 React 渲染器尝试渲染此 JSX 树时,它确实将 {window.location.href} 变量视为此树的节点并尝试渲染它,尽管实际上并未使用它!使用函数可以确保我们只在需要时让渲染器看到仅浏览器组件。

¥While you may expect that BrowserOnly hides away the children during server-side rendering, it actually can't. When the React renderer tries to render this JSX tree, it does see the {window.location.href} variable as a node of this tree and tries to render it, although it's actually not used! Using a function ensures that we only let the renderer see the browser-only component when it's needed.

useIsBrowser

你还可以使用 useIsBrowser() 钩子来测试组件当前是否位于浏览器环境中。在第一次客户端渲染后,它在 SSR 中返回 false,而 CSR 中返回 true。如果你只需要在客户端执行某些条件操作,而不渲染完全不同的 UI,请使用此钩子。

¥You can also use the useIsBrowser() hook to test if the component is currently in a browser environment. It returns false in SSR and true is CSR, after first client render. Use this hook if you only need to perform certain conditional operations on client-side, but not render an entirely different UI.

import useIsBrowser from '@docusaurus/useIsBrowser';

function MyComponent() {
const isBrowser = useIsBrowser();
const location = isBrowser ? window.location.href : 'fetching location...';
return <span>{location}</span>;
}

useEffect

最后,你可以将逻辑放在 useEffect() 中以延迟其执行,直到第一个 CSR 之后。如果你仅执行副作用但不从客户端状态获取数据,则这是最合适的。

¥Lastly, you can put your logic in useEffect() to delay its execution until after first CSR. This is most appropriate if you are only performing side-effects but don't get data from the client state.

function MyComponent() {
useEffect(() => {
// Only logged in the browser console; nothing is logged during server-side rendering
console.log("I'm now in the browser");
}, []);
return <span>Some content...</span>;
}

ExecutionEnvironment

ExecutionEnvironment 命名空间包含多个值,canUseDOM 是检测浏览器环境的有效方法。

¥The ExecutionEnvironment namespace contains several values, and canUseDOM is an effective way to detect browser environment.

请注意,它本质上是在后台检查 typeof window !== 'undefined',因此你不应该将其用于与渲染相关的逻辑,而只能将其用于命令式代码,例如通过发送 Web 请求对用户输入做出反应,或者动态导入库,其中 DOM 根本不会更新 。

¥Beware that it essentially checked typeof window !== 'undefined' under the hood, so you should not use it for rendering-related logic, but only imperative code, like reacting to user input by sending web requests, or dynamically importing libraries, where DOM isn't updated at all.

a-client-module.js
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';

if (ExecutionEnvironment.canUseDOM) {
document.title = "I'm loaded!";
}