静态站点生成 (SSG)
在 架构 中,我们提到主题在 Webpack 中运行。但请注意:这并不意味着它总是可以访问浏览器全局变量!主题会构建两次:
- 在 服务器端渲染 期间,主题在一个名为 React DOM Server 的沙盒中编译。您可以将其视为一个“无头浏览器”,其中没有
window
或document
,只有React。SSR 生成静态 HTML 页面。 - 在 客户端渲染 期间,主题被编译成最终在浏览器中执行的 JavaScript,因此它可以访问浏览器变量。
_服务器端渲染_和_静态站点生成_可能是不同的概念,但我们互换使用它们。
严格来说,Docusaurus 是一个静态站点生成器,因为它没有服务器端运行时——我们静态地渲染到部署在 CDN 上的 HTML 文件,而不是在每次请求时动态预渲染。这与 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'
。然后,在死代码消除后,您将获得不同的构建结果:
- 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}</>;
}
import React from 'react';
export default function expensiveComp() {
- if ('production' === '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 文件。这与像 Jekyll 和 Docusaurus v1 这样的站点生成器相反,其中所有文件都静态转换为标记,并且交互性通过使用<script>
标签链接的外部 JavaScript 添加。如果您检查构建输出,您仍然会在build/assets/js
下看到 JS 资产,它们实际上是 Docusaurus 的核心。
逃生舱
如果您想在屏幕上渲染任何依赖于浏览器 API 才能正常工作的动态内容,例如:
您可能需要从 SSR 中逃逸,因为静态 HTML 在不知道客户端状态的情况下无法显示任何有用的内容。
第一次客户端渲染生成与服务器端渲染完全相同的 DOM 结构非常重要,否则,React 将虚拟 DOM 与错误的 DOM 元素关联。
因此,if (typeof window !== 'undefined) {/* render something */}
的天真尝试并不能作为浏览器与服务器检测正常工作,因为第一次客户端渲染将立即渲染与服务器生成的不同的标记。
您可以在 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'
,因此您不应将其用于与渲染相关的逻辑,而只应将其用于命令式代码,例如通过发送 Web 请求来响应用户输入,或动态导入库,其中 DOM 完全没有更新。
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
if (ExecutionEnvironment.canUseDOM) {
document.title = "I'm loaded!";
}