Next.js 路由处理程序和中间件

前言

Next.js 本质上是一个全栈的 React 框架,所以想要学习它。必需要有 Node.js 等后端编程能力。才能理解接下来所写的路由处理程序和中间件。

路由处理程序

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

前后端分离架构中,客户端与服务端之间通过 API 接口来交互。Next.js 称呼这种 API 交互方式为路由处理程序。

定义路由处理程序

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

image.png

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

GET 请求

新建 app/api/posts/route.js 文件,代码如下:

1
2
3
4
5
6
7
8
9
10
import { NextResponse } from "next/server";
import axios from "axios";

export async function GET() {
const res = await axios.get("https://jsonplaceholder.typicode.com/posts");
const data = await res.data;

return NextResponse.json(data);
}

浏览器访问 http://localhost:3000/api/posts 查看接口返回的数据:

axios 返回的则是数组包对象的形式

image.png

支持方法

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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 请求试试

1
2
3
4
5
6
7
8
9
10
11
export async function POST(request) {
const article = await request.json();

return NextResponse.json(
{
id: Math.random().toString(36).slice(-8),
data: article,
},
{ status: 201 }
);
}

image-20240903162016865

传参

每个请求方法都会被传入两个参数,一个 request,一个 context 。两个参数都是可选的

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

request (optional)

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

1
2
3
4
5
6
export async function GET(request, context) {
// 访问 /home, pathname 的值为 /home
const pathname = request.nextUrl.pathname
// 访问 /home?name=lee, searchParams 的值为 { 'name': 'lee' }
const searchParams = request.nextUrl.searchParams
}

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

context (optional)

在 Next.js 中 context 只有一个值就是 params,它是一个包含当前动态路由参数的对象。

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

Example URL params
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'] }

写个 Demo 练习吧

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
import axios from "axios";
import { NextResponse } from "next/server";

export const GET = async (request, { params }) => {
const field = request.nextUrl.searchParams.get("dataField");
const { data } = await axios.get(
"https://jsonplaceholder.typicode.com/posts/" + params.id
);

const result = field ? data[field] : data;

return NextResponse.json(result);
};

既然是 GET 请求我们可以使用浏览器或 Postman 测试。

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

image-20240903164337993

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

image-20240903164357951

缓存行为

默认缓存

默认情况下,使用 Response 对象(NextResponse 也是一样的)的 GET 请求会被缓存。

让我们举个例子,新建 app/api/time/route.js,代码如下:

1
2
3
4
export async function GET() {
console.log('GET /api/time')
return Response.json({ data: new Date().toLocaleTimeString() })
}

注意:在开发模式下,并不会被缓存,每次刷新时间都会改变:

get-cache.gif

现在我们部署生产版本,运行 npm run build && npm run start

get-cache-1.gif

你会发现,无论怎么刷新,时间都不会改变。这就是被缓存了。

退出缓存

但大家也不用担心默认缓存带来的影响。实际上,默认缓存的条件是非常“严苛”的,这些情况都会导致退出缓存。

有很多方式会导致它退出默认缓存

  • GET 请求使用 Request 对象
  • 添加其他 HTTP 方法,比如 POST,因为 POST 请求往往用于改变数据
  • 使用像 cookies、headers 这样的动态函数

也可以手动配置动态渲染模式

1
export const dynamic = 'force-dynamic'

写接口常见问题

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

如何获取网址参数?
1
2
3
4
5
6
// app/api/search/route.js
// 访问 /api/search?query=hello
export function GET(request) {
const searchParams = request.nextUrl.searchParams
const query = searchParams.get('query') // query
}
如何处理 Cookie?
1
2
3
4
5
// app/api/route.js
export async function GET(request) {
const token = request.cookies.get('token')
request.cookies.set(`token2`, 123)
}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
// 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}` },
})
}

… 用到上网搜就好了,写了也记不住,不常用就是会忘或参考原文。

路由处理程序小结

本结说了:route.js 不能跟同级的 page.js 一起使用。在开发的时候,尽可能使用 NextRequest 和 NextResponse,它们是基于原生 Request 和 Response 的封装,提供了快捷处理 url 和 cookie 的方法。

中间件

中间件(Middleware),使用中间件可以拦截我们所有的请求,如在请求前判断用户有没有携带 token 之类的。

定义

写中间件,你需要在项目的根目录定义一个名为 middleware.js的文件:

1
2
3
4
5
6
7
8
9
10
11
12
// middleware.js
import { NextResponse } from 'next/server'

// 中间件可以是 async 函数,如果使用了 await
export function middleware(request) {
return NextResponse.redirect(new URL('/home', request.url))
}

// 设置匹配路径
export const config = {
matcher: '/about/:path*',
}

项目根目录指 app 同级的目录。这里就不写'/about/:path*'这是什么意思了。本来正则就没有学会,看到这种东西就一头雾水。为什么原文作者会

作者 VS 我

学历:原文作者毕业于武汉大学(985、211、双一流) 而我 不知名院校

经验:原文作者就职于淘宝(是阿里巴巴最核心的业务了,能就职于阿里巴巴就算不错了,在的部门也是最核心的。) 而我 没有经验还是学生。

2比0完败

这里聊个题外话,就是绝大大数人都是普通人,和这些人不是一个等级的。例如:李氏朝鲜的世宗大王设置韩文的事。

当时李朝的绝大多数文臣都包括宰相什么的,都不同意设计朝文。因为能当上这些职务的大臣一般都是知识分子,它们很聪明,所以它们能学会汉字。所以它们对汉族是有认同感的,且它们的祖先一般都是汉人,因为在唐朝时朝鲜就是汉族人所管辖的领地。很多汉人会在朝鲜半岛当官并且定居在那里,即使到后来五代的高丽,高丽的创建王建。王建极有可能是西汉乐浪郡汉人的后裔,因为王氏是当时乐浪郡的望族,且人户很多。且王建创建的高丽是第一个统一朝鲜半岛的王朝。

在到后来的李朝,李朝的创建着李成桂因为姓氏是汉氏所以也可以认为它就是汉人。而半岛主要人口是原住民,简称土著。它们是不会写汉字的也写不会,只有那批优秀的人才会写汉字。这也就是为什么李朝的文臣都反对设计韩文了。它们认为只有戎狄(歧视少数民族的用于,不单单汉人会歧视对待别的少数民族,如南北朝时期的北魏,北魏的开国君主是鲜卑族,它们对比他们更北边的柔然这个民族称之为蠕蠕(像蠕动的虫子),我来给大家介绍当时的情况,鲜卑是北方游牧民族,它们在前秦的草木皆兵的时候,天下大乱之势乘机崛起,并统一被放建立北朝魏,而柔然是生活在比北魏还北方的少数民族,因为游牧民族不能种田,它们没东西吃的时候就去抢,你会看到一个奇怪的现象当北魏还是游牧民族鲜卑的时候它们就去抢汉族的食物,而柔然则来抢鲜卑人的食物。从而导致鲜卑人恨柔然人。后面建立北魏后,它们游牧民族自己也开始建造长城来抵御游牧民族)才有自己的语言。
最主要想说的是这些能上985、211的人学习能力普遍就比一般人强。这些能写汉字的人要比着这些朝鲜人学习能力强。
image-20240903190413213

总有一种人有种错觉,它们认为只要它们努力,就能和那些厉害的人一样。实则让他们坚持学习一天都不行。更何况自律呢?

好了扯了那么多,我们还是用最蠢,最好理解的条件语句来应对这种需求吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { NextResponse } from 'next/server'

export function middleware(request) {
// 判断 URL 路径是否是 /about
if (request.nextUrl.pathname.startsWith('/about')) {
// 重定向到 /about-2
return NextResponse.rewrite(new URL('/about-2', request.url))
}

if (request.nextUrl.pathname.startsWith('/dashboard')) {
// 判断 URL 路径是否是 dashboard 重定向到 /dashboard/user
return NextResponse.rewrite(new URL('/dashboard/user', request.url))
}
}

中间件逻辑

对于传入的请求,NextRequest 提供了 getgetAllsetdelete方法处理 cookies,你也可以用 has检查 cookie 或者 clear删除所有的 cookies。

对于返回的响应,NextResponse 同样提供了 getgetAllsetdelete方法处理 cookies。

… 建议上网查找相对的需求或参考原文。

中间件的代码维护

建议参考原文我会的也就是第一种中间的维护方式。高级函数模式勉强能理解。剩下的…

值得一提的是 return 返回 NextResponse.next() 可以执行下一个中间件。

中间件小结

我最初了解中间件是在学习 Nodejs 的 Express 框架认识的,中间件可以让我们拦截请求,如我们可以在中间件写生成 Token 的逻辑之类的。中间件的功能十分强大。

总结

路由处理程序可以让我们定义各种接口和请求。中间件让我们可以在请求前做一些事情。