Next.js Router经典Web MVC设计
Tags: webdev, javascript, tutorial, react
Published: 2023-09-08 15:41:38


如果想看专栏的方式可以点击进入[Next图册目实践](http://rexweb.link/categories/Next%E5%9B%BE%E5%86%8C%E7%9B%AE%E5%AE%9E%E8%B7%B5/)

# Next.js Router

大家好呀，这里是梦兽编程。今天分享的内容是`Next.js`非常重要的内容。它关系到`Next.js`的整个系统的运行。如果你有`ThinkPHP3.2`的开发经验，那这个`Next.js 13`的文件路由是非常容易理解的。它就是一个很典型的 MVC Web 开发架构。

本次的花瓣发现案例将以**App Router**为例子。我们会在上次分享安装`Next.js`得到的项目目录如下:

``` shell
├── app
│   ├── favicon.ico
│   ├── globals.css
│   ├── layout.tsx
│   └── page.tsx
├── next.config.js
├── package-lock.json
├── package.json
├── pnpm-lock.yaml
├── postcss.config.js
├── public
│   ├── next.svg
│   └── vercel.svg
├── tailwind.config.ts
└── tsconfig.json
```

而我们的 app 项目路径就是`Next.js 13`使用 App router 的文件目录。如果你想使用`Page Router`。你可以改成`page`文件夹。注意的`page`文件的运行文件不是`page.tsx`

在`app`文件夹下我们可以看到有

``` shell
├── app
│   ├── favicon.ico
│   ├── globals.css // 全局的css
│   ├── layout.tsx  // 页面的layout布局
│   └── page.tsx    // page 只要的内容
```

当然我们还可以可选一些可选的文件内容,一般来说我们用的比较多的是前面两个。如果你是需要自定义路由则可以添加`route.tsx`文件进行定义。`next.js`app router 中优先匹配`route.tsx`路由规则。

> ps:`page.tsx`和`route.tsx`是不能共存的

- layout Shared UI for a segment and its children
- page Unique UI of a route and make routes publicly accessible
- loading Loading UI for a segment and its children (类似 React 中的 Suspense 一样的用法)
- not-found Not found UI for a segment and its children (没有匹配到路由的渲染页面可以理解成 404.html)
- error Error UI for a segment and its children
- global-error Global Error UI
- route Server-side API endpoint
- template Specialized re-rendered Layout UI （模版：可以理解成服务端渲染的组件，或者 C 语言中的宏，把公共的 HTML 渲染处理写在一起）
- default Fallback UI for Parallel Routes (如果你有一些数据需要异步加载想做骨架子的时候可以用到它)

> 以上的机制在每一个“page”（页面）文件夹都可以写一份，如果不存在会读取父级

# 多级路由映射

有了这些基础概念以上的基本路由概念之后，我们就可以愉快的做一些"MVC"的 web 开发了。如果我们需要拥有和本次项目实践差不多路由`https://huaban.com/discovery`。在`Next.js`要如何实现呢？

## 1. 开发小技巧

如果你想偷懒快速生成代码可以安装`ES7+React...`的插件
![截屏2023-09-01 21.40.31.png](https://x.imgs.ovh/x/2023/09/01/64f1ea7f3ad39.png)

我们只需要在 app 文件下新增一个`discovery`文件夹，并且新增`page.tsx`文件即可。

``` shell
// app 文件下
├── discovery
│   └── page.tsx
├── favicon.ico
├── globals.css
├── layout.tsx
└── page.tsx
```

在浏览器输入 http://localhost:3000/discovery 即可看到我们进入"MVC" 模式下的 discovery 应用页面。
![截屏2023-09-01 21.48.01.png](https://x.imgs.ovh/x/2023/09/01/64f1eba89e7f2.png)

# Next.js App Router 里的 Params 参数设置

## 1. 利用文件路由

在我们 React router 开发中经常会遇到这种路由`http://localhost:3000/discovery/:id`。在`Next.js 13`中应该如何实现这样的功能呢？

我们只需要在 discovery 文件夹下新增一个[id]文件夹目录同时新增`page.tsx`即可达到目的。这时候的文件目录将会变成以下结构:

``` shell 
//discovery 文件夹变成如下
├── [id]
│   └── page.tsx
└── page.tsx
```

### 我们如果需要获取`params`参数应该如何获取呢？

``` ts 
// [id]/page.tsx 文件内容
import React from "react";

function page({ params }: { params: { id: string } }) {
  return <div>discovery Id is : {params.id}</div>;
}

export default page;
```

是的 mvc 架构就是这么简单粗暴，我们在浏览器中输入`http://localhost:3000/discovery/rex`可以得到以下结果
![截屏2023-09-01 21.59.26.png](https://x.imgs.ovh/x/2023/09/01/64f1ee59ceb32.png)

## 2. 多个参数我们要如何应对？

在上面的例子中，我们知道传统 web mvc 架构的粗暴处理方式，为什么现在的 web 框架都是使用 router map 做正则配置呢？如果我们遇到这么一个路由`http://localhost:3000/discovery/:id/:pid/:cid`,在这个路由中如果我们还是安装 mvc 文件路由的方式，我们和容易就写出多个文件夹嵌套文件路径。参数多了人都会奔溃！！！

当然在 next.js 文件路由中。还是友好的给我们提供了方案的。我们只需要修改我们的文件夹就好了，比如

``` shell
// app 文件夹下新增demo 并且 [...slug]
.
├── demo
│   ├── [...slug]
│   │   └── page.tsx
│   └── page.tsx
```

主要这里我们多个参数获取的时候需要修改一下一下 page 的获取。需要在 slug 的属性下获取

``` ts
import React from "react";

function page({ params }: { params: { slug: string[] } }) {
  return <div>{params.slug.map((v) => v)}</div>;
}

export default page;
```

# Next.js Rest Api 的处理

## 1. Rest Api 如何定义并响应
我们做服务端应用开发最基础的功能就是对业务进行增删改成，在 Next.js 中同样给我们提供一套 Rest Api 的机制。就是我们上面提到的`router.tsx`文件。一般来说我们会在`app/api`里面进行开发，因为上面有提及到`route.(tsx|ts|jsx|js)`是不能与`page.(tsx|jsx)`共存一个文件目录下的。

同样路由规格还是以 next.js 的 mvc 文件规则一样。如果我们需要开发一个`http://localhost:3000/api/discovery`。我们需要把我们的项目结构改成：

``` shell
// app 文件夹下
.
├── api
│   └── discovery
│       └── route.ts
```

我们大致来看一下 route.ts 的大致模版

``` ts
// 分别对应 http 协议请求中的 get post put delete head 和 options 请求协议
export async function GET(request: Request) {}
export async function POST(request: Request) {}
export async function PUT(request: Request) {}
export async function DELTE(request: Request) {}
export async function HEAD(request: Request) {}
export async function OPTIONS(request: Request) {}
```

在服务端我们需要进行内容的响应也就是 HTTP 请求中的 Response，在 Next.js 中`next/server`中提供 NextResponse 的静态类方法。它和 w3c 定义中的 Response 类型基本的实现。这里我们就以`GET`请求例子，进行讲解。比如我们需要响应一个`{code:0}`的 json 格式

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

export async function GET(request: Request) {
  return NextResponse.json({ code: 0 }); // 默认http 响应状态码 为200
  // 可通过第二参数进行设置，它是一个 ResponseInit 的参数类型
  //   return NextResponse.json({ code: 0 }, { status: 500 });
}
```
## 2. 如何在 Rest Api 中获取Query和Param的参数类型还有Post的数据类型
这里以`POST`请求为例子，其他请求按需要获取

``` ts
// 项目结构
// .
// ├── api
// │   └── discovery
// │       ├── [id]
// │       │   └── route.ts
import { NextResponse } from "next/server";

export async function POST(request: Request, params: { id: string }) {
  // get query
  const { searchParams } = new URL(request.url);
  const allSearchParams = Object.fromEntries(searchParams);
  // get params
  const id = params.id;
  // get formdata
  const formdata = await request.formData();
  const formdataJson = Object.fromEntries(formdata);
  // get json 如果是 formdata 模式下不要请求
  // const json = await request.json();
  
  return NextResponse.json({
    allSearchParams,
    id,
    formdataJson,
    // json,
  });
}
```

这个时候我们使用curl命令进行测试
``` txt
curl -X POST -F "name=rex" "http://localhost:3000/api/discovery/rex?a=111"
# response json content
{"allSearchParams":{"a":"111"},"formdataJson":{"name":"rex"}}  
```
```txt
curl -X POST -H "Content-Type: application/json" -d '{"name":"rex"}' "http://localhost:3000/api/discovery/rex?a=111"

{"allSearchParams":{"a":"111"},"json":{"name":"rex"}
```

# 3. 获取 cookeis 和 headers 操作
这里以PUT请求为例子
``` ts
export async function PUT(request: NextRequest) {
    const cookies = request.cookies.getAll()
    // 不推荐 在构建类似 hexo 静态页面(SSG)的时候会报错，说你依赖了cookies无法构建
    // import {cookies} from 'next/headers'
    // const cookie = cookies()

    const headers = request.headers;
    const ContentType = headers.get('Content-Type')
}
```

# 服务端的页面跳转 redirect 在中间件里面用的比较多
``` ts
import { redirect } from 'next/navigation'
 
export async function GET(request: Request) {
  redirect('https://nextjs.org/')
}
```

# Middleware 中间件拦截器
Middleware 是我们服务端web开发里面非常重要的一个功能指标之一，你可以了解成`Axios`中的拦截器。在我们的开发过程中可以有一个地方进行处理。比如你的web应用需要进行Auth的校验可以使用Middleware进行一个入口全站应用，再比如你想每一个请求都进行一下日志打印的操作，这个时候中间件的出现真的是一杯美酒。

当然这个概念在其他web开发框架也有比如`java spring boot`,`php laravel`,`node.js nest.js`,`golang gin`。知道一个应用场景几乎其他语言的应用场景都会掌握只是语法上的区别和极端环境下的的问题了。

# 1. 我们在next.js中要如何使用Middleware
```shell
# 在app文件
.
...
├── ....
├── middleware.ts (添加)
```

中间件的具体内容

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

// This function can be marked `async` if using `await` inside
export async function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;
  // 比如我们对/api不做任何处理
  if (!["/api"].some((prefix) => pathname.startsWith(prefix))) {
    return NextResponse.next();
  }
  // 比如我们对 /demo 进行一下操作，如果没有权限的时候跳转到百度
  if (["/demo"].some((prefix) => pathname.startsWith(prefix))) {
    // handle someing
    return NextResponse.redirect("http://www.baidu.com");
  }
//   比如我们需要做一些日志操作的时候
//   if (request.method === "POST") {
//     console.log(await request.json());
//   }
  return NextResponse.next();
}

// See "Matching Paths" below to learn more
// 匹配的路由机制 除非你业务非常简单，要不不推荐使用
// export const config = {
//   matcher: '/:path*',
// }

```

# 结语
这里是梦兽编程，本次的代码更新将会放在Github[本次项目的Github连接](https://github.com/rexleimo/rex-huaban/tree/V2.0)中。

[我的B站视频号](https://space.bilibili.com/106325238?spm_id_from=333.1007.0.0)更多视频动态。

以上就是Next.js路由的所有内容了，如果对你的学习有任何帮助记得给梦兽编程一个一件三连