rerumaccusamus
Version:
The meta-framework suite designed from scratch for frontend-focused modern web development.
191 lines (132 loc) • 7.81 kB
Markdown
---
sidebar_position: 4
title: 一体化 SSR/SPR
---
在 Modern.js 中,和 Web Server 一样,SSR 服务与应用也是一体的,开发者无需为 SSR 编写复杂的服务端逻辑。同样,开发者也无需关心 SSR 服务的运维,Modern.js 拥有完备的 SSR 降级策略,保证页面能够安全运行。
Modern.js 启用 SSR 非常简单,只需要设置 [`server.ssr`](/docs/apis/config/server/ssr) 为 `true` 即可:
```json title="package.json"
{
"server": {
"ssr": true
}
}
```
:::note 注
目前,Modern.js 还不支持 Unbundled 开发模式下开启该功能。
:::
## SSR 时的数据请求
Modern.js 中提供了 `useLoader` Hooks API,可以在某些场景下代替 `useEffect` 进行数据请求。这是一个同构的 API,直接使用它即可完成在服务端的数据获取:
```ts
const { data, loading, error } = useLoader(() => {
return loadData()
});
```
Modern.js 打破传统的 SSR 开发模式,提供了用户无感的 SSR 开发体验。并且提供了优雅的降级处理,一旦 SSR 请求失败,会自动降级在浏览器端重新发起请求。
不过,开发者仍然需要关注数据的兜底处理,例如 `null` 值或不符合预期的数据返回。避免在 SSR 时产生 React 渲染错误或是返回凌乱的渲染结果。
:::info 补充信息
关于 `useLoader` 的详细介绍可以参考[这里](/docs/apis/runtime/container/use-loader)。
:::
## 保持渲染一致
代码中区分 SSR 和 CSR 渲染,通常需要根据当前的运行容器环境特征做判断,例如 [UA](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) 信息或是某个 Native 模块。如果处理不够仔细,此时很有可能出现不符合预期的渲染结果。
这里通过一个例子,演示当 SSR 与 CSR 渲染不一致时出现的问题。
在组件中添加以下代码:
```tsx
{
typeof window !== 'undefined' ? (
<div>
browser content
</div>
) : null;
}
```
启动应用后,访问页面,会发现浏览器控制台抛出警告信息:
```sh
Warning: Expected server HTML to contain a matching <div> in <div>.
```
这是因为 React 在客户端执行注水逻辑时,发现渲染结果与 SSR 渲染结果不一致造成的。虽然页面表现正常,但在复杂应用中,很有可能因此出现 DOM 层级混乱、样式混乱等问题。
:::note 注
关于注水逻辑请参考[这里](https://reactjs.org/docs/react-dom.html#hydrate)。
:::
应用需要保持 SSR 与 CSR 渲染结果的一致性,如果存在不一致的情况,说明这部分内容无需在 SSR 中进行渲染。Modern.js 为这类在 SSR 中不需要渲染的内容提供 [`<NoSSR>` 工具组件](/docs/apis/runtime/app/use-runtime-context):
```ts
import { NoSSR } from '@modern-js/runtime/ssr';
```
在不需要进行 SSR 的元素外部,用 `NoSSR` 组件包裹:
```tsx
<NoSSR>
<div>
client content
</div>
</NoSSR>
```
修改代码后,刷新页发现之前的 Waring 消失。打开浏览器开发者工具的 Network 窗口,查看返回的 HTML 文档是不包含 `NoSSR` 组件包裹的内容的。
:::info 补充信息
[`useRuntimeContext`](/docs/apis/runtime/app/use-runtime-context) 可以获取完整的请求信息,可以利用它保证 SSR 与 CSR 的渲染结果一致。
:::
## 关注内存泄漏
:::warning 警告
在 SSR 场景下,开发者需要特别关注内存泄露问题,即使是微小的内存泄露,在大量的访问后也会对服务造成影响。
:::
SSR 时,浏览器的每次请求,都会触发服务端重新执行一次组件渲染逻辑。所以,需要避免在全局定义任何可能不断增长的数据结构,或在全局进行事件订阅,或创建不会被销毁的流。
例如以下代码,使用 [redux-observable](https://redux-observable.js.org/) 时,习惯了 CSR 的开发者通常会在组件中这样编码:
```tsx
/* 代码仅作为示例,不可运行 */
import { createEpicMiddleware, combineEpics } from 'redux-observable';
const epicMiddleware = createEpicMiddleware();
const rootEpic = combineEpics();
export default function Test() {
epicMiddleware.run(rootEpic);
return <div>Hello Modern.js</div>;
}
```
在组件外层创建 Middleware 实例 `epicMiddleware`,并在组件内部调用 `epicMiddleware.run`。
在浏览器端,这段代码不会造成任何问题,但是在 SSR 时,Middleware 实例会一直无法被销毁。每次渲染组件,调用 `epicMiddleware.run(rootEpic)` 时,都会在内部添加新的事件绑定,导致整个对象不断变大,最终对应用性能造成影响。
CSR 中这类问题不易被发觉,因此从 CSR 切换到 SSR 时,如果不确定应用是否存在这类隐患,可以对应用进行压测。
## 收敛服务端数据
为了保持 SSR 阶段请求的数据,可以在浏览器端直接使用, Modern.js 会将渲染过程中收集的数据与状态注入到 HTML 内。但是,CSR 应用常常存在接口数据量大、组件状态未收敛的情况,这时如果直接使用 SSR,渲染得到的 HTML 体积可能会存在过大的问题。此时,SSR 不仅无法为应用带来用户体验上的提升,反而可能起到相反的作用。
因此,使用 SSR 时,**开发者需要为应用做合理的瘦身**:
1. 关注首屏,SSR 中可以只请求首屏需要的数据,并在浏览器端渲染剩余的部分。
2. 将与渲染无关的数据,从接口返回数据中剔除。
## Serverless Pre-render
Modern.js 提供 Serverless Pre-rendering (SPR) 这一特性来提升 SSR 性能。
SPR 利用预渲染与缓存技术,为 SSR 页面提供静态 Web 的响应性能。它让 SSR 应用拥有静态 Web 页面的响应速度与稳定性,同时还能保持数据的动态更新。
在 Modern.js 中使用 SPR 非常简单,只需要在组件中新增 `PreRender` 组件,该组件所在的页面就会自动开启 SPR。
这里模拟一个使用 `useLoader` API 的组件,`useLoader` 中的请求需要消耗 2s 时间。
```jsx
import { useLoader } from '@modern-js/runtime';
export default () => {
const { data } = useLoader(
async () => {
await new Promise((resolve, reject) => {
setTimeout(() => {
resolve(null);
}, 2000);
});
return {
message: 'Hello Modern.js',
};
},
{
params: 'foo',
},
);
return <div>{data?.message}</div>;
};
```
执行 `dev` 命令后,打开页面,可以明显的察觉到页面需要等到 2s 后才返回。
接下来使用 `PreRender` 组件来进行优化,该组件可以直接从 `@modern-js/runtime/ssr` 中导出:
```ts
import { PreRender } from '@modern-js/runtime/ssr';
```
在路由组件内使用 `PreRender` 组件,并设置参数 `interval`,用于表示该次渲染结果的过期时间为 5s:
```tsx
<PreRender interval={5} />
```
修改后,执行 `pnpm run build && pnpm run start` 启动应用,并打开页面。
首次打开时,和之前的渲染并没有什么不同,同样存在 2s 延迟。点击刷新,页面瞬间打开,但此时,页面数据并没有因为刷新发生变化,这是因为缓存还没有过期。
等待 5s,重新刷新页面,页面的数据仍然没有变化。再一次刷新页面数据发生变化,但是页面仍然几乎是瞬间响应的。
这是因为在之前的请求时,SPR 已经在后台异步获取了新的渲染结果,本次请求到的页面是已经缓存在服务器中的版本。
可以想象,当 `interval` 设置为 1 时,用户可以在感知到实时数据的同时,拥有静态页面的响应体验。
:::info 补充信息
`PreRender` 的详细使用可以参考[这里](/docs/apis/runtime/app/pre-render)。
:::