Skip to content

网络请求 ​

一、强缓存 ​

对于基本不更新的网络资源走强缓存策略

在服务端组件中,使用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信息,有两种解决办法:

  1. 在服务端组件中获取数据,然后通过props传递给客户端组件

  2. 通过API路由进行接口转发

七、在动态路由中使用 ​

  1. 动态路由如果不使用generateStaticParams提前指定路由参数,接口设定的缓存策略依旧会生效,但页面的缓存策略为private, no-cache, no-store, max-age=0, must-revalidate

  2. 动态路由中的接口只要有缓存策略为不缓存,使用generateStaticParams生成的动态路由参数,打包后不会生成对应的html文件

  3. 接口数据更新后,使用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>;
}