agentmentoragentmentor

第 07 节:服务器上取数据:async、fetch、loading

本节 objectives:

  • 能读懂 async functionawait fetch(...) 的基本形状。
  • 能在 Server Component 页面中获取并渲染远程数据。
  • 能添加一个 loading.tsx 作为加载占位。

先修:会写路由、列表、Server/Client Component 基本判断 | 上一节 << 06 | 下一节 08 >>

数据可以先在服务器上准备好

浏览器当然可以取数据,但 Next.js 的 App Router 允许你在 Server Component 里直接写异步取数代码。官方数据获取文档展示了在组件中使用 fetch 获取数据的模式1。这会让页面代码更接近「拿数据,渲染页面」的顺序。

你只需要先掌握三个词:async 表示函数里可以等待异步结果;await 表示等这个 Promise 完成;fetch 是 Web 平台用于发网络请求的 API23

讲解

async 让函数可以等待。
MDN 把 async function 描述为会返回 Promise 的异步函数2。你现在不必完全理解 Promise,先记住:看到 await,外层函数通常要写 async

fetch 拿到的是响应,不是直接的数据。
常见写法是:

tsx
const response = await fetch("https://api.vercel.app/blog");const posts = await response.json();

第一行拿响应,第二行把响应体解析成 JavaScript 数据。MDN 的 fetch 指南也把请求、响应和解析分开讲3

Server Component 可以直接 await
在 App Router 中,页面组件可以写成 export default async function Page() { ... },然后等待数据1。这和 Client Component 里用 useEffect 不是同一条路。

loading.tsx 是同一路由的加载状态。
当某个路由段在准备内容时,loading.tsx 可以显示占位界面。它和 page.tsx 放在同一个路由文件夹里,服务同一段页面体验4

跟我做一遍(worked example)

目标:创建 /posts 页面,从一个示例接口读取博客标题。

  1. 新建 app/posts/page.tsx:
tsx
type Post = {  id: number;  title: string;};
async function getPosts(): Promise<Post[]> {  const response = await fetch("https://api.vercel.app/blog");
  if (!response.ok) {    throw new Error("读取文章失败");  }
  return response.json();}
export default async function PostsPage() {  const posts = await getPosts();
  return (    <main>      <h1>文章列表</h1>      <ul>        {posts.slice(0, 5).map((post) => (          <li key={post.id}>{post.title}</li>        ))}      </ul>    </main>  );}
  1. 新建 app/posts/loading.tsx:
tsx
export default function Loading() {  return <p>正在读取文章...</p>;}

为什么这样写:

  • getPosts 把取数逻辑集中起来。
  • response.ok 先做最小错误判断。
  • PostsPage 是 async Server Component。
  • 取回来的数组继续用上一节学过的 map 渲染。

换你补全(faded example)

下面要读取同一个接口,只显示前三篇文章。补全 asyncawaitmap 的关键位置。

tsx
async function getPosts() {  const response = ______ fetch("https://api.vercel.app/blog");  return response.json();}
export default ______ function PostsPage() {  const posts = ______ getPosts();
  return (    <main>      <h1>前三篇文章</h1>      <ol>        {posts.slice(0, 3).______((post) => (          <li key={post.id}>{post.title}</li>        ))}      </ol>    </main>  );}

参考答案:

tsx
async function getPosts() {  const response = await fetch("https://api.vercel.app/blog");  return response.json();}
export default async function PostsPage() {  const posts = await getPosts();
  return (    <main>      <h1>前三篇文章</h1>      <ol>        {posts.slice(0, 3).map((post) => (          <li key={post.id}>{post.title}</li>        ))}      </ol>    </main>  );}

关键判断点:等待网络请求和等待 getPosts 都要 await;数组转 JSX 仍然用 map

小结 + 通向下一节

你已经能在服务器上取数据并显示列表。最后一节把路由、组件、交互和取数收束成一个小站,再用构建命令检查它是否能进入下一阶段。

Footnotes

  1. Next.js Fetching Data — https://nextjs.org/docs/app/getting-started/fetching-data 2

  2. MDN: async function — https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function 2

  3. MDN: Using the Fetch API — https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch 2

  4. Next.js Layouts and Pages — https://nextjs.org/docs/app/getting-started/layouts-and-pages

练习

Level 1:标出数据流。
做法:把 worked example 的代码复制到笔记,用数字标出顺序:请求接口、检查响应、解析 JSON、渲染列表。

提示 1

先找所有 await

提示 2

再看数据变量从 response 变成 posts。 Level 2:加一个空状态。
做法:在 PostsPage 里,如果 posts.length === 0,显示「暂时没有文章」。

看参考答案
tsx
if (posts.length === 0) {  return (    <main>      <h1>文章列表</h1>      <p>暂时没有文章。</p>    </main>  );}

常见错误剖析:把 await fetch(...) 写进 Client Component 的点击事件里,然后又期待页面首次打开时就有数据。服务器取数和浏览器交互是两种路径;这一节练的是页面加载前在服务器准备数据。

自评