NextJs 初级篇 - 安装 | 路由 | 中间件
- 一. NextJs 的安装
- 二. 路由
- 2.1 路由和页面的定义
- 2.2 布局的定义和使用
- 2.3 模板的定义和使用
- ① 模板 VS 布局
- ② 什么是 use client
- 2.4 路由跳转的方式
- 2.5 动态路由
- 2.6 路由处理程序
- ① GET 请求的默认缓存机制
- ② 控制缓存或者退出缓存的手段
- ③ 控制缓存的时效 revalidate
- ④ 常见的编写问题
- 3. 中间件
一. NextJs 的安装
首先,NextJs
要想使用,node
版本不能太低,最低也要18以上,版本不够的,可以用nvm
来管理,Mac用户的安装可以参考这篇文章:
mac 系统正确安装nvm
版本满足的同学,建议直接使用脚手架来创建nextJs
,输入脚手架命令(这里又切了阿里的镜像,以防万一关闭SSL
认证):
npm config set registry https://registry.npm.taobao.org
npm config set strict-ssl false
npx create-next-app@latest
结果如下:这里我们优先使用App Router
(后面会讲解)
生成结果如下:
启动项目: npm run dev
,之后访问:http://localhost:3000/
即可
二. 路由
NextJs
拥有两套路由(两者兼容):
Pages Router
:在pages
目录下创建对应的文件或者目录即是一个路由。App Router
:NextJs
从版本13.4起的默认路由模式
为什么官方在新版本中,默认的路由模式采用了App
呢?
pages
下每个文件都会被当成路由,不符合开发习惯app
架构新增了布局(layout
)、模版(template
)、加载状态(loading
)、错误处理(error
)、404 等文件,为项目开发提供了一套规范。
2.1 路由和页面的定义
我们这里主要讲官方更推荐的AppRouter
,如图:
这里简单介绍下:
- 我们约定使用
page
来代表一个页面,就好比React
中使用:index
。和React
一样,默认导出个组件即可。 - 文件的路径就是对应的路由。
app/page.tsx
对应路由/
app/about/page.tsx
对应路由/about
app/address/page.tsx
对应路由/address
- 后缀名可以使用:
.js、.jsx、.tsx
2.2 布局的定义和使用
首先布局组件有这么几个特征:
- 定义一个布局,需要新建一个名称为:
layout
的固定文件,该组件接收一个children
,代表子页面或者子布局。 - 布局可以嵌套,父布局中有一个子布局。
根布局还有额外几个特征:
- 定义在
app/layout.tsx
文件中,会应用于所有的路由。并且此文件必须存在。 - 根布局文件中必须包含
html
和body
标签。同时其他布局不能包含这些标签。
根布局文件内容示例:
import { Inter } from "next/font/google";
import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
);
}
布局的嵌套: address
下有个子目录home
,每个文件都有自己的layout
布局。
AddressLayout
:
const AddressLayout = ({ children }: any) => {
return (
<>
<nav>
我是Address的Layout
</nav>
{children}
</>
)
}
export default AddressLayout
HomeLayout
:
const HomeLayout = ({ children }: any) => {
return (
<>
<nav>
我是Home的Layout
</nav>
{children}
</>
)
}
export default HomeLayout
访问http://localhost:3000/address/home
,呈现效果:
2.3 模板的定义和使用
模板跟布局的用法是一样的,例如我们在address
下定义一个固定名称的模板文件:template.tsx
const AddressTemplate = ({ children }: any) => {
return (
<>
<nav>
我是Address的模板
</nav>
{children}
</>
)
}
export default AddressTemplate
此时我们访问 http://localhost:3000/address/home
,呈现效果:
这说明了:layout
会包裹 template
,template
再包裹 page
。
那模板和布局之间,存在着什么差异?
① 模板 VS 布局
状态的维持:
- 模板:路由切换的时候,模板的内容会随之变更。
- 布局:路由切换的时候,布局的内容不会更改。
框架默认行为的更改:以组件Suspense
为例,这个组件就是一个渲染fallback组件
效果的功能。
- 模板:模板内使用
Suspense
,每次切换路由的时候,都会展示fallback组件
。 - 布局:模板内使用
Suspense
,每次切换路由的时候,只会展示一次。
其实这两个点比较类似,说白了就是,每次在切换路由的时候,使用 template
,就可以重新触发渲染。
来个案例,目录结构如下:
sonA
:
const SonA = () => {
return <>
我是SonA!!!
</>
}
export default SonA
sonB
:
const SonB = () => {
return <>
我是SonB!!!
</>
}
export default SonB
父组件:page.tsx
import Link from "next/link";
const Parent = () => {
return <>
<nav>
当前页面是父组件
</nav>
<nav>
跳转到:<Link href='/parent/sonA'>SonA</Link>
</nav>
<nav>
跳转到:<Link href='/parent/sonB'>SonB</Link>
</nav>
</>
}
export default Parent
template.tsx
:
'use client'
import { useState } from "react"
const AddressTemplate = ({ children }: any) => {
const [count, setCount] = useState<number>(0);
return (
<>
<h1>Template点击次数: {count}</h1>
<button onClick={() => setCount(count + 1)}>
点击
</button>
{children}
</>
)
}
export default AddressTemplate
layout.tsx
:
'use client'
import { useState } from "react"
const ParentLayout = ({ children }: any) => {
const [count, setCount] = useState<number>(0);
return (
<>
<h1>Layout点击次数: {count}</h1>
<button onClick={() => setCount(count + 1)}>
点击
</button>
{children}
</>
)
}
export default ParentLayout
结果如下:
② 什么是 use client
'use client'
是一个指令,放置在文件的顶部(通常是模块的第一行)。它告诉 Nextjs
该模块应该在客户端环境中执行,而不是在服务器端。
备注:默认情况下,NextJs
中的页面是由服务端渲染的,即SSR
。
2.4 路由跳转的方式
在上述案例中,我们使用 Link
组件来完成路由的跳转。而NextJs
中,一共支持3种路由跳转的方式:
Link
组件。- 使用钩子函数:
useRouter
- 使用
redirect
函数
第一种 Link
组件,是最为推荐的方式,例如上述案例中的代码。
<Link href='/parent/sonA'>SonA</Link>
除此之外,还可以更改默认的跳转行为:App Router
的默认行为是滚动到新路由的顶部。可以通过传参 scroll
为false
改变。
<Link href='/parent/sonA' scroll={false}>SonA</Link>
第二种使用useRouter
函数:
'use client'
import Link from "next/link";
import { useRouter } from 'next/navigation'
const Parent = () => {
const router = useRouter();
return <>
<nav>
当前页面是父组件
</nav>
<nav>
跳转到:<Link href='/parent/sonA'>SonA</Link>
</nav>
<nav>
跳转到:<button onClick={() => router.push('/parent/sonB')}>SonB</button>
</nav>
</>
}
export default Parent
我们还能发现,一旦使用钩子函数,这种客户端触发的动作,就需要加上标识'use client'
,代表它是需要客户端渲染。
第三种:redirect
,用于给服务端组件用的,伪代码如下:
import { redirect } from 'next/navigation'
async function login(id) {
const res = await login()
if (!res.ok) {
redirect('/error')
}
return res.json()
}
2.5 动态路由
假设我们访问某个订单详情页,订单号可能是动态会变的,这里假设订单号是123456,一般我们可以设计两种URL
:
/orderInfo/123456
/orderInfo?orderId=123456
先说第一种,就需要通过动态路由的方式来访问。NextJs
中,需要将文件夹的名字用方括号[]
扩住。例如我们创建一个目录:orderInfo/[orderId]
,代表orderId
是一个动态路由,其中这个动态参数可以通过orderId
字段来读取。
然后页面上我们希望把订单号展示出来。其中组件接收一个参数:params
,如下:
import React from 'react'
const About = ({ params }: any) => {
return <>
<h1>订单详情页面</h1>
<span>{params.orderId}</span>
</>
}
export default About
那么在访问 http://localhost:3000/orderInfo/123
的时候,效果如下:
那如果我们动态参数后还跟着路由,例如 http://localhost:3000/orderInfo/123/shop
若直接访问我们看看结果:
我们发现此时404了,可见这种情况下,动态参数[orderId]
只会接收第一个路由片段,那怎么办呢?我们可以将[orderId]
改为: [[..orderId]]
,表示捕获所有后面所有的路由片段。
例如:
2.6 路由处理程序
NextJs
中,对于客户端和服务端之间的API
交互,叫做路由处理程序。
编写一个简单的路由处理程序,规则如下:
- 接口的路径跟路由比较相似,对应文件目录下创建一个名为
route
的文件即可。 - 文件中需要导出固定名称的函数(无需使用
default export
)GET、POST
等。 - 可以使用
NextResponse
代表返回的数据。
// 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) {}
一个简单的GET
和POST
请求:
GET
请求:
import { NextRequest, NextResponse } from 'next/server'
export async function GET() {
const data = [{ id: 1, name: 'LJJ' }]
return NextResponse.json({ data })
}
对应结果:
POST
请求:
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
const data = await request.json();
return NextResponse.json(data)
}
对应结果:
① GET 请求的默认缓存机制
默认情况下,使用NextResponse
对象的GET
请求会被NextJs
缓存起来。(本地模式下不会缓存,生产模式则会)
我们改一下我们的getUserList
接口:
import { NextResponse } from 'next/server'
export async function GET() {
const data = [{ id: 1, name: 'LJJ', date: new Date().getMilliseconds() }]
return NextResponse.json({ data })
}
然后跑命令: npm run build & npm run start
我们能发现构建产物:/api/getUserList
是静态的。
我们请求接口发现:无论请求几遍,时间戳永远不会变,是因为被缓存了。
② 控制缓存或者退出缓存的手段
退出缓存的方式有这么几种:
GET
请求中使用了Request
对象。例如:返回对象依赖于Request
的参数。- 添加其他
HTTP
方法,比如同一个route
文件中,同时定义了GET
和POST
函数。 - 使用像
cookies
、headers
这样的动态函数。 - 也可以手动声明接口为动态模式。 文件顶端增加:
export const dynamic = 'force-dynamic'
即可
// 返回结果依赖于request的参数
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const data = [{ id: 1, name: searchParams.toString(), date: new Date().getMilliseconds() }]
return NextResponse.json({ data })
}
// 使用cookies这种动态函数
export async function GET(request) {
const token = request.cookies.get('token')
return Response.json({ data: new Date().toLocaleTimeString() })
}
③ 控制缓存的时效 revalidate
我们可以在route
文件的顶部增加声明,例如以下就是标识缓存的有效期为10秒。
export const revalidate = 10
④ 常见的编写问题
首先是:获取URL
上的参数、获取Header
、获取Request
请求体(JSON
)、Cookie
等
import { NextRequest, NextResponse } from 'next/server'
export async function POST(request: NextRequest) {
// 获取URL上的参数
const searchParams = request.nextUrl.searchParams
console.log(searchParams.get('orderId'));
// 处理 Header
const headersList = new Headers(request.headers)
const referer = headersList.get('myHeader')
console.log(referer)
// 获取Request请求体 (JSON)
const data = await request.json();
// 如果是FormData
// const formData = await request.formData()
// const name = formData.get('name')
return NextResponse.json(data)
}
Postman
请求:http://localhost:3000/api/getDetail?orderId=123
结果如下:
重定向:使用redirect
即可。
import { redirect } from 'next/navigation'
export async function POST(request: NextRequest) {
redirect('https://www.baidu.com')
}
获取Cookie
、并设置到Response
中,则需要返回一个使用 Set-Cookie
header
的 Response
实例
import { cookies } from 'next/headers'
export async function POST() {
const cookieStore = cookies()
const token = cookieStore.get('test')
console.log(token)
return new Response('OK', {
status: 200,
headers: { 'Set-Cookie': `myNewToken=${12312312312}` },
})
}
请求后:
3. 中间件
NextJs
中,中间件的定义,可以在根目录中定义一个固定名称的文件:middleware.ts
,它的内容有两个部分组成:
// middleware.js
import { NextResponse } from 'next/server'
// 第一部分:导出固定名称的中间件
export function middleware(request) {
// ... 中间件逻辑
}
// 第二部分:设置匹配路径
export const config = {
matcher: '/api/:path*',
}
这里直接给个案例,假设我中间件有多个功能:
- 中间件
headers
:处理相关的Header
- 中间件
logging
:记录相关的日志
我们可以在根目录下创建个专门编写中间件的文件夹:middlewares
两个中间件内容:
// logging.ts
import { NextRequest } from "next/server";
export const logging = (next: any) => {
return async (request: NextRequest) => {
// ...
console.log('logging')
return next(request);
};
};
// headers.ts
import { NextRequest } from "next/server";
export const headers = (next: any) => {
return async (request: NextRequest) => {
console.log('headers')
return next(request);
};
};
然后我们可以设置下别名,tsconfig.json
文件中:
这样就可以在真正设置中间件的地方(根路径的middleware.ts
中)进行设置:
import { logging } from "@/middlewares/logging";
import { headers } from "@/middlewares/headers";
import { NextResponse } from "next/server";
function chain(functions: any, index = 0) {
const current = functions[index];
if (current) {
const next: any = chain(functions, index + 1);
return current(next);
}
return () => NextResponse.next();
}
export default chain([logging, headers]);
export const config = {
matcher: '/api/:path*',
}
这样访问任何一个接口,都会走相关的逻辑了。