7
章节7
数据获取
现在您已经创建并填充了数据库,接下来我们将讨论为应用程序获取数据的不同方法,并构建您的仪表盘概览页面。
在本章中...
我们将涵盖以下主题:
了解一些获取数据的方法:API、ORM、SQL 等。
服务器组件如何帮助您更安全地访问后端资源。
什么是网络瀑布。
如何使用 JavaScript 模式实现并行数据获取。
选择如何获取数据
API 层
API 是您的应用程序代码和数据库之间的中间层。在以下几种情况下您可能需要使用 API:
- 如果您正在使用提供 API 的第三方服务。
- 如果您正在从客户端获取数据,您需要有一个在服务器上运行的 API 层,以避免将数据库秘密暴露给客户端。
在 Next.js 中,您可以使用路由处理程序创建 API 端点。
数据库查询
在创建全栈应用程序时,您还需要编写与数据库交互的逻辑。对于像 Postgres 这样的关系型数据库,您可以使用 SQL 或 ORM 来完成。
在以下几种情况下,您必须编写数据库查询:
- 创建 API 端点时,您需要编写与数据库交互的逻辑。
- 如果您使用 React 服务器组件(在服务器端获取数据),您可以跳过 API 层,直接查询数据库,而无需冒着将数据库秘密暴露给客户端的风险。
让我们了解更多关于 React 服务器组件的知识。
使用服务器组件获取数据
默认情况下,Next.js 应用程序使用 **React 服务器组件**。使用服务器组件获取数据是一种相对较新的方法,使用它们有以下几个好处:
- 服务器组件支持 JavaScript Promises,为像数据获取这样的异步任务提供了原生的解决方案。您可以使用
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 函数 允许您查询数据库。
import postgres from 'postgres';
const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });
// ...您可以在服务器上的任何位置(例如服务器组件)调用 sql。但是为了让您更轻松地导航组件,我们将所有数据查询都保存在 data.ts 文件中,您可以将它们导入到组件中。
注意:如果您在第 6 章中使用了自己的数据库提供商,您需要更新数据库查询以使其与您的提供商兼容。您可以在
/app/lib/data.ts中找到这些查询。
获取仪表板概览页面的数据
现在您已经了解了获取数据的不同方法,接下来我们为仪表板概览页面获取数据。导航到 /app/dashboard/page.tsx,粘贴以下代码,并花一些时间进行探索。
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
export default async function Page() {
return (
<main>
<h1 className={`${lusitana.className} mb-4 text-xl md:text-2xl`}>
Dashboard
</h1>
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
{/* <Card title="Collected" value={totalPaidInvoices} type="collected" /> */}
{/* <Card title="Pending" value={totalPendingInvoices} type="pending" /> */}
{/* <Card title="Total Invoices" value={numberOfInvoices} type="invoices" /> */}
{/* <Card
title="Total Customers"
value={numberOfCustomers}
type="customers"
/> */}
</div>
<div className="mt-6 grid grid-cols-1 gap-6 md:grid-cols-4 lg:grid-cols-8">
{/* <RevenueChart revenue={revenue} /> */}
{/* <LatestInvoices latestInvoices={latestInvoices} /> */}
</div>
</main>
);
}上面的代码是故意注释掉的。现在我们将开始逐一讲解每一部分。
- 这个 `page` 是一个 **async** 服务器组件。这允许您使用 `await` 来获取数据。
- 还有 3 个接收数据的组件:
<Card>、<RevenueChart>和<LatestInvoices>。它们目前被注释掉了,尚未实现。
为 <RevenueChart/> 获取数据
要为 <RevenueChart/> 组件获取数据,请从 data.ts 导入 fetchRevenue 函数并在组件内部调用它
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
import { fetchRevenue } from '@/app/lib/data';
export default async function Page() {
const revenue = await fetchRevenue();
// ...
}接下来,让我们执行以下操作:
- 取消注释
<RevenueChart/>组件。 - 导航到组件文件 (
/app/ui/dashboard/revenue-chart.tsx) 并取消注释其中的代码。 - 检查
localhost:3000,您应该会看到一个使用revenue数据的图表。

让我们继续导入更多数据并将其显示在仪表板上。
为 <LatestInvoices/> 获取数据
对于 <LatestInvoices /> 组件,我们需要获取按日期排序的最近 5 张发票。
您可以获取所有发票并使用 JavaScript 对其进行排序。对于我们的小型数据来说这不是问题,但随着应用程序的增长,这会显著增加每次请求传输的数据量以及排序所需的 JavaScript 代码。
您可以不通过在内存中对最新发票进行排序,而是使用 SQL 查询只获取最近 5 张发票。例如,这是您的 data.ts 文件中的 SQL 查询:
// Fetch the last 5 invoices, sorted by date
const data = await sql<LatestInvoiceRaw[]>`
SELECT invoices.amount, customers.name, customers.image_url, customers.email
FROM invoices
JOIN customers ON invoices.customer_id = customers.id
ORDER BY invoices.date DESC
LIMIT 5`;在您的页面中,导入 fetchLatestInvoices 函数
import { Card } from '@/app/ui/dashboard/cards';
import RevenueChart from '@/app/ui/dashboard/revenue-chart';
import LatestInvoices from '@/app/ui/dashboard/latest-invoices';
import { lusitana } from '@/app/ui/fonts';
import { fetchRevenue, fetchLatestInvoices } from '@/app/lib/data';
export default async function Page() {
const revenue = await fetchRevenue();
const latestInvoices = await fetchLatestInvoices();
// ...
}然后,取消注释 <LatestInvoices /> 组件。您还需要取消注释 <LatestInvoices /> 组件本身中位于 /app/ui/dashboard/latest-invoices 的相关代码。
如果您访问您的本地主机,您应该会看到数据库只返回了最后 5 条数据。希望您已经开始看到直接查询数据库的优势了!

练习:为 <Card> 组件获取数据
现在轮到您为 <Card> 组件获取数据了。这些卡片将显示以下数据:
- 已收款发票总额。
- 待处理发票总额。
- 发票总数。
- 客户总数。
您可能会再次尝试获取所有发票和客户,并使用 JavaScript 操作数据。例如,您可以使用 Array.length 获取发票和客户的总数。
const totalInvoices = allInvoices.length;
const totalCustomers = allCustomers.length;但使用 SQL,您可以只获取您需要的数据。虽然比使用 Array.length 稍微长一些,但这意味着在请求期间需要传输的数据更少。这是 SQL 的替代方案:
const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;您需要导入的函数名为 fetchCardData。您需要解构从该函数返回的值。
提示
- 检查卡片组件,看看它们需要哪些数据。
- 检查
data.ts文件,看看函数返回了什么。
准备好后,展开下面的开关以查看最终代码
太棒了!您现在已经为仪表板概览页面获取了所有数据。您的页面应该看起来像这样:

然而...有两件事需要注意:
- 数据请求无意中相互阻塞,造成了**请求瀑布**。
- 默认情况下,Next.js 会**预渲染**路由以提高性能,这称为**静态渲染**。因此,如果您的数据发生变化,它不会反映在您的仪表盘中。
让我们在本章讨论第一点,然后在下一章详细探讨第二点。
什么是请求瀑布?
“瀑布”是指一系列网络请求,这些请求的完成依赖于先前请求的完成。在数据获取的情况下,每个请求只能在前一个请求返回数据后才能开始。

例如,我们需要等待 fetchRevenue() 执行完毕,然后 fetchLatestInvoices() 才能开始运行,依此类推。
const revenue = await fetchRevenue();
const latestInvoices = await fetchLatestInvoices(); // wait for fetchRevenue() to finish
const {
numberOfInvoices,
numberOfCustomers,
totalPaidInvoices,
totalPendingInvoices,
} = await fetchCardData(); // wait for fetchLatestInvoices() to finish这种模式不一定不好。在某些情况下,您可能需要瀑布,因为您希望在发出下一个请求之前满足某个条件。例如,您可能希望首先获取用户的 ID 和个人资料信息。一旦您拥有 ID,您就可以继续获取他们的朋友列表。在这种情况下,每个请求都依赖于前一个请求返回的数据。
然而,这种行为也可能并非有意为之,并会影响性能。
并行数据获取
避免瀑布的一种常见方法是同时启动所有数据请求——并行进行。
在 JavaScript 中,您可以使用 Promise.all() 或 Promise.allSettled() 函数同时启动所有 promise。例如,在 data.ts 中,我们在 fetchCardData() 函数中使用了 Promise.all()
export async function fetchCardData() {
try {
const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;
const invoiceStatusPromise = sql`SELECT
SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS "paid",
SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) AS "pending"
FROM invoices`;
const data = await Promise.all([
invoiceCountPromise,
customerCountPromise,
invoiceStatusPromise,
]);
// ...
}
}通过使用这种模式,您可以:
- 同时开始执行所有数据获取,这比等待每个请求在瀑布中完成要快。
- 使用可应用于任何库或框架的本地 JavaScript 模式。
然而,仅依赖这种 JavaScript 模式有一个**缺点**:如果一个数据请求比其他所有请求都慢,会发生什么?让我们在下一章中了解更多。
这有帮助吗?



