静态网站生成 (SSG)
In architecture, we mentioned that the theme is run in Webpack. 但是要注意,这并不代表它总是可以访问到浏览器的全局变量! 主题会被构建两次:
- 在服务器端渲染中,主题会在一个叫做 React DOM Server 的沙盒中被编译。 你可以把它理解成一个「无头浏览器」,这里没有
window
或者document
,只有 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'
。 你会在无用代码消除 (dead code elimination) 后得到不同的构建结果:
- 开发模式
- 生产模式
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 就是用 JS 运行的)的情况下工作,也就是纯 HTML/CSS。 这就是服务端渲染提供的东西:把你的 React 代码静态渲染为没有任何动态内容的 HTML。 HTML 文件没有客户端状态的概念(它纯粹是标记语言),所以它不应该依赖浏览器 API。
These HTML files are the first to arrive at the user's browser screen when a URL is visited (see routing). 在此之后,浏览器会抓取并运行其他相应的 JS 代码,从而提供网站的「动态」部分——所有用 JavaScript 实现的内容。 然而,在此之前,页面的主要内容已经可供阅读了,从而加快了加载速度。
在仅客户端渲染的应用程序中,所有 DOM 元素都是由 React 在客户端生成的,而 HTML 文件只包含一个根元素,供 React 挂载 DOM;在 SSR 中,React 已经面对的是一个完全构建好的 HTML 页面,而它只需要将 DOM 元素与它的模型中的虚拟 DOM 关联起来。 这一步被称为「注水」(hydration)。 React 完成对静态 HTML 的注水之后,应用就开始像正常的 React 应用一样工作了。
要注意,Docusaurus 最终仍然是一个单页应用程序,所以静态网站生成只是一种优化(也就是所谓的_渐进增强_),但我们的功能并不完全依赖于这些 HTML 文件。 这与 Jekyll 和 Docusaurus v1 等网站生成器不同。在这些应用里,所有文件都会被静态转换为 HTML,而交互性则通过 <script>
标签所关联的外部的 JavaScript 提供。 如果你检查构建输出,你仍然会在 build/assets/js
下看到所有 JS 资源,而这些实际上 才是 Docusaurus 的核心。
逃生通道
如果你想要在屏幕上渲染任何只有依赖浏览器 API 才能正常工作的动态内容,例如:
在这些情况下,你可能需要避免 SSR,因为如果不知道客户端状态,就无法显示任何有用信息。
客户端的首次渲染必须生成与服务端渲染完全相同的 DOM 结构,否则,React 会把虚拟 DOM 与错误的 DOM 元素相关联。
因此,你不能用 if (typeof window !== 'undefined) {/* 渲染某些东西 */}
这种 naïve 的方法检测浏览器和服务器,因为这样客户端的首次渲染就会立即生成和服务端不同的 DOM。
你可以在 The Perils of Rehydration 这篇文章中详细了解这个坑。
我们提供了几种更可靠的方法来脱离 SSR。
<BrowserOnly>
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>
的 children 不是 JSX 元素,而是一个_返回_元素的函数。 这是设计使然。 考虑如下代码:
import BrowserOnly from '@docusaurus/BrowserOnly';
function MyComponent() {
return (
<BrowserOnly>
{/* 别这么写——不行的 */}
<span>page url = {window.location.href}</span>
</BrowserOnly>
);
}
虽然你可能期望 BrowserOnly
会在服务端渲染过程中把它的 children 藏起来,但实际上它做不到。 当 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("I'm now in the browser");
}, []);
return <span>某些内容……</span>;
}
ExecutionEnvironment
The ExecutionEnvironment
namespace contains several values, and canUseDOM
is an effective way to detect browser environment.
要注意,它本质上做的就是 typeof window !== 'undefined'
,所以你不能用它来做渲染相关的逻辑,而只能用来做命令式的操作,比如响应用户输入并发送网络请求,或者动态导入库,而不更新任何 DOM。
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
if (ExecutionEnvironment.canUseDOM) {
document.title = "我加载好了!";
}