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 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
函数 允许您查询数据库
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
。
如果您访问您的 localhost,您应该会看到仅从数据库返回了最后 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 预渲染路由以提高性能,这称为 静态渲染。因此,如果您的数据发生更改,它将不会反映在您的仪表板中。
让我们在本章中讨论第 1 点,然后在下一章中详细了解第 2 点。
什么是请求瀑布流?
“瀑布流”指的是一系列网络请求,这些请求依赖于先前请求的完成。在数据获取的情况下,每个请求只能在前一个请求返回数据后才能开始。

例如,我们需要等待 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 模式有一个缺点:如果一个数据请求比所有其他请求都慢,会发生什么?让我们在下一章中了解更多信息。
这有帮助吗?