主题
网络请求
一、强缓存
对于基本不更新的网络资源走强缓存策略
在服务端组件中,使用fetch获取数据默认走强缓存策略。即只有当缓存中没有该资源或者手动清除时,才会发起网络请求。
tsx
async function getProjects() {
const res = await fetch(`https://...`);
// 相当于 const res = fetch(`https://...`, { cache: 'force-cache' })
const projects = await res.json();
return projects;
}
export default async function Index() {
const projects = await getProjects();
return projects.map((project) => <div>{project.name}</div>);
}提示:在开发环境中,就算指定强缓存,接口还是会每次去重新请求。在生产环境中,缓存策略会生效,直接返回缓存的值
二、强缓存有效时间
虽说在强缓存中,只有当缓存中没有该资源或者手动清除时,才会发起网络请求。但强缓存也是有缓存时间的,默认为365天。
对于不经常更新的网络资源可以手动设置缓存有效时间。在fetch请求中使用revalidate指定缓存时间,单位为秒
ts
//缓存有效期3600s
const revalidatedData = await fetch(`https://...`, {
next: { revalidate: 60 * 60 },
});如果想在缓存时间内进行更新,可修改请求头,例如在请求头中添加版本信息
ts
async function getData() {
const data = await fetch("http://127.0.0.1:9000/api/list", {
next: { revalidate: 36000 },
headers: {
version: "20240406",
},
}).then((res) => res.json());
return data;
}因为 Next.js 在处理请求时,会根据请求头等信息生成唯一的缓存 key,用于缓存响应数据。如果修改请求头中自定义属性的值,会使 Next.js 重新生成缓存信息
三、缓存存储地址与解码
缓存的数据都在.next/catch/fetch-cache下
body上的数据就是接口的数据,可用base64解码
四、不缓存
对于经常更新的网络资源,不走缓存策略
在fetch中使用{ cache: 'no-store' },指定每次请求都获取最新数据,不读取缓存数据
ts
const dynamicData = await fetch(`https://...`, { cache: "no-store" });五、不缓存数据共享
对于缓存的请求,即使在不同子组件内多次调用,接口也只会被调用一次,不会影响性能。
对于不缓存的请求,每次发起都会实时返回最新的数据。如果在同一页面内,若两个子组件均需依赖相同的接口数据,一种处理方式是进行状态提升。
在它们共同的父级组件中发起数据请求,并通过props将数据传递至子组件。不过,这种方式可能会有两个问题:当组件层级较为复杂时,通过层层传递props会比较繁琐;若父级组件中包含静态数据,则这些数据会因为需要等待接口数据返回而延迟显示,无法立即呈现给用户。
为了解决这些问题,我们可以利用React的cache API来实现数据共享
1. 新建user/utils.ts
使用cache对接口数据进行缓存
ts
import { cache } from "react";
//模拟网络请求
async function getNews(): Promise<{ name: number; id: number }[]> {
return new Promise((resolve) => {
const num = Math.random();
setTimeout(() => {
resolve([
{
name: num,
id: 1,
},
{
name: 1233,
id: 2,
},
]);
}, 1000);
});
}
export const getItem = cache(async () => {
const data = await getNews();
return data;
});2. 新建user/layout.tsx
在布局组件中请求接口数据
tsx
import { getItem } from "./utils";
export default async function Layout({
children,
}: {
children: React.ReactNode;
}) {
const data = await getItem();
return (
<section>
{data.map((o) => (
<div key={o.id}>{o.name}</div>
))}
------{children}
</section>
);
}3. 新建user/page.tsx
在页面组件中请求接口数据
tsx
import { getItem } from "./utils";
export default async function Home() {
const data = await getItem();
return (
<article>
{data.map((o) => (
<div key={o.id}>{o.name}</div>
))}
</article>
);
}访问路由“/user”,页面显示为相同的内容,说明接口只被调用了一次。刷新页面,页面显示的内容同步刷新
六、客户端组件中获取数据
在服务端组件中,使用fetch获取接口数据。在客户端组件中,可借助第三方库获取接口数据,例如SWR或者ahooks的useRequest。
在客户端组件中获取数据,接口的请求信息将会展示在客户端,如果不想暴露请求头中的token信息,有两种解决办法:
在服务端组件中获取数据,然后通过props传递给客户端组件
通过API路由进行接口转发
七、在动态路由中使用
动态路由如果不使用generateStaticParams提前指定路由参数,接口设定的缓存策略依旧会生效,但页面的缓存策略为private, no-cache, no-store, max-age=0, must-revalidate
动态路由中的接口只要有缓存策略为不缓存,使用generateStaticParams生成的动态路由参数,打包后不会生成对应的html文件
接口数据更新后,使用generateStaticParams打包后的动态路由的html页面内容也会同步更新
八、封装fetch请求
1. 新建src/utils/request.ts
ts
import queryString from "query-string";
type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
interface Params {
time?: number; //缓存时间,单位为s。默认强缓存,0为不缓存
params?: Record<string, any>;
}
interface Props extends Params {
url: string;
method: Method;
}
type Config =
| { next: { revalidate: number } }
| { cache: "no-store" }
| { cache: "force-cache" };
class Request {
/**
* 请求拦截器
*/
interceptorsRequest({ url, method, params, time }: Props) {
let queryParams = ""; //url参数
let requestPayload = ""; //请求体数据
//请求头
const headers = {
authorization: `Bearer ...`,
};
const config: Config =
time || time === 0
? time > 0
? { next: { revalidate: time } }
: { cache: "no-store" }
: { cache: "force-cache" };
if (method === "GET" || method === "DELETE") {
//fetch对GET请求等,不支持将参数传在body上,只能拼接url
if (params) {
queryParams = queryString.stringify(params);
url = `${url}?${queryParams}`;
}
} else {
//非form-data传输JSON数据格式
if (
!["[object FormData]", "[object URLSearchParams]"].includes(
Object.prototype.toString.call(params),
)
) {
Object.assign(headers, { "Content-Type": "application/json" });
requestPayload = JSON.stringify(params);
}
}
return {
url,
options: {
method,
headers,
body:
method !== "GET" && method !== "DELETE" ? requestPayload : undefined,
...config,
},
};
}
/**
* 响应拦截器
*/
interceptorsResponse<T>(res: Response): Promise<T> {
return new Promise((resolve, reject) => {
const requestUrl = res.url;
if (res.ok) {
return resolve(res.json() as Promise<T>);
} else {
res
.clone()
.text()
.then((text) => {
try {
const errorData = JSON.parse(text);
return reject({
message: errorData || "接口错误",
url: requestUrl,
});
} catch (text) {
return reject({ message: text, url: requestUrl });
}
});
}
});
}
async httpFactory<T>({ url = "", params = {}, method }: Props): Promise<T> {
const req = this.interceptorsRequest({
url: process.env.NEXT_PUBLIC_BASEURL + url,
method,
params: params.params,
time: params.time,
});
const res = await fetch(req.url, req.options);
return this.interceptorsResponse<T>(res);
}
async request<T>(method: Method, url: string, params?: Params): Promise<T> {
return this.httpFactory<T>({ url, params, method });
}
get<T>(url: string, params?: Params): Promise<T> {
return this.request("GET", url, params);
}
post<T>(url: string, params?: Params): Promise<T> {
return this.request("POST", url, params);
}
put<T>(url: string, params?: Params): Promise<T> {
return this.request("PUT", url, params);
}
delete<T>(url: string, params?: Params): Promise<T> {
return this.request("DELETE", url, params);
}
patch<T>(url: string, params?: Params): Promise<T> {
return this.request("PATCH", url, params);
}
}
const request = new Request();
export default request;2. 定义接口返回类型
新建src/apis/model.ts
ts
export interface New {
url: string;
thumbnail_pic_s: string;
title: string;
}3. 定义接口
新建src/apis/api.ts
ts
import request from "@/utils/request";
import type { New } from "./model";
export const getNews = (params: { type: string; key?: string }) => {
return request.get<{ result: { data: New[] } }>("", {
params: { key: process.env.NEXT_PUBLIC_KEY, ...params },
time: 60 * 60 * 24,
});
};4. 使用接口
在getData中使用try...catch处理接口错误
tsx
import { getNews } from "@/apis/api";
async function getData() {
try {
return await getNews();
} catch (error) {
console.error("11", error);
}
}
export default async function Home() {
const data = await getData();
console.log("data", data);
return <h1 className="text-3xl font-bold underline">Hello world!</h1>;
}