通过在服务器端将网页的 HTML 内容渲染成完整的网页(Server-Side Rendering,简称 SSR),然后将生成的网页发送到浏览器端,浏览器端只需要显示网页即可,不需要再进行额外的渲染。
它主要的优势在于:
如果你有以下场景的需求,开发者可以考虑使用 SSR 来渲染你的页面:
在 Modern.js 中,SSR 也是开箱即用的。开发者无需为 SSR 编写复杂的服务端逻辑,也无需关心 SSR 的运维,或是创建单独的服务。
除了开箱即用的 SSR 服务,为了保证开发者的开发体验,Modern.js 还具备:
在 Modenr.js 启用 SSR 非常简单,只需要设置 server.ssr
为 true
即可:
如果你还不了解 Data Loader 如何使用或 Client Loader 的概念,请先阅读数据获取。
Modern.js 中提供了 Data Loader,方便开发者在 SSR、CSR 下同构地获取数据。每个路由模块,如 layout.tsx
和 page.tsx
都可以定义自己的 Data Loader:
在组件中可以通过 Hooks API 的方式获取 loader
函数返回的数据:
该功能需要 x.36.0 以上版本,推荐使用框架最新版本。
默认情况下,在 SSR 应用中,loader
函数只会在服务端执行。但有些场景下,开发者可能期望在浏览器端发送的请求不经过 SSR 服务,直接请求数据源,例如:
Modern.js 支持在 SSR 应用中额外添加 .data.client
文件,同样具名导出 loader
。此时 SSR 应用在服务端执行 Data Loader 报错降级,或浏览器端切换路由时,会像 CSR 应用一样在浏览器端执行该 loader
函数,而不是再向 SSR 服务发送数据请求。
要使用 Client Loader,必须有对应的 Server Loader,且 Server Loader 必须是 .data
文件约定,不能是 .loader
文件约定。
在 Modern.js 中,如果应用在 SSR 过程中出现异常,Modern.js 会自动降级到 CSR 模式,并在 CSR 重新发起数据请求,保证页面能够正常展示。SSR 降级的原因主要分为两种:
默认情况下,如果路由对应的 loader
函数执行报错,框架会在服务端直接渲染 <ErrorBoundary>
组件,并展示错误信息,这也是社区内大多数框架的默认行为。
Modern.js 也支持通过 server.ssr
配置项中的 loaderFailureMode
字段,自定义降级策略。当该字段被配置为 clientRender
时,会直接降级到 CSR 模式,并重新发起数据请求。
此时,如果路由中定义了 Client Loader,则会优先使用 Client Loader 发起数据请求。如果重新渲染仍然出错,再展示 <ErrorBoundary>
组件。
当组件渲染报错时,Modern.js 会自动降级到 CSR 模式,并重新发起数据请求。如果重新渲染仍然出错,则展示 <ErrorBoundary>
组件。
组件渲染报错的行为,不会受到 loaderFailureMode
的影响,也不会在浏览器端执行 Client Loader。
敬请期待
Modern.js 中内置了缓存的能力,详细请参考渲染缓存。
SSR 应用会同时运行在服务端和浏览器端,两者在运行环境上不完全相同,存在 Web API 和 Node API 的差异。
开启 SSR 时,Modern.js 会用相同的入口,构建出 SSR Bundle 和 CSR Bundle 两份产物。因此,在 SSR Bundle 中存在 Web API,或是在 CSR Bundle 中存在 Node API 时,都可能导致运行出错。出现这类问题的场景主要是两类:
这种场景通常出现在应用从 CSR 迁移到 SSR,CSR 应用通常会在代码中引入 Web API。例如应用希望做全局的事件监听:
对于这种场景,你可以直接使用 Modern.js 内置的环境变量 MODERN_TARGET
进行判断,在构建时删除无用代码:
开发环境打包后,SSR 产物和 CSR 产物会被编译成以下内容。因此 SSR 环境中不会再因为 Web API 报错:
更多内容可以查看环境变量。
这类场景是在 SSR 应用中随时可能出现的,因为社区中的包并不都支持在两个运行环境中运行,有些包也无需在两个环境中运行。例如在代码中引入了包 A,它内部有使用了 Web API 的副作用:
如果直接引用到组件中,会造成 CSR 加载报错,即使你已经使用环境变量进行判断,但仍然无法移除依赖中副作用的执行。
Modern.js 也支持通过 .server.
后缀的文件来区分 SSR Bundle 和 CSR Bundle 产物的打包文件。可以创建同名的 .ts
和 .server.ts
文件做一层代理:
在文件中直接引入 ./a
,此时 SSR 打包下会优先使用 .server.ts
后缀的文件,CSR 打包下会使用 .ts
后缀的文件。
SSR 业务需要保证在服务端渲染时的结果和浏览器端 Hydrate 的结果一致,否则很有可能出现不符合预期的渲染结果。这里通过一个例子,演示当 SSR 与 CSR 渲染不一致时出现的问题,在组件中添加以下代码:
启动应用后,访问页面,会发现浏览器控制台抛出警告信息:
这是 React hydrate 结果与 SSR 渲染结果不一致造成的。虽然当前页面表现正常,但在复杂应用中,很有可能因此出现 DOM 层级混乱、样式混乱等问题。
关于 React hydrate 逻辑请参考这里。
应用需要保持 SSR 与 CSR 渲染结果的一致性,如果存在不一致的情况,说明这部分内容无需在 SSR 中进行渲染。Modern.js 为这类在 SSR 中不需要渲染的内容提供 <NoSSR>
工具组件:
在不需要进行 SSR 的元素外部,用 NoSSR
组件包裹:
修改代码后,刷新页发现之前的 Waring 消失。打开浏览器开发者工具的 Network 窗口,查看返回的 HTML 文档是不包含 NoSSR
组件包裹的内容的。
在实际场景中,有些应用的 UI 展示会和用户设备有关,例如 UA 信息。Modern.js 也提供了
useRuntimeContext
这类 API,可以在组件中获取完整的请求信息,利用它保证 SSR 与 CSR 的渲染结果一致。
在 SSR 场景下,开发者需要特别关注内存泄露问题,即使是微小的内存泄露,在大量的访问后也会对服务造成影响。
SSR 时,浏览器的每次请求,都会触发服务端重新执行一次组件渲染逻辑。所以,需要避免在全局定义任何可能不断增长的数据结构,或在全局进行事件订阅,或创建不会被销毁的流。
例如以下代码,使用 redux-observable 时,习惯了 CSR 的开发者通常会在组件中这样编码:
在组件外层创建 Middleware 实例 epicMiddleware
,并在组件内部调用 epicMiddleware.run
。
在浏览器端,这段代码不会造成任何问题,但是在 SSR 时,Middleware 实例会一直无法被销毁。每次渲染组件,调用 epicMiddleware.run(rootEpic)
时,都会在内部添加新的事件绑定,导致整个对象不断变大,最终对应用性能造成影响。
CSR 中这类问题不易被发觉,因此从 CSR 切换到 SSR 时,如果不确定应用是否存在这类隐患,可以对应用进行压测。