nextjs中对获取数据的理解

0 阅读7分钟

1.JSON 是什么:

JSON(JavaScript Object Notation)本质是一种轻量级的数据交换格式,核心作用是解决「不同系统 / 语言之间的数据传递」问题 —— 比如前端和后端(Python/Java/Go)、手机 App 和服务器、甚至不同编程语言的程序之间,都能用 JSON 传递数据。

image.png

2.换取数据的方法

客户端 = 前端(浏览器);服务端 = 后端(服务器)

1.服务端数据库直接查询:

服务器组件就是async函数,但是客户端组件不是所以不能用async和await image.png 这里非常明显可以看出来:在服务器组件中可以直接连接数据库,然后利用query等函数来查询和获取数据,把数据传递给users,最后返回。此过程中干净利索没有弯弯绕绕。

2.客户端api获取数据:

要先写一个api路由,这里要注意的是这是在服务端并且内容必须写在route.ts文件中

问:为什么必须写在route.ts文件中?

答:到时候const res = await fetch('/api/users')会调用这个api路由,也就是客户端组件调用 fetch('/api/users'),Next.js 会自动找到 app/api/users/route.ts 文件,执行里面的 GET/POST 函数,返回数据。具体的如图:

image.png

image.png 在这段代码中

NextResponse 是 Next.js 提供的工具类(需要导入),专门用来处理 API 路由的响应

.json(users) 是把 users(数据库查出来的用户数组)转换成「JSON 格式的响应」;


这里是想获取数据的地方: image.png 解释代码:

1.因为想修改 users 状态,所以要用得到hook函数usestate
2.因为这里存在state因为是客户端组件,要改变值就是改变状态(setUser),那么一旦状态改变就会重新渲染,重新渲染又会重新执行代码,这时如不阻拦那代码就会陷入死循环,这时useEffect就发挥了拦截的作用setUser 虽然触发了重绘,但重绘时 useEffect 拒绝再次执行,死循环被成功拦截。这里主要是[]依赖数组起作用了。这时组件的渲染顺序就正常了:

执行顺序

  1. 组件初始化,users = []
  2. 组件同步渲染(页面先展示空列表)
  3. 渲染完成后,useEffect 执行一次(依赖项 []
  4. 发起请求 → 拿到数据 → setUsers
  5. 组件重渲染,展示用户列表
  6. 后续不再重复执行,无循环

依赖数组这里执行副作用函数

在 React 组件中:

  • 主职工作:根据 propsstate 计算出 HTML(JSX)并显示出来。
  • 副作用:除了画图之外的所有事情:手动修改 DOM 设置定时器 订阅信息 记录日志 加载数据这种它触及了外部世界且不可预测且需要时间的行为

React 组件会因为各种原因频繁重新运行。如果你直接在函数体里写加载数据的代码,每画一次网页就发一次网络请求,服务器会被你搞崩溃。

useEffect 的作用就是把这些“危险”或“耗时”的操作关进一个小房间: React 说:“我先负责把网页框架画出来,等用户看到东西后,我再让 useEffect 里的副作用函数去后台悄悄取数据。”

3.这里是客户端组件为什么能用异步函数,因为组件函数确实不能用但是可以在里面定义一个子函数使用也就是这里的fetchUsers(),但是async返回的是promise对象,useEffect函数只接受无返回值和清除的类型所以要用箭头函数包裹,箭头函数包裹后放在里面才合法
4.在下面的fetch调用api路由时也就对应了上面解释内容必须写在route.ts文件中,这里会执行路由里面的Get函数进行查询,之后通过NextResponse.json(users)把users内容转化成json形式返回到客户端的res,res通过json方法把JSON 字符串 → JS 对象 / 数组(拆包拿来用)。

为什么这两个json方法名字一样但是所处的地方不同作用也不同?

image.png

3.客户端Server Actions

image.png

4.客户端Props 传递 (Server \rightarrow Client)

// app/blog/page.tsx (Server Component)
import { fetchArticleData } from '@/app/lib/data'; // 假设这是你的 SQL 函数
import LikeButton from '@/app/ui/blog/like-button';
export default async function Page() {
// 1. 在服务器上直接查数据库,拿到数据
const article = await fetchArticleData('123');
return (
<main>
<h1>{article.title}</h1>
<p>{article.content}</p>
{/* 2. 把拿到的数据(likes)作为 props 传给客户端组件 */}
<LikeButton initialLikes={article.likes} />
</main>
);
}
// app/ui/blog/like-button.tsx (Client Component)
'use client';
import { useState } from 'react';
export default function LikeButton({ initialLikes }: { initialLikes: number }) {
// 3. 直接使用父组件传下来的 props 作为初始状态
const [likes, setLikes] = useState(initialLikes);
return (
<button onClick={() => setLikes(likes + 1)}>
👍 点赞数:{likes}
</button>
);
}

5.总结: 为什么会有这四种?(技术的演进逻辑)

在上面的代码用了不同的写法表达了一种方式搜数据后展示仅此而已,所以客户端的几种方式看起来是别扭的,因为是从服务端拿数据,就排除. Server Actions,是只展示没有交互排除 Props 传递,没有第三方所以排除API 路由

技术不是为了复杂而复杂,而是为了解决不同的痛点

  1. 直接 SQL 查询 (Server Components)

    • 痛点:以前加载网页得等接口返回 JSON,再渲染页面(白屏久)。
    • 方案:直接在服务器把数查好,吐出完整的 HTML。
    • 地位现代全栈的“根基”
  2. Props 传递 (Server \rightarrow Client)

    • 痛点:数据在服务器,但用户点击效果在浏览器。
    • 方案:把查好的数像接力棒一样传给客户端组件。
    • 地位前后端协作的“粘合剂”
  3. Server Actions

    • 痛点:以前点个“删除”按钮得写 API、写 fetch、写状态码,烦死了。
    • 方案:像调普通函数一样改数据库。
    • 地位表单交互的“终结者”
  4. API 路由

    • 痛点:如果我的数据不仅网页要用,安卓 App 也要用怎么办?
    • 方案:保留一个标准的 HTTP 窗口。
    • 地位对外开放的“大门”

    对比Server Actions和Props 传递: image.png 一图流展示不同: image.png

3.使用sql

1. postgres.js 是什么?

postgres.js 是一个专门用于 Node.js/Next.js 连接 PostgreSQL 数据库的轻量级库(可以理解为:它是你的代码和 PostgreSQL 数据库之间的「翻译官」)。

  • 它的核心作用:帮你执行 SQL 语句,把代码里的指令转换成数据库能懂的 SQL,再把数据库返回的结果转换成代码能直接用的对象(比如数组、对象)。
  • 优势(文中提到的):自带 SQL 注入防护(不用自己写复杂的防注入逻辑,库已经帮你做好了),且直接用原生 SQL,比 ORM 更直观。

2.拆解代码

// /app/lib/data.ts
import postgres from 'postgres'; 
const sql = postgres(process.env.POSTGRES_URL!, { ssl: 'require' });

1.import postgres from 'postgres':就是从 postgres.js 库中导入核心函数,这个函数的作用是创建数据库连接

2.调用 postgres 函数,传入「数据库连接地址」和「配置项」,返回一个 sql 函数(sql 函数只能在服务器端调用所以客户端组件不能直接调用 data.ts 里的 getUsers() 等函数),这个 sql 函数就是你后续执行 SQL 语句的「入口」

process.env.POSTGRES_URL!

  • process.env 是 Node.js/Next.js 里获取「环境变量」的方式(环境变量是存放敏感信息的地方,比如数据库地址、密码);
  • POSTGRES_URL 是你配置的 PostgreSQL 数据库连接地址(格式类似:postgres://用户名:密码@数据库地址:端口/数据库名);

{ ssl: 'require' }

  • 配置 SSL 连接(大部分云数据库(比如 Vercel Postgres、Supabase)都要求强制 SSL 连接,保证数据传输加密);
  • 简单说:不加这个配置,可能连不上云数据库。

3.这里的data.ts就是专门存放执行增删改查的函数的地方:

image.png

这里面每一个函数执行获取数据时都离不开定义的sql。const sql = SELECT * FROM users WHERE id = ${userId};这里是手动拼接字符串,若userId = "1 OR 1=1"那么就是true那就会暴露数据,但是加入sql函数的 使用就会参数化查询 ,即把 SQL 语句和参数分开传,数据库先解析语句,再代入参数,杜绝篡改。

image.png

若服务端想用直接查数据就可以用命名导入的形式,之后可以直接const users = await getUsers();去调用函数

4.瀑布式

请求瀑布图(Request Waterfall) :指多个网络请求按顺序排队执行后一个请求必须等前一个完全结束才能开始,像瀑布一样从上到下依次流淌,不能并行。

你给的代码就是典型的请求瀑布

// 三个请求排队执行,一个等一个
const revenue = await fetchRevenue();
const latestInvoices = await fetchLatestInvoices(); 
const cardData = await fetchCardData(); 

这三个请求之间,没有任何数据依赖!

  • fetchRevenue() 不需要 latestInvoices 的数据
  • fetchLatestInvoices() 不需要 cardData 的数据
  • 它们完全可以同时执行

但你用了 连续 await,强制让它们排队,这就是无意的、伤害性能的请求瀑布

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,
    ]);

那这里算不算并行?上面是查三个函数,这个是一个函数中的三个不同的查找请求

答:算。如果没有all promise都要一个一个加载,就算是一个函数中的三个不同的查找请求也要一个一个加载

image.png

无意瀑布会带来什么性能问题1.总加载时间成倍增加2.页面渲染延迟3.资源利用率低

什么时候瀑布是必要的只有一种情况:后一个请求需要前一个请求返回的数据

image.png

如何修复你代码里的 “无意瀑布”?(最优方案)

使用 Promise.all() 让请求并行执行

tsx

// 同时发送所有请求
const [revenue, latestInvoices, cardData] = await Promise.all([
  fetchRevenue(),
  fetchLatestInvoices(),
  fetchCardData()
]);

✅ 时间从 600ms → 200ms✅ 不改变逻辑✅ 不产生任何副作用 为什么要用 Promise.all()?它到底做了什么?

Promise.all() 就是让多个异步请求【同时一起发】,而不是【排队发】。

Promise.all() 做了什么?

它把所有请求一次性全部发出去,让它们同时在后台跑

const [a, b, c] = await Promise.all([
  fetch1(),
  fetch2(),
  fetch3()
]);

执行流程:

  1. 立刻同时发送 3 个请求
  2. 等待 最慢的那个请求完成
  3. 一次性返回所有结果

总时间 ≈ 200ms(只等最慢的一个)

image.png

额外补充:泛型的再理解

以前认为

泛型 = 给 “类型” 留个占位符,用的时候再填具体类型
  • 不写死类型 → 代码复用
  • 又不会变成 any → 类型安全
  • 调用时传什么类型,里面就自动变成什么类型
type就是传参的时候类型声明用的或者作为全局的参数

这两个没什么联系

“类型 和 泛型 为什么能放一起?”

答案:泛型的作用,就是接收 “类型” 作为参数!

具体写法:

//app\lib\definitions.ts
export type InvoicesTable = {
  id: string;
  customer_id: string;
  name: string;
  email: string;
  image_url: string;
  date: string;
  amount: number;
  status: 'pending' | 'paid';
}; 


//app\lib\data.ts
import {
InvoicesTable,
} from './definitions';
export async function fetchFilteredInvoices(
  query: string,
  currentPage: number,
) {
  const offset = (currentPage - 1) * ITEMS_PER_PAGE;

  try {
    const invoices = await sql<InvoicesTable[]>`
      SELECT
        invoices.id,
        invoices.amount,
        invoices.date,
        invoices.status,
        customers.name,
        customers.email,
        customers.image_url
      FROM invoices
      JOIN customers ON invoices.customer_id = customers.id
      WHERE
        customers.name ILIKE ${`%${query}%`} OR
        customers.email ILIKE ${`%${query}%`} OR
        invoices.amount::text ILIKE ${`%${query}%`} OR
        invoices.date::text ILIKE ${`%${query}%`} OR
        invoices.status ILIKE ${`%${query}%`}
      ORDER BY invoices.date DESC
      LIMIT ${ITEMS_PER_PAGE} OFFSET ${offset}
    `;