数据获取
现在您已经创建并填充了数据库,让我们讨论为应用程序获取数据的不同方式,并构建仪表盘概览页面。
选择数据获取方式
API 层
API 是应用程序代码与数据库之间的中间层。在以下情况下您可能会使用 API:
- 使用提供 API 的第三方服务时
- 从客户端获取数据时,您需要运行在服务端的 API 层来避免向客户端暴露数据库凭证
在 Next.js 中,您可以使用 路由处理器 (Route Handlers) 创建 API 端点。
数据库查询
构建全栈应用时,您还需要编写与数据库交互的逻辑。对于 Postgres 等关系型数据库,您可以使用 SQL 或 ORM。
以下情况需要编写数据库查询:
- 创建 API 端点时,需要编写与数据库交互的逻辑
- 使用 React 服务端组件 (Server Components) 时(在服务端获取数据),您可以跳过 API 层直接查询数据库,而不会向客户端暴露数据库凭证
让我们进一步了解 React 服务端组件。
使用服务端组件获取数据
默认情况下,Next.js 应用使用 React 服务端组件。使用服务端组件获取数据是一种较新的方式,具有以下优势:
- 服务端组件原生支持 JavaScript Promise,为数据获取等异步任务提供了解决方案。您可以直接使用
async/await
语法,无需依赖useEffect
、useState
或其他数据获取库 - 服务端组件运行在服务端,因此可以将昂贵的数据获取和逻辑保留在服务端,仅将结果发送到客户端
- 由于服务端组件运行在服务端,您可以直接查询数据库而无需额外的 API 层,从而减少需要编写和维护的代码量
使用 SQL
在仪表盘应用中,您将使用 postgres.js 库和 SQL 编写数据库查询。我们选择 SQL 的原因如下:
- SQL 是查询关系型数据库的行业标准(例如 ORM 底层也是生成 SQL)
- 掌握 SQL 基础有助于理解关系型数据库的核心原理,这些知识可应用于其他工具
- SQL 功能强大,可以获取和操作特定数据
postgres.js
库提供了防止 SQL 注入的保护机制
如果您之前没有使用过 SQL 也不用担心——我们已经为您准备好了查询语句。
转到 /app/lib/data.ts
文件,您会看到我们正在使用 postgres
。sql
函数允许您查询数据库:
您可以在服务端的任何地方(如服务端组件中)调用 sql
。但为了便于导航组件,我们将所有数据查询保留在 data.ts
文件中,您可以将其导入组件使用。
注意: 如果您在第 6 章使用了其他数据库提供商,需要更新
/app/lib/data.ts
中的查询语句以适配您的提供商。
为仪表盘概览页面获取数据
现在您已经了解了不同的数据获取方式,让我们为仪表盘概览页面获取数据。导航到 /app/dashboard/page.tsx
,粘贴以下代码并仔细阅读:
上述代码有意被注释掉。现在我们将逐步解析每个部分:
page
是一个 async 服务端组件,允许您使用await
获取数据- 还有三个接收数据的组件:
<Card>
、<RevenueChart>
和<LatestInvoices>
,它们当前被注释且尚未实现
为 <RevenueChart/>
获取数据
要为 <RevenueChart/>
组件获取数据,从 data.ts
导入 fetchRevenue
函数并在组件中调用:
接下来:
- 取消注释
<RevenueChart/>
组件 - 导航到组件文件 (
/app/ui/dashboard/revenue-chart.tsx
) 并取消注释其中的代码 - 检查
localhost:3000
,您应该能看到使用revenue
数据的图表

让我们继续导入更多数据并显示在仪表盘上。
为 <LatestInvoices/>
获取数据
对于 <LatestInvoices />
组件,我们需要获取按日期排序的最新 5 条发票记录。
您可以使用 JavaScript 获取所有发票并进行排序。虽然当前数据量小没有问题,但随着应用增长,这会显著增加每次请求传输的数据量和排序所需的 JavaScript 代码。
与其在内存中排序,不如使用 SQL 查询直接获取最新的 5 条发票。例如,这是 data.ts
文件中的 SQL 查询:
在页面中导入 fetchLatestInvoices
函数:
然后取消注释 <LatestInvoices />
组件。您还需要取消注释位于 /app/ui/dashboard/latest-invoices
的 <LatestInvoices />
组件中的相关代码。
访问本地主机时,您应该看到数据库只返回了最新的 5 条记录。希望您开始体会到直接查询数据库的优势!

练习:为 <Card>
组件获取数据
现在轮到您为 <Card>
组件获取数据了。这些卡片将显示以下数据:
- 已收发票总金额
- 待处理发票总金额
- 发票总数
- 客户总数
再次提醒,您可能会想获取所有发票和客户数据,然后用 JavaScript 处理。例如,使用 Array.length
获取发票和客户总数:
但使用 SQL 可以只获取您需要的数据。虽然比使用 Array.length
代码量稍多,但意味着请求期间需要传输的数据更少。这是 SQL 的实现方式:
您需要导入的函数名为 fetchCardData
,并需要对函数返回的值进行解构。
提示:
- 查看卡片组件了解它们需要哪些数据
- 检查
data.ts
文件了解函数返回的内容
准备好后,展开下方查看最终代码:
太棒了!您现在已经为仪表盘概览页面获取了所有数据。页面应该如下所示:

但是...有两点需要注意:
- 数据请求无意中相互阻塞,形成了请求瀑布流
- 默认情况下,Next.js 会预渲染路由以提高性能,这称为静态渲染。因此如果数据变化,仪表盘不会更新
我们将在本章讨论第一点,然后在下一章详细探讨第二点。
什么是请求瀑布流?
"瀑布流"指的是一系列相互依赖的网络请求。在数据获取场景中,每个请求必须等待前一个请求返回数据后才能开始。

例如,我们需要等待 fetchRevenue()
执行完成后,fetchLatestInvoices()
才能开始运行,依此类推。
这种模式不一定不好。有时您可能希望形成瀑布流,因为需要满足某些条件才能发起下一个请求。例如,您可能希望先获取用户 ID 和个人资料信息,然后根据 ID 获取好友列表。这种情况下,每个请求都依赖于前一个请求返回的数据。
然而,这种行为也可能是无意的,并影响性能。
并行数据获取
避免瀑布流的常见方法是同时发起所有数据请求——即并行获取。
在 JavaScript 中,您可以使用 Promise.all()
或 Promise.allSettled()
函数同时发起所有 Promise。例如,在 data.ts
中,我们在 fetchCardData()
函数中使用了 Promise.all()
:
使用这种模式,您可以:
- 同时开始所有数据获取,比等待每个请求依次完成的瀑布流方式更快
- 使用适用于任何库或框架的 JavaScript 原生模式
但这种 JavaScript 模式有一个缺点:如果某个数据请求比其他请求慢会怎样?我们将在下一章详细探讨。