跳至内容

7

获取数据

现在您已经创建并填充了数据库,让我们讨论一下您可以使用哪些不同的方法来为您的应用程序获取数据,并构建您的仪表板概览页面。

在本节中…

以下是我们将介绍的主题

了解一些获取数据的方法:API、ORM、SQL 等。

服务器组件如何帮助您更安全地访问后端资源。

什么是网络瀑布。

如何使用 JavaScript 模式实现并行数据获取。

选择如何获取数据

API 层

API 是应用程序代码和数据库之间的中间层。在一些情况下,您可能会使用 API

  • 如果您正在使用提供 API 的第三方服务。
  • 如果您是从客户端获取数据,您希望拥有一个在服务器上运行的 API 层,以避免将您的数据库密钥暴露给客户端。

在 Next.js 中,您可以使用路由处理器创建 API 端点。

数据库查询

当您创建全栈应用程序时,您还需要编写与数据库交互的逻辑。对于关系型数据库(如 Postgres),您可以使用 SQL 或ORM来完成此操作。

在一些情况下,您必须编写数据库查询

  • 创建 API 端点时,需要编写与数据库交互的逻辑。
  • 如果您使用的是 React 服务器组件(在服务器上获取数据),您可以跳过 API 层,并直接查询您的数据库,而无需冒将您的数据库密钥暴露给客户端的风险。

让我们进一步了解 React 服务器组件。

使用服务器组件获取数据

默认情况下,Next.js 应用程序使用 **React 服务器组件**。使用服务器组件获取数据是一种相对较新的方法,它有一些好处

  • 服务器组件支持 Promise,为异步任务(如数据获取)提供了一种更简单的解决方案。您可以使用 `async/await` 语法,而无需使用 `useEffect`、`useState` 或数据获取库。
  • 服务器组件在服务器上执行,因此您可以将昂贵的`数据获取`和逻辑保留在服务器上,只将结果发送到客户端。
  • 如前所述,由于服务器组件在服务器上执行,因此您可以直接查询数据库,而无需额外的 API 层。

使用 SQL

对于您的仪表板项目,您将使用Vercel Postgres SDK和 SQL 编写数据库查询。我们使用 SQL 有几个原因

  • SQL 是查询关系型数据库的行业标准(例如,ORM 在后台生成 SQL)。
  • 了解 SQL 的基本知识可以帮助您理解关系型数据库的基础知识,从而将您的知识应用于其他工具。
  • SQL 功能多样,允许您获取和操作特定数据。
  • Vercel Postgres SDK 提供了针对SQL 注入的保护。

如果您以前从未使用过 SQL,请不要担心 - 我们已为您提供了查询。

转到 ` /app/lib/data.ts`,在这里您会看到我们正在导入来自 `@vercel/postgres` 的`sql` 函数。此函数允许您查询您的数据库

/app/lib/data.ts
import { sql } from '@vercel/postgres';
 
// ...

您可以在任何服务器组件内部调用 `sql`。但是,为了让您可以更轻松地浏览组件,我们将所有数据查询都保存在 `data.ts` 文件中,您可以将其导入到组件中。

**注意:**如果您在第 6 章中使用了您自己的数据库提供程序,则需要更新数据库查询以使其与您的提供程序一起使用。您可以在 ` /app/lib/data.ts` 中找到查询。

为仪表板概览页面获取数据

现在您已经了解了不同的数据获取方法,让我们为仪表板概览页面获取数据。导航到 ` /app/dashboard/page.tsx`,粘贴以下代码,并花一些时间探索它

/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>
  );
}

在上面的代码中

  • 页面是一个 **异步** 组件。这允许您使用 `await` 来获取数据。
  • 还有 3 个接收数据的组件:`<Card>`、`<RevenueChart>` 和 `<LatestInvoices>`。它们目前已注释掉,以防止应用程序出错。

为 **`<RevenueChart/>`** 获取数据

要为 `<RevenueChart/>` 组件获取数据,请从 `data.ts` 中导入 `fetchRevenue` 函数并在您的组件内部调用它

/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';
import { fetchRevenue } from '@/app/lib/data';
 
export default async function Page() {
  const revenue = await fetchRevenue();
  // ...
}

然后,取消注释 `<RevenueChart/>` 组件,导航到组件文件(` /app/ui/dashboard/revenue-chart.tsx`)并在其中取消注释代码。检查您的本地主机,您应该能够看到一个使用 `revenue` 数据的图表。

Revenue chart showing the total revenue for the last 12 months

让我们继续导入更多数据查询!

为 **`<LatestInvoices/>`** 获取数据

对于 `<LatestInvoices />` 组件,我们需要获取最新的 5 个发票,按日期排序。

您可以使用 JavaScript 获取所有发票并进行排序。由于我们的数据量较小,这不成问题,但随着应用程序的增长,它可能会显著增加每次请求传输的数据量以及用于排序的 JavaScript 代码量。

与其在内存中对最新的发票进行排序,不如使用 SQL 查询仅获取最新的 5 张发票。例如,这是您 data.ts 文件中的 SQL 查询

/app/lib/data.ts
// 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 函数

/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';
import { fetchRevenue, fetchLatestInvoices } from '@/app/lib/data';
 
export default async function Page() {
  const revenue = await fetchRevenue();
  const latestInvoices = await fetchLatestInvoices();
  // ...
}

然后,取消 <LatestInvoices /> 组件的注释。您还需要取消位于 /app/ui/dashboard/latest-invoices<LatestInvoices /> 组件本身中的相关代码注释。

如果您访问您的本地主机,您应该会看到数据库中只返回了最后 5 条记录。希望您已经开始看到直接查询数据库的优势!

Latest invoices component alongside the revenue chart

练习:为 <Card> 组件获取数据

现在轮到您为 <Card> 组件获取数据了。卡片将显示以下数据

  • 已收发票的总金额。
  • 待处理发票的总金额。
  • 发票总数。
  • 客户总数。

同样,您可能会想要获取所有发票和客户,并使用 JavaScript 操作数据。例如,您可以使用 Array.length 获取发票和客户的总数

const totalInvoices = allInvoices.length;
const totalCustomers = allCustomers.length;

但使用 SQL,您可以仅获取所需的数据。它比使用 Array.length 略长,但意味着在请求期间需要传输的数据更少。这是 SQL 的替代方案

/app/lib/data.ts
const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;

您需要导入的函数称为 fetchCardData。您需要解构函数返回的值。

提示

  • 检查卡片组件以查看它们需要哪些数据。
  • 检查 data.ts 文件以查看该函数返回什么。

准备好后,展开下面的切换按钮以查看最终代码

太棒了!您现在已经为仪表板概览页面获取了所有数据。您的页面应该如下所示

Dashboard page with all the data fetched

但是...有两件事您需要注意

  1. 数据请求意外地相互阻塞,形成了一个**请求瀑布**。
  2. 默认情况下,Next.js 会**预渲染**路由以提高性能,这称为**静态渲染**。因此,如果您的数据发生更改,它将不会反映在您的仪表板上。

让我们在本节中讨论第一个问题,然后在下一节中详细了解第二个问题。

什么是请求瀑布?

“瀑布”指的是一系列网络请求,这些请求依赖于先前请求的完成。在数据获取的情况下,每个请求只有在先前的请求返回数据后才能开始。

Diagram showing time with sequential data fetching and parallel data fetching

例如,我们需要等待 fetchRevenue() 执行完毕,然后 fetchLatestInvoices() 才能开始运行,依此类推。

/app/dashboard/page.tsx
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()

/app/lib/data.ts
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 模式有一个**缺点**:如果一个数据请求比所有其他请求都慢会发生什么?

您已完成本章7

您了解了在 Next.js 中获取数据的几种不同方法。

下一章

8:静态和动态渲染

了解 Next.js 中的不同渲染模式。