Next.js 中有两种处理页面加载的方式,一种是 Loading UI 一种是 Streaming。接下来我将介绍这两种的区别,以及实际的业务场景。
当我们进入某个页面时,需要获取页面数据,可能是从数据库读取也有可能是 API 服务,总之这是一个异步任务,我们可以在获取数据过程中提示用户数据正在加载,比如放置一些骨架屏,提升用户的体验。
如果不对这些进行处理,使用体验会大打折扣。
假设我们有一个场景,进入 /posts 页面后获取所有的文章数据。
app/posts/page.tsx
:
import Link from "next/link";
const PostsPage = async () => {
const posts = await fetch("https://jsonplaceholder.typicode.com/posts").then(
(response) => response.json()
);
// await new Promise((resolve) => setTimeout(resolve, 2000));
return (
<div className="p-10 max-w-3xl mx-auto">
<ul className="list-decimal">
{posts.map((post) => (
<li key={post.id} className="">
<h2 className="text-xl font-semibold my-2">
<Link href={`/posts/${post.id}`} className="hover:underline">
{post.title}
</Link>
</h2>
<p>{post.body}</p>
</li>
))}
</ul>
</div>
);
};
export default PostsPage;
可以解开上面的注释延长加载时间。
如果我们从首页进入到 /posts 页面,不会立即进入,需要等待 await 之后才会进入页面,这个等待时间就取决于我们的网络了,当网络较差时,可能很久才获取数据,这就大大影响了用户体验。
Loading UI
在 posts 同级目录创建 app/posts/loading.tsx
文件:
(ps:我项目使用的 TypeScript,如果是 JavaScript 则为 jsx 后缀)
const PostsLoading = () => {
return <p>Loading...</p>;
};
export default PostsLoading;
这样当我们进入 /posts 页面后不会等待数据加载完成后才进入页面,而是直接进入 /posts 页面,数据加载过程中显示的内容为 loading.tsx 文件的内容,我们就可以写一些文字提示或者骨架屏之类的内容。
Streaming (Suspense)
另一种方式,也是比较推荐的方式:Streaming (Suspense),流式传输。
比如我们一个页面有多个异步组件,有文章、评论等等,如果我们使用上述第一种方案,页面会等所有的数据加载完成才渲染,而通过 Suspense 可以对每一个组件异步渲染。
React 18 引入了 Suspense
组件,支持在组件树中展示异步组件。Next.js 利用这一特性,让开发者可以定义一个组件在加载期间显示的备选内容。
通过 Suspense
,你可以在组件等待数据时显示一个备用 UI,这样用户就不会看到不完整的空白界面。一旦数据加载完毕,React 将渲染实际的组件。这种异步加载方式特别适用于大型组件或数据,它们可能需要长时间来加载和渲染。
新建 app/posts2/page.tsx
文件:
import Link from "next/link";
import { Suspense } from "react";
const Posts2Page = () => {
return (
<div>
<Suspense fallback={<PostsSkeleton />}>
<PostList />
</Suspense>
</div>
);
};
const PostsSkeleton = () => {
return (
<div className="p-10 max-w-3xl mx-auto">
<ul className="space-y-2 list-decimal">
{[...Array(5).keys()].map((i) => (
<li key={i} className="space-y-2">
<div
className="animate-pulse h-7 w-1/2 bg-slate-200 rounded duration-1000"
style={{
animationDelay: `${i * 0.05}s`,
}}
></div>
<div
className="animate-pulse h-5 w-full bg-slate-200 rounded duration-1000"
style={{
animationDelay: `${i * 0.05}s`,
}}
></div>
<div
className="animate-pulse h-5 w-3/4 bg-slate-200 rounded duration-1000"
style={{
animationDelay: `${i * 0.05}s`,
}}
></div>
</li>
))}
</ul>
</div>
);
};
const PostList = async () => {
const posts = await fetch("https://jsonplaceholder.typicode.com/posts").then(
(response) => response.json()
);
await new Promise((resolve) => setTimeout(resolve, 1000));
return (
<div className="p-10 max-w-3xl mx-auto">
<ul className="list-decimal">
{posts.map((post) => (
<li key={post.id} className="">
<h2 className="text-xl font-semibold my-2">
<Link href={`/posts/${post.id}`} className="hover:underline">
{post.title}
</Link>
</h2>
<p>{post.body}</p>
</li>
))}
</ul>
</div>
);
};
export default Posts2Page;
Posts2Page 使用 Suspense 组件包裹 PostList 组件。当 PostList 组件在加载数据时,Suspense 会显示它的 fallback 属性,也就是 PostsSkeleton 组件。
PostsSkeleton 是一个骨架屏组件,用于在数据加载时显示。它会显示5个文章的占位符。
PostList 是一个异步组件,从 https://jsonplaceholder.typicode.com/posts 这个 URL 获取文章数据,然后显示出来。
最后,如果文章对您有所帮助,还希望能够点赞、收藏、关注,您的每一次点击都会为我带来无穷的动力来写出更好的文章。