Skip to content

Next 自己写接口(了解就好)

前言

路由处理程序是指使用 Web RequestResponse API 对于给定的路由自定义处理逻辑。

简单的来说,前后端分离架构中,客户端与服务端之间通过 API 接口来交互。这个“API 接口”在 Next.js 中有个更为正式的称呼,就是路由处理程序。

本篇我们会讲解如何定义一个路由处理程序以及写路由处理程序时常遇到的一些问题。

定义路由处理程序

写路由处理程序,你需要定义一个名为 route.js 的特殊文件。(注意是 route 不是 router)

该文件必须在 app 目录下,可以在 app 嵌套的文件夹下,但是要注意 page.jsroute.js 不能在同一层级同时存在。

想想也能理解,page.jsroute.js 本质上都是对路由的响应。page.js 主要负责渲染 UI,route.js 主要负责处理请求。如果同时存在,Next.js 就不知道用谁的逻辑了。

GET

让我们从写 GET 请求开始,比如写一个文章列表的接口

新建app/api/posts/route.js 代码如下

js
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/posts 查看接口返回的数据:

在这个例子中:

  • 我们export一个名为GETasync函数来定义 GET 请求处理.注意是export而不是export default

  • 我们使用next/serverNextResponse 对象用于设置响应内容,但这里不一定非要用 NextResponse,直接使用 Response 也是可以的:

ts
export async function GET() {
  const res = await fetch("https://jsonplaceholder.typicode.com/posts");
  const data = await res.json();
  return Response.json({ data });
}

但在实际开发中,推荐使用 NextResponse,因为它是 Next.js 基于 Response 的封装,它对 TypeScript 更加友好,同时提供了更为方便的用法,比如获取 Cookie 等。

  • 我们将接口写在了 app/api 文件夹下,并不是因为接口一定要放在名为 api 文件夹下(与 Pages Router 不同)。如果你代码写在 app/posts/route.js,对应的接口地址就是 /posts。放在 api 文件夹下只是为了方便区分地址是接口还是页面。

支持方法

Next.js 支持 GETPOSTPUTPATCHDELETEHEADOPTIONS 这些 HTTP 请求方法。如果传入了不支持的请求方法,Next.js 会返回 405 Method Not Allowed

ts
// route.js
export async function GET(request) {}

export async function HEAD(request) {}

export async function POST(request) {}

export async function PUT(request) {}

export async function DELETE(request) {}

export async function PATCH(request) {}

// 如果 `OPTIONS` 没有定义, Next.js 会自动实现 `OPTIONS`
export async function OPTIONS(request) {}

现在让我们再写一个 POST 请求练练手。

传入参数

现在让我们具体看下请求方法。每个请求方法的处理函数会被传入两个参数,一个 request,一个 context 。两个参数都是可选的:

ts
export async function GET(request, context) {}

request(optional)

request 对象是一个 NextRequest 对象,它是基于 Web Request API 的扩展。使用 request ,你可以快捷读取 cookies 和处理 URL

我们这里讲讲如何获取 URL 参数:

ts
export async function GET(request, context) {
  //  访问 /home, pathname 的值为 /home
  const pathname = request.nextUrl.pathname;
  // 访问 /home?id=2, searchParams 的值为 id=2
  const searchParams = request.nextUrl.searchParams;
  return NextResponse.json({
    pathname,
    params: searchParams.toString(),
    id: searchParams.get("id"),
  });
}

其中 nextUrl 是基于 Web URL API 的扩展(如果你想获取其他值,参考 URL API),同样提供了一些方便使用的方法。

context(optional)

目前 context 只有一个值就是 params,它是一个包含当前动态路由参数的对象。举个例子:

ts
// app/dashboard/[team]/route.js
export async function GET(request, { params }) {
  const team = params.team;
}

当访问 /dashboard/1 时,params 的值为 { team: '1' }。其他情况还有:

ExampleURLparams
app/dashboard/[team]/route.js/dashboard/1{ team: '1' }
app/shop/[tag]/[item]/route.js/shop/1/2{ tag: '1', item: '2' }
app/blog/[...slug]/route.js/blog/1/2{ slug: ['1', '2'] }

注意第二行:此时 params 返回了当前链接所有的动态路由参数。

示例代码

现在让我们写个 demo 复习下这些知识。

需求:目前 GET 请求 /api/posts 时会返回所有文章数据,现在希望 GET 请求 /api/posts/1?dataField=title 获取 post id 为 1 的文章数据,dataField 用于指定返回哪些字段数据。

让我们开始写吧,新建/api/posts/[id]/route.js,代码如下:

ts
import { NextResponse } from "next/server";

export async function GET(request, { params }) {
  const field = request.nextUrl.searchParams.get("dataField");
  const data = await (
    await fetch(`https://jsonplaceholder.typicode.com/posts/${params.id}`)
  ).json();
  const result = field ? { [field]: data[field] } : data;
  return NextResponse.json(result);
}

Postman 测试一下,如果请求地址是 http://localhost:3000/api/posts/1?dataField=title,效果如下:

如果请求地址是 http://localhost:3000/api/posts/1,效果如下:

缓存行为

默认是有缓存的,所以有的时候刷新不会有变化.如果想清空缓存,可以强制

ts
export const dynamic = "force-dynamic";

export async function GET() {
  return Response.json({ data: new Date().toLocaleTimeString() });
}

重新验证

除了退出缓存,也可以设置缓存的时效,适用于一些重要性低、时效性低的页面。

有两种常用的方案,一种是使用路由配置项

修改 app/api/time/route.js,代码如下:

ts
export const revalidate = 10;

export async function GET() {
  return Response.json({ data: new Date().toLocaleTimeString() });
}
  • export const revalidate = 10 表示这是重新验证频率为10S,但是要注意:

这句代码的效果并不是设置服务器每 10s 会自动更新一次 /api/time。而是最少 10s 后才重新验证。

举个例子: 假设你现在访问了 /api/time,此时时间设为 0s,10s 内持续访问,/api/time 返回的都是之前缓存的结果。当 10s 过后,假设你第 12s 又访问了一次 /api/time,此时虽然超过了 10s,但依然会返回之前缓存的结果,但同时会触发服务器更新缓存,当你第 13s 再次访问的时候,就是更新后的结果。

写接口常见问题

接下来我们讲讲写接口时常遇到的一些问题,比如如何获取网址参数,如何读取 cookie,各种方法了解即可。实际开发中遇到问题的时候再来查就行。

如何获取网址参数?

ts
// app/api/search/route.js
// 访问 /api/search?query=hello
export function GET(request) {
  const searchParams = request.nextUrl.searchParams;
  const query = searchParams.get("query"); // query
}

第一种方法是通过 NextRequest 对象:

ts
// app/api/route.js
export async function GET(request) {
  const token = request.cookies.get("token");
  request.cookies.set(`token2`, 123);
}

其中,request 是一个 NextRequest 对象。正如上节所说,NextRequest 相比 Request 提供了更为便捷的用法,这就是一个例子。

此外,虽然我们使用 set 设置了 cookie,但设置的是请求的 cookie,并没有设置响应的 cookie

第二种方法是通过 next/headers 包提供的 cookies 方法。

因为 cookies 实例只读,如果你要设置 Cookie,你需要返回一个使用 Set-Cookie headerResponse 实例。示例代码如下:

ts
// app/api/route.js
import { cookies } from "next/headers";

export async function GET(request) {
  const cookieStore = cookies();
  const token = cookieStore.get("token");

  return new Response("Hello, Next.js!", {
    status: 200,
    headers: { "Set-Cookie": `token=${token}` },
  });
}

如何处理 Headers ?

第一种方法是通过 NextRequest 对象:

ts
// app/api/route.js
export async function GET(request) {
  const headersList = new Headers(request.headers);
  const referer = headersList.get("referer");
}

第二种方法是 next/headers 包提供的 headers 方法。

因为 headers 实例只读,如果你要设置 headers,你需要返回一个使用了新 header 的 Response 实例。使用示例如下:

ts
// app/api/route.js
import { headers } from "next/headers";

export async function GET(request) {
  const headersList = headers();
  const referer = headersList.get("referer");

  return new Response("Hello, Next.js!", {
    status: 200,
    headers: { referer: referer },
  });
}

如何重定向?

重定向使用 next/navigation 提供的 redirect 方法,示例如下:

ts
import { redirect } from "next/navigation";

export async function GET(request) {
  redirect("https://nextjs.org/");
}

如何获取请求体内容?

ts
// app/items/route.js
import { NextResponse } from "next/server";

export async function POST(request) {
  const res = await request.json();
  return NextResponse.json({ res });
}

如果请求正文是 FormData 类型:

ts
// app/items/route.js
import { NextResponse } from "next/server";

export async function POST(request) {
  const formData = await request.formData();
  const name = formData.get("name");
  const email = formData.get("email");
  return NextResponse.json({ name, email });
}

如何设置 CORS ?

ts
// app/api/route.ts
export async function GET(request) {
  return new Response("Hello, Next.js!", {
    status: 200,
    headers: {
      "Access-Control-Allow-Origin": "*",
      "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
      "Access-Control-Allow-Headers": "Content-Type, Authorization",
    },
  });
}

如何响应无 UI 内容?

你可以返回无 UI 的内容。在这个例子中,访问 /rss.xml 的时候,会返回 XML 结构的内容:

ts
// app/rss.xml/route.ts
export async function GET() {
  return new Response(`<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0">
 
<channel>
  <title>Next.js Documentation</title>
  <link>https://nextjs.org/docs</link>
  <description>The React Framework for the Web</description>
</channel>
 
</rss>`);
}

Streaming

openai 的打字效果背后用的就是流:

ts
// app/api/chat/route.js
import OpenAI from "openai";
import { OpenAIStream, StreamingTextResponse } from "ai";

const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

export const runtime = "edge";

export async function POST(req) {
  const { messages } = await req.json();
  const response = await openai.chat.completions.create({
    model: "gpt-3.5-turbo",
    stream: true,
    messages,
  });

  const stream = OpenAIStream(response);

  return new StreamingTextResponse(stream);
}

当然也可以直接使用底层的 Web API 实现 Streaming:

ts
// app/api/route.js
// https://developer.mozilla.org/docs/Web/API/ReadableStream#convert_async_iterator_to_stream
function iteratorToStream(iterator) {
  return new ReadableStream({
    async pull(controller) {
      const { value, done } = await iterator.next();

      if (done) {
        controller.close();
      } else {
        controller.enqueue(value);
      }
    },
  });
}

function sleep(time) {
  return new Promise((resolve) => {
    setTimeout(resolve, time);
  });
}

const encoder = new TextEncoder();

async function* makeIterator() {
  yield encoder.encode("<p>One</p>");
  await sleep(200);
  yield encoder.encode("<p>Two</p>");
  await sleep(200);
  yield encoder.encode("<p>Three</p>");
}

export async function GET() {
  const iterator = makeIterator();
  const stream = iteratorToStream(iterator);

  return new Response(stream);
}