跳至主要内容
版本:3.5.2

静态网站生成 (SSG)

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

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

服务器端渲染静态网站生成 可能代表不同的概念,但我们在本文中可以互换使用。

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

因此,虽然你可能知道不要访问像 process (或者可以访问吗?) 或 'fs' 模块这样的 Node 全局变量,但你也不可以随意访问浏览器全局变量。

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 <>This component is not shown in development</>;
}
const res = someExpensiveOperationThatLastsALongTime();
return <>{res}</>;
}

在 Webpack 构建期间,process.env.NODE_ENV 将被替换为该值,可以是 'development''production'。然后,你在进行死代码消除后会获得不同的构建结果

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

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 关联起来。此步骤称为“水合”。在 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') {/* 渲染某些内容 */} 这样的简单尝试不会作为浏览器与服务器检测方法正常工作,因为第一次客户端渲染会立即渲染与服务器生成的标记不同的标记。

你可以在 The Perils of Rehydration 中详细了解此陷阱。

我们提供了几种更可靠的方法来从 SSR 中逃逸。

<BrowserOnly>

如果你需要只在浏览器中渲染某些组件(例如,因为该组件依赖于浏览器特性才能正常工作),一种常见的方法是使用 <BrowserOnly> 将你的组件包裹起来,以确保它在 SSR 期间不可见,并且只在 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 元素,而是一个返回元素的函数。这是一个设计决策。请考虑以下代码

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} 变量作为此树的一个节点,并尝试渲染它,尽管它实际上没有被使用!使用函数可以确保我们只在需要时让渲染器看到浏览器特定的组件。

useIsBrowser

你也可以使用 useIsBrowser() 钩子来测试组件当前是否处于浏览器环境中。它在 SSR 中返回 false,在第一次客户端渲染后在 CSR 中返回 true。如果你只需要在客户端执行某些条件操作,而不是渲染完全不同的 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 之后。如果只是执行副作用,而没有从客户端状态获取数据,那么这将是最合适的。

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 是检测浏览器环境的有效方法。

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

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

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