北鸟南游的博客 北鸟南游的博客
首页
  • 前端文章

    • JavaScript
    • Nextjs
  • 界面

    • html
    • css
  • 计算机基础
  • 后端语言
  • linux
  • mysql
  • 工具类
  • 面试相关
  • 图形学入门
  • 入门算法
  • 极客专栏
  • 慕课专栏
  • 电影资源
  • 儿童动漫
  • 英文
关于我
归档
GitHub (opens new window)
首页
  • 前端文章

    • JavaScript
    • Nextjs
  • 界面

    • html
    • css
  • 计算机基础
  • 后端语言
  • linux
  • mysql
  • 工具类
  • 面试相关
  • 图形学入门
  • 入门算法
  • 极客专栏
  • 慕课专栏
  • 电影资源
  • 儿童动漫
  • 英文
关于我
归档
GitHub (opens new window)
  • JavaScript

    • 原生js
    • vue
    • react
    • node
    • nextjs
      • 01-nextjs起步-[翻译官网案例]
      • 02-样式设置【翻译官网案例】
      • 03-优化字体和图像
      • 04-创建layouts布局和pages页面
      • 05-在页面之间导航
      • 06-设置数据库
      • 07-fetch获取数据
        • 本章目标
        • 选择如何获取数据
          • API层
          • 数据库查询
          • 使用react server组件获取数据
          • 使用SQL
        • 获取仪表板概述页的数据
        • 获取 RevenueChart 组件数据
        • 获取 LatestInvoices 组件数据
        • 获取Card组件数据
        • 什么是请求waterfall?
        • 并行获取数据
      • 08-静态和动态渲染
      • 09-Streaming数据模式
      • 10-部分预渲染
      • 11-添加搜索和分页
      • 12-提交数据
      • 13-处理错误
      • 14-提高交互性
      • 15-添加认证授权
      • 16-添加Metadata头数据
    • 其它框架
  • 界面

  • front
  • javascript
  • nextjs
北鸟南游
2024-06-24
目录

07-fetch获取数据

原文链接:https://nextjs.org/learn/dashboard-app/fetching-data (opens new window)

现在您已经创建并初始化了数据库。让我们讨论一下为应用程序获取数据的不同方法,并构建您的仪表板概览页面。

# 本章目标

  • 了解一些获取数据的方法:APIs, ORMs, SQL, etc.
  • 服务器组件如何帮助您更安全地访问后端资源
  • waterfalls是什么样的网络
  • 如何使用JavaScript模式实现并行数据提取

# 选择如何获取数据

# API层

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

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

在Next.js中,您可以使用路由处理程序 (opens new window)创建API端点。

# 数据库查询

当您创建一个全栈应用程序时,您还需要编写与数据库交互的逻辑。对于像Postgres这样的关系数据库 (opens new window),您可以使用SQL或ORM (opens new window)来实现这一点。 在一些情况下,您必须编写数据库查询:

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

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

# 使用react server组件获取数据

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

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

# 使用SQL

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

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

如果您以前没有使用过SQL,请不要担心,我们已经为您提供了查询。 转到/app/lib/data.ts文件,在这里你会看到我们正在从@vercel/postgres导入sql函数。此功能允许您查询数据库:

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

// ...
1
2
3

您可以在任何服务器组件内部调用sql。但为了让您更容易地浏览组件,我们已经将所有的数据查询保存在data.ts文件中,您可以将它们导入到组件中。

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

# 获取仪表板概述页的数据

dashboard overview页面数据。 现在您已经了解了获取数据的不同方法,让我们为仪表板概述页面获取数据。导航到/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>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

在上面的代码中:

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

# 获取 RevenueChart 组件数据

为<RevenueChart/>组件获取数据。从data.ts导入fetchRevenue函数,并在组件内部调用它:修改第5行和第8行代码

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
2
3
4
5
6
7
8
9
10

然后,取消注释<RevenueChart/>组件,导航到组件文件(/app/ui/dashboard/revenue-chart.tsx);并取消注释其中的代码。检查 localhost,您应该能够看到使用收入数据的图表。 image.png 让我们继续导入更多的数据查询!

# 获取 LatestInvoices 组件数据

对于<LatestInvoices/>组件,我们需要获得按日期排序的最新5张发票。 您可以获取所有invoices并使用JavaScript对其进行排序。这不是问题,因为我们的数据很小,但随着应用程序的增长,它可以显著增加每次请求传输的数据量以及对其进行排序所需的JavaScript。 代替用排序获取最新的5条数据,您可以使用SQL查询只获取最后5个invoices。例如,这是来自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`;
1
2
3
4
5
6
7

在您的页面中,导入fetchLatestInvoices函数: 修改第5行和第9行

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();
  // ...
}
1
2
3
4
5
6
7
8
9
10
11

然后,取消对<LatestInvoices/>组件的注释。您还需要取消注释<LatestInvoices/>组件本身中的相关代码,位于文件/app/ui/dashboard/latest-invoices中。 如果您访问localhost,您应该会看到数据库只返回最后5个。希望您开始看到直接查询数据库的优势! image.png

# 获取Card组件数据

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

  • 已完成支付的invoices的总量
  • 支付中的invoices的总量
  • invoices的总量
  • customers的总量

同样,你可能会被诱惑去获取所有的invoices和customers,并使用JavaScript来处理数据。例如,您可以使用Array.length来获取invoices和customers的总数:

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

但是使用SQL,你只能获取你需要的数据,它是一个比Array.length更小的数据。这意味着在请求期间需要传输的数据更少。这是SQL备选方案:

const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`;
const customerCountPromise = sql`SELECT COUNT(*) FROM customers`;
1
2

您需要导入的函数称为fetchCardData;您将需要销毁从函数返回的值。

提示:

  • 检查卡组件,查看它们需要什么数据
  • 检查data.ts文件以查看函数返回的内容。

准备好后,查看最终代码:

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>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

太棒了现在,您已经获取了仪表板概述页面的所有数据。您的页面应该如下所示: image.png 然而您需要注意两件事:

  • 数据请求无意中相互阻塞,从而创建了一个请求瀑布。
  • 默认情况下,Next.js预渲染路由以提高性能,这被称为静态渲染。因此,如果您的数据发生更改,它将不会反映在您的仪表板中。

# 什么是请求waterfall?

“瀑布”是指一系列网络请求,这些请求取决于先前请求的完成情况。在数据提取的情况下,每个请求只能在前一个请求返回数据后开始。 image.png 例如,在执行fetchLatestInvoices()运行之前,我们需要等待fetchRevenue()完成,等等。

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
1
2
3
4
5
6
7
8

这种模式并不一定是糟糕的。在某些情况下,您希望在获取下一个请求之前先满足某个条件。例如,您可能希望首先获取用户的ID和配置文件信息。一旦你有了ID,你就可以继续获取他们的朋友列表。在这种情况下,每个请求都取决于从上一个请求返回的数据。 但是,这种行为也可能是无意的,会影响性能。

# 并行获取数据

避免waterfall的一种常见方法是同时启动所有数据请求——并行获取数据。 在JavaScript中,您可以使用Promise.all()或Promise.allSettled()函数同时启动所有Promise。例如,在data.ts中,我们在fetchCardData()函数中使用Promise.all():

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,
    ]);
    // ...
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

通过使用此模式,您可以:

  • 同时开始执行所有数据提取,这可以提高性能。
  • 使用可应用于任何库或框架的本地JavaScript模式。

然而,只依赖这种JavaScript模式有一个缺点:如果一个数据请求比其他所有数据请求都慢,会发生什么?

编辑 (opens new window)
上次更新: 2025/04/19, 14:22:11
06-设置数据库
08-静态和动态渲染

← 06-设置数据库 08-静态和动态渲染→

最近更新
01
色戒2007
04-19
02
真实real
04-19
03
Home
更多文章>
Theme by Vdoing | Copyright © 2018-2025 北鸟南游
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式