跳到内容

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 语法,而无需 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

如果您访问您的 localhost,您应该会看到仅从数据库返回了最后 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 预渲染路由以提高性能,这称为 静态渲染。因此,如果您的数据发生更改,它将不会反映在您的仪表板中。

让我们在本章中讨论第 1 点,然后在下一章中详细了解第 2 点。

什么是请求瀑布流?

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

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 中不同的渲染模式。