静态网站生成 (SSG)
在架构中,我们提到主题是在 Webpack 中运行的。但请注意:这并不意味着它总能访问浏览器全局变量!主题会被构建两次:
- 在服务端渲染期间,主题在一个名为 React DOM Server 的沙箱中被编译。你可以将其看作一个"无头浏览器",其中没有
window
或document
,只有 React。SSR 会生成静态 HTML 页面。 - 在客户端渲染期间,主题被编译成最终在浏览器中执行的 JavaScript,因此它可以访问浏览器变量。
服务端渲染 和 静态网站生成 可能是不同的概念,但我们在此交替使用它们。
严格来说,道格龙(Docusaurus)是一个静态网站生成器,因为没有服务端运行时——我们静态渲染成部署在 CDN 上的 HTML 文件,而不是在每次请求时动态预渲染。这与 Next.js 的工作模式不同。
因此,尽管你可能知道不要访问像 process
这样的 Node 全局变量(或者可以吗?)或 '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'
。经过死码消除后,你将得到不同的构建结果:
- Development
- Production
import React from 'react';
export default function expensiveComp() {
if ('development' === 'development') {
+ return <>此组件在开发模式下不显示</>;
}
- const res = someExpensiveOperationThatLastsALongTime();
- return <>{res}</>;
}
import React from 'react';
export default function expensiveComp() {
- if ('production' === '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 完成了对静态 HTML 的‘水合’操作(也就是为它注入了所有的交互逻辑和状态),这个原本静态的页面就**‘活’了过来**,变成了一个功能齐全、响应用户操作的普通 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>加载中...</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
,在首次客户端渲染后在 CSR 中返回 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>一些内容...</span>;
}
ExecutionEnvironment
ExecutionEnvironment
命名空间包含多个值,canUseDOM
是检测浏览器环境的有效方法。
请注意,它本质上是在底层检查 typeof window !== 'undefined'
,所以你不应该将它用于与渲染相关的逻辑,而只能用于命令式代码,比如通过发送 Web 请求来响应用户输入,或者动态导入库,这时 DOM 完全不更新。
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
if (ExecutionEnvironment.canUseDOM) {
document.title = "我被加载了!";
}