跳到主要内容

静态站点生成(SSG)

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

  • 服务器端渲染期间,主题在称为 React DOM Server 的沙盒中编译。您可以将其视为一个"无头浏览器",那里没有 windowdocument,只有 React。SSR 生成静态 HTML 页面。
  • 客户端渲染期间,主题被编译为最终在浏览器中执行的 JavaScript,因此可以访问浏览器变量。
SSR 还是 SSG?

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

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

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

import React from 'react';

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

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

ReferenceError: window is not defined

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

process.env.NODE_ENV 呢?

对于"无 Node 全局变量"规则,有一个例外是 process.env.NODE_ENV。事实上,您可以在 React 中使用它,因为 Webpack 将此变量注入为全局变量:

import React from 'react';

export default function expensiveComp() {
if (process.env.NODE_ENV === 'development') {
return <>此组件在开发环境中不显示</>;
}
const res = someExpensiveOperationThatLastsALongTime();
return <>{res}</>;
}

在 Webpack 构建期间,process.env.NODE_ENV 将被替换为值,要么是 'development',要么是 'production'。之后,您将在死代码消除后获得不同的构建结果:

import React from 'react';

export default function expensiveComp() {
if ('development' === 'development') {
+ return <>此组件在开发环境中不显示</>;
}
- const res = someExpensiveOperationThatLastsALongTime();
- return <>{res}</>;
}

理解 SSR

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

当访问一个 URL 时,这些 HTML 文件是首先到达用户浏览器屏幕的(参见路由)。之后,浏览器获取并运行其他 JS 代码以提供站点的"动态"部分——任何用 JavaScript 实现的内容。但在此之前,您页面的主要内容已经可见,从而实现更快的加载。

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

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

逃生舱口

如果您想渲染任何依赖浏览器 API 才能正常工作的动态内容,例如:

  • 我们的实时代码块,它在浏览器的 JS 运行时中运行
  • 我们的主题图像,它检测用户的配色方案以显示不同的图像
  • 调试面板的 JSON 查看器,它使用 window 全局变量进行样式设置

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

注意

对于第一次客户端渲染来说,生成的 DOM 结构必须与服务器端渲染完全相同,否则 React 将虚拟 DOM 与错误的 DOM 元素关联。

因此,天真地尝试 if (typeof window !== 'undefined') {/* 渲染某些内容 */} 不会适当地作为浏览器与服务器检测,因为第一个客户端渲染会立即渲染与服务器生成的不同的标记。

您可以在重新水合的危险中阅读更多关于这个陷阱的内容。

我们提供了几种更可靠的方式来逃离 SSR。

<BrowserOnly>

如果您需要仅在浏览器中渲染某个组件(例如,因为该组件依赖于浏览器特定功能才能正常工作),一种常见的方法是用 <BrowserOnly> 包装您的组件,以确保它在 SSR 期间不可见,仅在 CSR 中渲染。

import BrowserOnly from '@docusaurus/BrowserOnly';

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

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

import BrowserOnly from '@docusaurus/BrowserOnly';

function MyComponent() {
return (
<BrowserOnly>
{/* 不要这样做 - 实际上不起作用 */}
<span>页面 url = {window.location.href}</span>
</BrowserOnly>
);
}

虽然您可能期望 BrowserOnly 在服务器端渲染期间隐藏子元素,但它实际上无法做到。当 React 渲染器尝试渲染这个 JSX 树时,它确实看到了 {window.location.href} 变量作为树的一个节点并尝试渲染它,尽管它实际上并未使用!使用函数可以确保仅在需要时让渲染器看到仅浏览器的组件。

useIsBrowser

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

import useIsBrowser from '@docusaurus/useIsBrowser';

function MyComponent() {
const isBrowser = useIsBrowser();
const location = isBrowser ? window.location.href : '正在获取位置...';
return <span>{location}</span>;
}

useEffect

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

function MyComponent() {
useEffect(() => {
// 仅在浏览器控制台中记录;在服务器端渲染期间不记录任何内容
console.log("我现在在浏览器中");
}, []);
return <span>Some content...</span>;
}

ExecutionEnvironment

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

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!";
}