likes
comments
collection
share

Next.js14从入门到实战012:NextJS基础篇之页面获取数据

作者站长头像
站长
· 阅读数 30

现在,您已经创建了数据库并为其添加了数据,让我们来讨论为应用程序获取数据的不同方法,并构建dashboard概览页面。

本章你将学习到

  • 了解一些获取数据的方法:应用程序接口、ORM、SQL 等。
  • 服务器组件如何帮助您更安全地访问后端资源。
  • 什么是网络瀑布
  • 如何使用 JavaScript Pattern实现并行数据获取。

选择获取数据的方式

API layer 应用程序接口层

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

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

在 Next.js 中,您可以使用路由控制器来创建 API 。

数据库查询

在创建全栈应用程序时,您还需要编写与数据库交互的逻辑。对于 Postgres 这样的关系型数据库,可以使用 SQL 或 Prisma 这样的 ORM 来实现。

当然,在一些情况下,您还需要编写数据库查询:

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

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

使用服务端组件获取数据

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

  • 服务端组件支持promises,为数据获取等异步任务提供了更简单的解决方案。您可以使用 async/await 语法,而无需使用 useEffect 、 useState 或数据获取库。

  • 服务端组件在服务器上执行,因此可以在服务器上保留昂贵的数据获取和逻辑,只将结果发送给客户端。

  • 如前所述,由于服务端组件是在服务器上执行的,因此可以直接查询数据库,而无需额外的 API 层。

使用SQL

在dashboard项目中,您将使用 Vercel Postgres SDK 和 SQL 编写数据库查询。使用 SQL 有几个原因:

  • SQL 是查询关系数据库的行业标准(例如,ORM 在底层也是生成 SQL)。
  • 对 SQL 有基本的了解可以帮助你理解关系数据库的基本原理,从而将知识应用到其他工具中。
  • SQL 用途广泛,可让您获取和处理特定数据。
  • Vercel Postgres SDK 提供防止 SQL 注入的保护。

如果您以前没有使用过 SQL,也不用担心,我们已经为您提供了查询。 转到 /app/lib/data.ts ,在这里你会看到我们从 @vercel/postgres 导入了 sql 函数。通过该函数,您可以查询数据库:

import { sql } from '@vercel/postgres';
 
// ...

您可以在任何服务器组件中调用 sql 。不过,为了让你更轻松地查看组件,我们在 data.ts 文件中保留了所有数据查询,你可以将它们导入到组件中。

为dashboard 页面获取数据

现在,您已经了解了获取数据的不同方法,让我们为为dashboard页面获取数据。打开 /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 抓取数据。
  • 还有 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 ) 并取消对其中代码的注释。检查本地主机,应该可以看到使用 revenue 数据的图表。

Next.js14从入门到实战012:NextJS基础篇之页面获取数据

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

获取<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 中。

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

Next.js14从入门到实战012:NextJS基础篇之页面获取数据

练习:为<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 文件,查看函数的返回值。

怎样?如果你已经写完了,可以核对下下面的答案

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

你现在已经获取了仪表盘概览页面的所有数据。您的页面应该是这样的

Next.js14从入门到实战012:NextJS基础篇之页面获取数据

不过......有两件事您需要注意:

  1. 数据请求无意中相互阻塞,形成了请求瀑布。
  2. 默认情况下,Next.js 会预先渲染路由以提高性能,这就是所谓的静态渲染。因此,如果数据发生变化,它不会反映在dashboard上。

什么是请求瀑布?

瀑布指的是一连串依赖于前一个请求完成的网络请求。就数据获取而言,只有在前一个请求返回数据后,才能开始每个请求。

Next.js14从入门到实战012:NextJS基础篇之页面获取数据

例如,在 fetchLatestInvoices() 开始运行之前,我们需要等待 fetchRevenue() 执行,依此类推。

// /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.js
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 模式。

不过,这种模式有一个缺点:如果一个数据请求比其他所有数据请求都慢,该怎么办?

转载自:https://juejin.cn/post/7360879591236681762
评论
请登录