NextJs 数据篇 - 数据获取 | 缓存 | Server Actions
- 前言
- 一. 数据获取 fetch
- 1.1 缓存 caching
- ① 服务端组件使用fetch
- ② 路由处理器 GET 请求使用fetch
- 1.2 重新验证 revalidating
- ① 基于时间的重新验证
- ② 按需重新验证
- revalidatePath
- revalidateTag
- 1.3 缓存的退出方式
- 二. Server Actions
- 2.1 PageRouter下 API 简单案例
- 2.2 AppRouter 下 Server Actions 简单案例
前言
之前讲了:
- NextJs 初级篇 - 安装 | 路由 | 中间件
- NextJs 渲染篇 - 什么是CSR、SSR、SSG、ISR 和服务端/客户端组件
这篇文章就打算专门讲一下NextJs
中的数据获取方式、缓存以及什么是Server Actions
一. 数据获取 fetch
NextJs
中,在服务端使用 fetch
函数是常规的数据请求方式,它扩展了原生的API
,在此基础上有这么几个功能(针对服务端的请求):
caching
:缓存revalidating
:重新验证
例如这么一个简单的服务端组件:
export default async function Page() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts')
const data = await res.json()
return (
<ul>
{data.map((item: any) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
)
}
1.1 缓存 caching
默认情况下,NextJs
会自动缓存服务端中 fetch
请求的返回值。但是在以下情况下不会自动缓存:
- 在
Server Action
中使用的时候。 - 在定义了非
GET
方法的路由处理程序中使用。
即在服务端组件内部或者只有 GET
方法的路由处理程序中使用 fetch
函数,返回结果会自动缓存。
接下来我们来测试一下缓存,为了更加直观的让大家看到缓存的生效,我们可以在next.config.mjs
文件中增加以下配置,这样在fetch
的时候,就会打印相关的信息,包括缓存是否命中
/** @type {import('next').NextConfig} */
const nextConfig = {
logging: {
fetches: {
fullUrl: true
}
}
};
export default nextConfig;
① 服务端组件使用fetch
例如我有这么一个服务端组件:
export default async function Page() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts')
const data = await res.json()
return (
<ul>
{data.map((item: any) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
)
}
页面多次访问后:可以发现缓存命中
② 路由处理器 GET 请求使用fetch
// app/api/getData/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts')
const data = await res.json()
return NextResponse.json(data)
}
然后多次访问 http://localhost:3000/api/getData
,结果如下:
1.2 重新验证 revalidating
重新验证的定义:清除缓存数据,然后重新获取最新的数据。 而NextJs 中提供了两种方式完成重新验证:
- 基于时间的重新验证:根据指定的时间,自动进行重新验证。
- 按需重新验证:根据事件手动重新验证数据。
① 基于时间的重新验证
这种验证方式,只需要在fetch
的时候,增加参数revalidate
即可,如下代表这个请求的缓存时长为10秒钟
fetch('https://...', { next: { revalidate: 10 } })
或者在路由段配置项中进行配置,可以再页面上或者路由处理程序中增加以下属性的导出:
// layout.jsx | page.jsx | route.js
export const revalidate = 10
② 按需重新验证
我们可以在路由处理程序中或者 Server Actions
中通过两种方式来实现按需重新验证:
- 路径
revalidatePath
- 缓存标签
revalidateTag
revalidatePath
我们写一个简单的页面,这个页面每次加载的时候都会随机加载一个图片,但是由于fetch被缓存了,加载多次还是同一个图片。
async function getData() {
// 接口每次调用都会返回一个随机的猫猫图片数据
const res = await fetch('https://api.thecatapi.com/v1/images/search')
if (!res.ok) {
throw new Error('Failed to fetch data')
}
return res.json()
}
export default async function Page() {
const data = await getData()
return <img src={data[0].url} width="300" />
}
效果如下:
那我如何按需让这个fetch
请求做到刷新呢?例如我创建这么一个路由处理程序:
// app/api/revalidatePathTest/route.ts
import { revalidatePath } from 'next/cache'
import { NextRequest } from 'next/server'
export async function GET(request: NextRequest) {
const path = request.nextUrl.searchParams.get('path')
if (path) {
revalidatePath(path)
return Response.json({ revalidated: true, now: Date.now() })
}
return Response.json({
revalidated: false,
now: Date.now(),
message: 'Missing path to revalidate',
})
}
这段代码啥意思呢?
- 如果我请求的地址没有参数
refreshPath
,那就是个普通的接口。 - 如果我请求的地址带上参数
refreshPath
,就会通过revalidatePath
函数,重新验证对应路由或者接口。
倘若我访问:http://localhost:3000/api/revalidatePathTest?refreshPath=/
,之后再访问:http://localhost:3000/
,可见图片发生了刷新:
说明成功让路由 /
进行了重新验证(缓存刷新)
revalidateTag
除此之外,NextJs
中有一个路由标签系统,即revalidateTag
,它的实现逻辑如下:
使用 fetch
的时候,设置一个或者多个标签标记请求
调用 revalidateTag
方法重新验证该标签对应的所有请求
例如我们修改app/page.tsx
文件:
async function getData() {
// 接口每次调用都会返回一个随机的猫猫图片数据
const res = await fetch('https://api.thecatapi.com/v1/images/search', { next: { tags: ['refresh'] } })
if (!res.ok) {
throw new Error('Failed to fetch data')
}
return res.json()
}
export default async function Page() {
const data = await getData()
return <img src={data[0].url} width="300" />
}
修改revalidatePathTest
:
// app/api/revalidatePathTest/route.ts
import { revalidatePath } from 'next/cache'
import { NextRequest } from 'next/server'
export async function GET(request: NextRequest) {
const tag = request.nextUrl.searchParams.get('tag')
revalidateTag(tag)
return Response.json({ revalidated: true, now: Date.now() })
}
操作如下:
我们可以发现:
- 当我们多次刷新
http://localhost:3000/
,图片并没有改变,因为缓存的作用。 - 当我们访问:
http://localhost:3000/api/revalidatePathTest?tag=refresh
,我们带上了指定的tag
。值为refresh
。 - 此时再次访问首页,发现图片修改。
- 而我们的图片在
fetch
的时候,await fetch('https://api.thecatapi.com/v1/images/search', { next: { tags: ['refresh'] } })
,指定了tag
为refresh
- 因此可以联动做到重新验证。
1.3 缓存的退出方式
上面说的都是重新验证,说白了就是缓存的一种刷新机制,那么我们是否有办法主动退出缓存呢?
当使用 fetch
的时候,若满足以下情形,可以做到默认退出缓存机制:
fetch
请求添加了cache: 'no-store'
或者revalidate: 0
属性。例如
fetch('', { cache: 'no-store' })
fetch
请求在路由处理程序中并使用了其他方法,例如POST
。- 函数体内使用到了
headers
或cookies
等方法。
export async function GET(request) {
const token = request.cookies.get('token')
return Response.json({ data: new Date().toLocaleTimeString() })
}
- 配置了路由段选项
const dynamic = 'force-dynamic'
export const dynamic = 'force-dynamic'
- 配置了路由段选项
fetchCache
,默认会跳过缓存
二. Server Actions
Server Actions
是指在服务端执行的异步函数,但是可以在服务端和客户端组件中使用。 我们什么情况下可以用到这个呢?
- 在
PageRouter
情况下,若需要前后端进行交互,则需要先定义一个接口。 - 在
AppRouter
情况下,这种操作则可以简化为Server Actions
。我们可以定义一个Server Actions
,然后直接在客户端使用获取数据。
它的基本定义方式就是通过 'use server'
声明,一般分为两种:
- 模块级别:在文件的顶部声明,那么该文件下声明的所有导出函数都是
Server Actions
。 - 函数级别:在函数内部的顶端添加声明,那么只有该函数是
Server Actions
。
例如:
// 函数级别
async function getData() {
'use server'
// ...
}
// 模块级别 app/serverActions/action.ts
'use server'
export async function getData() {
// ...
}
export async function create() {
// ...
}
2.1 PageRouter下 API 简单案例
我们定义一个接口:
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const res = await fetch('https://jsonplaceholder.typicode.com/posts')
const data = await res.json()
return NextResponse.json(data)
}
然后定义一个页面,通过API
的方式从后端取数据。
'use client'
import { useEffect, useState } from "react"
export default function Page() {
const [data, setData] = useState<any>(null)
async function getList() {
const data = await (await fetch('/api/getData')).json();
setData(data);
}
useEffect(() => {
getList();
}, [])
return (
<ul>
{data?.map((item: any) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
)
}
2.2 AppRouter 下 Server Actions 简单案例
我们来看下使用Server Actions
的简单案例,一般我们定义一个actions
文件夹:
里面的函数如下:
'use server'
export async function getData() {
const res = await fetch('https://jsonplaceholder.typicode.com/posts')
const data = await res.json()
return data;
}
然后在组件中使用:服务端组件和客户端组件都可以使用
import { getData } from '../actions/actions'
export default async function Page() {
const data = await getData();
return (
<ul>
{data.map((item: any) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
)
}
可以看到Server Actions
代码更加简洁,无需手动创建一个接口。并且这个函数可以在代码的任何一个地方使用。