理想状态
组件侧利用 react 这个 rfc 中提到的 await
能力, 直接发起数据请求
export default async function Comments() {
const comments = await getData();
return (
<div>
{comments.map((comment, i) => (
<p className="comment" key={i}>
{comment}
</p>
))}
</div>
);
}
组件外部,使用 Suspense
包裹, SSR 渲染时,这类组件等待数据请求返回后,以流式的方式返回渲染结果
<Suspense fallback={<Loading />}>
<Comments />
</Suspense>
这是 React 和 Next.js 在 Server Component 中流式渲染内容,所使用的方式
面临的问题
1. await 能力尚不可用
react 组件内的 await
能力尚未正式发布,正式版本还无法直接使用
2. 需要 throw promise
由于问题 1 未解决,所以组件需要主动在 async 函数未执行完毕的时候,throw promise 供 Suspense 组件消费,这也是 react 的 lazy 和官方 suspense demo 中的做法。需要框架提供统一解决方案
3. 数据透传问题
不同于 Server Component,只需要下发 vdom,传统 SSR 应用下发 HTML 的同时,还需要下发数据,所以遇到了数据如何透传的 问题 。
因此组件不能在内部自己直接发起数据请求,自己消费,这样框架就无法获取数据,帮助透传。
4. SSR 降级
在集团内环境,我们需要区分 Server 请求和 Client 端请求两套属性,供 SSR 降级时用,在组件内直接发起数据请求,无法做代码移除
方案
结合上述问题,一种解决思路是对组件的编码规范做出约定
- 通过导出
suspense = true
声明需要被流式返回 - 通用导出
Loading
组件,来声明 loading 状态时的 UI - 通用导出
Fallback
组件,来声明兜底状态的 UI - 通过导出
serverDataLoader
声明 server 端的数据请求 - 通过导出
dataLoader
声明 Client 端的数据请求 - 通过
useData
来消费数据
import { useData } from 'ice';
export default function Footer() {
// 消费数据
const data = useData();
return (
<div>
<h2>{data.title}</h2>
</div>
);
}
// 声明 loading 时的 UI
export function Loading() {
return (
<div>loading...</div>
);
}
export const serverDataLoader = () => {
// SSR 时的数据请求
};
export const dataLoader = () => {
// CSR 时的数据请求
};
// 标记需要被 suspense
export const suspense = true;
对于这类声明了 suspense
的组件,框架会在其外层主动包裹 IceSuspense 组件,其职责包括:
- 帮助组件发起数据请求
- 数据请求未 ready 时,throw promise 给 React 原始 Suspense 组件消费
- 透传数据的请求结果,用于 hydrate
demo 见 #5584
基于这个方案的思路,还需要讨论的问题是:
要不要限定路由组件才能使用 Suspense 能力
如果限定路由组件
- 可以延续 dataLoader 的声明方式
- 可以自动包裹 Suspense 组件
如果所有组件都可以使用 Suspense 能力
- 灵活度更高
- 无法自动处理,需要业务自己调用 IceSuspense 组件进行传参
- 需要给组件标记唯一 ID,用于透传数据时使用
- 需要对非路由组件也开启代码移除能力
- 如果想要代码移除,必须导出 serverDataLoader,即使不被其他组件消费
- 路由组件不能直接 Suspense,如果路由组件声明了 serverDataLoader,供 Suspense 消费,会和框架默认的数据请求逻辑冲突
考虑到路由组件存在数据请求串行的问题 https://github.com/alibaba/ice/issues/5697, 普通组件也需要支持流式渲染的能力
使用方式如下:
import { Suspense } from 'ice';
import * as Comments from '@/components/Comments';
import * as Footer from '@/components/Footer';
export default function Home() {
return (
<div>
<Suspense module={Comments} id="comments" />
<Suspense module={Footer} id="footer" />
</div>
);
}
8条答案
按热度按时间vc9ivgsu1#
组件内的
await
针对的应该是 server component,对于 client 端,官方推荐用use
biswetbf2#
可以直接是
useSuspenseData -> useData
跟现在保持一致,内部通过上层 Context 信息来决定是 suspense 还是正常后去数据svgewumm3#
Polyfill for
First class support of async/await
to react components.Usage
Note that
withPromise
can be automaticlly handled by compiling or packer.Impl for withPromise
nkcskrwz4#
对于这类声明了 suspense 的组件,框架会在其外层主动包裹 IceSuspense 组件
这里如果是让开发者自行包装 IOC 呢,会更直接一些
希望是做更少声明参数 - 影响一些黑盒逻辑的事情, 而把做了什么事情暴露给用户
p1iqtdky5#
这种方式的限制是:
vom3gejh6#
线下讨论结论是
zhte4eai7#
线下讨论结论是
确定性的方案 可以直接更新到上述的方案中
jslywgbw8#
实现过程中暴露了一些问题,比如:
因此,对之前的方案做出了一些调整:
具体用法如下:
前置条件
在
document.tsx
中调整 JS 加载顺序Scripts 需配置为 async 或 defer,以确保 JS 不会阻塞流式内容的上屏
defer
等待所有流式内容返回完成后,再执行 JSasync
JS 加载过程中,不阻塞内容上屏,JS 加载完成后,优先执行 JS,再渲染后续返回的内容在
ice.config.mts
中配置 dataLoader 为 false组件实现
需要被流式渲染的组件,按以下规则导出内容:
使用 ice 导出的 Suspense 组件包裹需要被流式渲染的内容