Next.js加载异步组件 & 骨架屏

2023-12-16 08:53:38

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 可以对每一个组件异步渲染。

image.png

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 获取文章数据,然后显示出来。

2023-12-13_15-38-10.gif

最后,如果文章对您有所帮助,还希望能够点赞、收藏、关注,您的每一次点击都会为我带来无穷的动力来写出更好的文章。

文章来源:https://blog.csdn.net/qq_44600038/article/details/134974120
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。