UNPKG

rerumaccusamus

Version:

The meta-framework suite designed from scratch for frontend-focused modern web development.

191 lines (132 loc) 7.81 kB
--- 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):::