跳到内容

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 语法,而无需使用 useEffectuseState 或其他数据获取库。
  • 服务器组件在服务器端运行,因此您可以将昂贵的数据获取和逻辑保留在服务器端,只将结果发送到客户端。
  • 由于服务器组件在服务器端运行,您可以直接查询数据库,而无需额外的 API 层。这节省了您编写和维护额外代码的精力。

使用 SQL

对于您的仪表盘应用程序,您将使用 postgres.js 库和 SQL 编写数据库查询。我们将使用 SQL 的原因有:

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

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

打开 /app/lib/data.ts。在这里您会看到我们正在使用 postgressql 函数 允许您查询数据库。

/app/lib/data.ts
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,粘贴以下代码,并花一些时间进行探索。

/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 函数并在组件内部调用它

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

接下来,让我们执行以下操作:

  1. 取消注释 <RevenueChart/> 组件。
  2. 导航到组件文件 (/app/ui/dashboard/revenue-chart.tsx) 并取消注释其中的代码。
  3. 检查 localhost:3000,您应该会看到一个使用 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 /> 组件。您还需要取消注释 <LatestInvoices /> 组件本身中位于 /app/ui/dashboard/latest-invoices 的相关代码。

如果您访问您的本地主机,您应该会看到数据库只返回了最后 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 中不同的渲染模式。

App Router: 获取数据 | Next.js 框架