数据获取 3个月前

编程语言
859
数据获取

现在你已经创建并填充了数据库,让我们讨论你可以用来获取应用数据的不同方式,并构建你的仪表板概览页面。

在本章中

我们将涵盖以下主题:

  1. 数据获取方法的选择: 了解一些数据获取的方式,比如 API、ORM 和 SQL 等。
  2. 服务器组件的使用: 学习服务器组件如何帮助你更安全地访问后端资源。
  3. 什么是网络瀑布流: 了解网络瀑布流的概念。
  4. 实现并行数据获取: 使用 JavaScript 模式来实现并行的数据获取。

选择数据获取方式

API 层

API 是你的应用代码和数据库之间的中介层。你可能会在以下几种情况下使用 API:

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

在 Next.js 中,你可以使用 路由处理程序(Route Handlers) 来创建 API 端点。

数据库查询

当你创建一个全栈应用时,你还需要编写逻辑来与数据库交互。对于 关系型数据库(如 Postgres),你可以使用 SQL 或 ORM

在以下几种情况下,你需要编写数据库查询:

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

现在是测试时间!

在以下场景中,你不应该直接查询数据库的是哪个?

A. 当你在客户端获取数据时 B. 当你在服务器上获取数据时 C. 当你创建自己的 API 层与数据库交互时

查看答案

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

使用服务器组件获取数据

默认情况下,Next.js 应用使用 React 服务器组件。使用服务器组件获取数据是一种相对较新的方法,它有几个优点:

  • 服务器组件支持 Promise,提供了一个更简单的解决方案来处理异步任务,比如数据获取。你可以使用 async/await 语法,而无需使用 useEffectuseState 或数据获取库。
  • 服务器组件在服务器上执行,因此你可以将昂贵的数据获取和逻辑保留在服务器上,只将结果发送到客户端。
  • 正如前面提到的,服务器组件在服务器上执行,因此你可以直接查询数据库,而无需额外的 API 层。

现在是测试时间!

使用 React 服务器组件获取数据的一个优点是什么?

A. 它们自动保护你免受 SQL 注入攻击。 B. 它们允许你直接从服务器查询数据库,而无需额外的 API 层。 C. 它们要求你使用 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 文件中,你可以将它们导入到组件中。

现在是测试时间!

测试你的知识,看看你刚学到的内容。

SQL 在数据获取方面允许你做什么?

A. 无差别地获取所有数据 B. 获取和操作特定数据 C. 自动缓存数据以提高性能 D. 动态更改数据库模式

查看答案

注意: 如果你在第六章中使用了自己的数据库提供商,你需要更新数据库查询以使其适应你的提供商。你可以在 /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 是一个 异步 组件。这允许你使用 await 来获取数据。
  • 还有三个接收数据的组件:<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 数据的图表。

image

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

为 <LatestInvoices/> 获取数据

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

你可以获取所有发票,然后使用 JavaScript 对它们进行排序。虽然这样做没有问题,因为数据量小,但随着应用程序的增长,这会显著增加每次请求的数据传输量以及需要排序的 JavaScript 代码量。

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

// /app/lib/data.ts
// 获取按日期排序的最后 5 张发票
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 张发票。希望你开始看到直接查询数据库的优势!

image

练习:为 <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 文件以了解函数返回的内容。

一旦准备好,查看最终代码:

解决方案

// /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, fetchCardData } from '@/app/lib/data';

export default async function Page() {
  const revenue = await fetchRevenue();
  const latestInvoices = await fetchLatestInvoices();
  const {
    numberOfInvoices,
    numberOfCustomers,
    totalPaidInvoices,
    totalPendingInvoices,
  } = await fetchCardData();
  
  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>
  );
}

太棒了!你现在已经为仪表板概览页面获取了所有数据。你的页面应该看起来像这样:

image

不过,有两件事你需要注意:

  1. 数据请求无意中互相阻塞,形成了 **请求瀑布流(request waterfall)**。
  2. 默认情况下,Next.js 预渲染(prerenders) 路由以提高性能,这叫做 **静态渲染(Static Rendering)**。所以如果数据发生变化,它不会在你的仪表板中反映出来。

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

什么是请求瀑布流?

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

image

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

// /app/dashboard/page.tsx
const revenue = await fetchRevenue();
const latestInvoices = await fetchLatestInvoices(); // 等待 fetchRevenue() 完成
const {
  numberOfInvoices,
  numberOfCustomers,
  totalPaidInvoices,
  totalPendingInvoices,
} = await fetchCardData(); // 等待 fetchLatestInvoices() 完成

这种模式不一定是坏的。在某些情况下,你可能希望使用瀑布流,因为你希望在进行下一个请求之前满足某个条件。例如,你可能希望首先获取用户的 ID 和个人信息。获得 ID 后,你可能会继续获取他们的好友列表。在这种情况下,每个请求都依赖于前一个请求返回的数据。

然而,这种行为也可能是无意的,会影响性能

现在是测试时间!

在什么情况下你可能会使用瀑布流模式?

A. 为了在进行下一个请求之前满足条件 B. 为了同时发起所有请求 C. 通过一次获取减少服务器负载

查看答案

并行数据获取

避免瀑布流的一种常见方法是同时发起所有数据请求——即并行处理。

在 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 模式有一个缺点: 如果某个数据请求比其他请求都慢,该怎么办?

image
EchoEcho官方
无论前方如何,请不要后悔与我相遇。
1377
发布数
439
关注者
2222918
累计阅读

热门教程文档

Objective-C
29小节
Java
12小节
Golang
23小节
MyBatis
19小节
React Native
40小节