一、NextJs14快速上手

Nextjs13开始,更推荐使用App Router,我们也基于App Router来展开。

我们通过 npx create-next-app@latest 创建项目并使用其默认选项。

Nextjs13+中,默认都是服务端组件,如果我们要使用 hook 等,则需要通过 'use client' 声明为 客户端组件

路由

Nextjs 使用基于文件系统的路由,一个 page.js/ts/jsx/tsx 会作为路由的 UI 进行渲染。

Pages 和 Layouts 和 Templates

一个 page.js/ts/jsx/tsx 文件是一个路由的 UI 界面,这个文件总是这个路由子树的叶子节点

一个 layout.tsx 中的内容会在多个子路由页面内共享,可以嵌套,在导航时会保留布局状态和交互,不会重新渲染

最顶层要有一个 Root Layout ,其必须包含 htmlbody 标签,一般来说会使用 app/layout.tsx 作为 Root Layout 但不是必须的

layout 会包裹 page 的内容放到 children 属性的渲染位置

比如我们创建目录结构如下:

1
2
3
4
5
6
7
- app
- dashboard
- layout.tsx
- page.tsx # 对应路由 /dashboard
- settings
- layout.tsx
- page.tsx # 对应路由 /dashboard/settings

我们代码如下:

1
2
3
4
5
6
7
8
9
// /app/dashboard/layout.tsx
export default function Layout({children}: { children: React.ReactNode}) {
return (
<div className="w-[500px] h-[500px] bg-red-600">
<span>这是/dashboard/layout.tsx</span>
{ children }
</div>
)
}
1
2
3
4
5
6
// /app/dashboard/page.tsx
export default function Page() {
return (
<div className="w-[150px] h-[150px] bg-green-500 m-auto mt-[200px]">dashboard/page.tsx</div>
)
}

当我们访问 /dashboard 路由时可以看到如下页面:

此时我们来完善一下/dashboard/setting的路由内容

1
2
3
4
5
6
7
8
9
// /app/dashboard/settings/layout.tsx
export default function Layout({children}: { children: React.ReactNode}) {
return (
<div className="w-[400px] h-[400px] m-auto mt-4 bg-yellow-600">
<span>这是/dashboard/settings/layout.tsx</span>
{ children }
</div>
)
}
1
2
3
4
5
6
// /app/dashboard/setting/page.tsx
export default function Page() {
return (
<div className="w-[150px] h-[150px] bg-green-500 m-auto mt-[200px]">dashboard/settings/page.tsx</div>
)
}

当我们访问 /dashboard/settings 路由时可以看到如下页面:

可见其路由渲染规则,layout 可以嵌套且会被下级路由共享, page 作为路由的子节点来渲染页面

templatelayout 比较相似,也一样会包裹每个子布局与页面,但是在共享 template 的路由之间导航时,不回保留状态,因此在一些依赖于 useState/useEffect 等副作用函数的情况下会更合适。

这里我们在之前的案例基础上,在dashboard目录下新建一个 template.tsx:

1
2
3
4
5
6
7
8
9
// /app/dashboard/template.tsx
export default function Template({children}: { children: React.ReactNode}) {
return (
<div className="w-[450px] h-[450px] m-auto bg-blue-600">
<span>这是/dashboard/template.tsx</span>
{ children }
</div>
)
}

此时再次刷新页面查看 /dashboard/settings 路由对应的页面:

我们可以得出结论,在嵌套情况下, **template 会在 layoutchild layout 之间进行呈现 **

我们实现路由跳转有两种方式:

  • <Link></Link> 组件跳转
  • useRouter 编程式导航

Link 是扩展的内置标签 a 标签,通过 href 属性指定跳转地址:

1
2
3
4
5
import Link from 'next/link'

export default function Page() {
return <Link href="/dashboard/settings">Settings</Link>
}

useRouter 编程式路由导航,由于用到了 hook, 所以只能用于客户端组件。

1
2
3
4
5
6
7
8
9
10
11
12
'use client'
import { useRouter } from 'next/navigation'

export default function Page() {
const router = useRouter()
const handleClick = () => {
router.push('/dashboard/settings')
}
return (
<button type="button" onClick={handleClick}>Settings</button>
)
}

如果是通过用户点击来触发路由导航,我们总是建议使用 Link 组件来实现

路由组

app 目录下的文件夹名字会被映射为路由的 url,如果我们只想将文件夹用于组织文件而不影响路径,那么就可以使用路由组来实现,语法为 (foldername)

1
2
3
4
5
6
7
8
- app
- layout.tsx
- (marketing)
- about
- page.tsx # /about
- (shop)
- account
- page.tsx # /account

路由组的命名除了组织代码之外没有特殊意义

使用路由组还要注意避免路径重复

动态路由

如果不确定具体的路由路径,可以创建动态路由,语法为 [foldername]

动态路由会作为 params 参数传递

1
2
3
4
// /app/blog/[slug]/page.tsx
export default function Page({params}: {params: {slug: string}}) {
return <div>{params.slug}</div>
}

还可以使用 [...foldername] 来捕获后续路由参数,得到的 params 中的参数会是一个数组

平行路由

平行路由允许你同时或者有条件的渲染一个或多个页面在同一个layout中。

目录名使用@foldername创建。会作为参数传递给同级的layout

假设有目录结构为:

1
2
3
4
5
6
- app
- layout.tsx
- @analytics
- page.tsx
- @team
- page.tsx

此时同级的layoutapp/layout.tsx会接收这两个slots属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// app/layout.tsx
export default function Layout(props: {
children: React.ReactNode
analytics: React.ReactNode
team: React.ReactNode
}) {
return (
<div>
{porps.children}
{/* 可以同时渲染或者有选择的渲染 */}
{props.team /* app/team/page.tsx */}
{props.analytics /* app/analytics/page.tsx */}
</div>
)
}

children属性是一个隐式的插槽,不需要映射到文件夹。这意味着app/page.tsx相当于app/@children/page.tsx

路由处理程序

路由处理程序仅在 App Router 下可用,类似于 Page RouterAPI Routes

路由处理程序定义在一个特殊的文件 route.ts 中,但是同级不能存在一个 page.js/ts/jsx/tsx 文件了。这样如果目录下是 page 文件就是页面, 是 route 文件就是 API 且遵循 Rest API 风格.

路由处理程序在GET请求时默认会进行缓存。请求方法支持GET, POST, PUT, DELETE, HEAD, PATCH, OPTIONS,如果要建立怎么方法的请求,将其作为函数名即可。

比如我们有文件 app/api/v1/blogs/route.ts 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// GET请求 /api/v1/blogs
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')
const res = await fetch(`/xxx/xxx/${id}`)
const data = await res.json()
return Response.json({data})
}

// POST /api/v1/blogs
export async function POST() {}

// PUT /api/v1/blogs
export async function PUT() {}

路由处理程序不进行缓存的几种方式:

  • Request对象与GET方法一起使用
  • 使用非GET请求的其他HTTP方法。
  • 使用动态函数
  • 手动指定动态模式

route handler可以根据目录结构来构建RESTful规范的接口,规范的请求方法为:

方法 场景 示例
GET 获取数据 获取单个: GET /api/tasks/1
获取多个: GET /api/tasks?page=1&size=10
POST 创建数据 创建单个: POST /api/tasks
PATCH 差量修改数据 修改单个: PATCH /api/tasks/1
PUT 全量修改数据 修改单个: PUT /api/tasks/1
DELETE 删除数据 删除单个: DELETE /api/tasks/1
删除多个: DELETE /api/tasks?ids=1,2,3

接口路径使用资源名词而非动词,动作应该由HTTP Method来体现。

接口路径中的资源名词使用复数。

完成以上功能,需要有文件目录如下:

1
2
3
4
5
6
7
# 目录结构如下
app
- api
- tasks
- route.ts
- [id]
- route.ts

中间件

中间件也是一个特殊的文件,我们将其命名为 middleware.ts,但是要注意需要定义在项目根目录

示例:

1
2
3
4
5
6
7
8
9
10
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
export function middleware(request: NextRequest) {
// 可以使用NextResponse进行重定向
return NextResponse.redirect(new UTL('/home', request.url))
}
export const config = {
// 匹配在哪些路径上应用中间件
matcher: '/about/:path*'
}

其他特殊文件

前边我们已经说了 layout, page, template, route, middleware 这几个作为文件名的特殊意义,还有一些其他文件也会被路由匹配并有其意义。

  • loading.tsx - 基于 React.Suspense 创建加载 UI, 渲染完成后自动替换

可以将其效果理解为:

1
2
3
4
5
<Layout>
<Suspense fallback={<Loading />}>
<Page />
</Suspense>
</Layout>
  • error.tsx - 处理错误

还有比较特殊的目录名,_foldername 以下划线开头的目录作为私有目录,不会被解析为路由,比如我们可以使用 _components 来存放一些用于当前同级页面的组件

数据获取

有四种方式使用获取数据:

  • 在服务端组件使用 fetch
  • 在服务端组件使用第三方库
  • 在客户端组件使用 Route Handlers
  • 在客户端组件使用第三方库

第三方库的内容不展开了,后边我们还会说到 React Query, 这里说一下两外两个方式

在服务端组件使用 fetch

Nextjs 扩展了原生的 fetch, 允许为每个请求配置缓存和重新验证行为

fetch的基本使用:

1
2
3
4
5
6
7
8
9
10
11
async function getData() {
const res = await fetch('...')
return res.json()
}

export default function Page() {
const data = await getData()
return (
<main></main>
)
}

默认情况下会自动缓存 fetch 请求的数据,即便是 POST 请求也会自动的缓存,除非是在 Route Hanler 中使用 POST 请求

重新验证数据有两种方式:

  • 基于时间的重新验证,在经过一段时间后自动重新验证数据,对于不经常更改且新鲜度不高的数据很适用。
1
fetch('http:', { next: { revalidate: 3600 } } )
  • 按需重新验证,根据事件手动重新验证数据。
1
fetch('', { next: { tags: ['collection'] } })

重新验证:

1
2
3
4
5
'use server'
import { revalidateTag } from 'next/cache'
export default async function action() {
revalidateTag('collection')
}

fetch在以下几种情况下不会缓存:

  • cache: 'no-store'添加到请求配置中
  • revalidate: 0添加到请求配置中
  • fetch请求位于POST方法的Route Handler
  • fetch请求使用动态数据headers/cookies之后发出的
  • 使用const dynamic = 'force-dynamic'路由段
  • fetchCache路由段配置为跳过缓存
  • fetch请求使用请求头Authorization / Cookie,并且组件树上有一个未缓存的请求

在客户端组件使用 Route Handlers

比如有目录如下:

1
2
3
4
5
6
app
- api
- tasks
- route.ts
- demo
- page.tsx

我们可以这样来处理请求:

1
2
3
4
5
6
7
8
9
10
11
12
// app/demo/page.tsx
'use client'
export default function Page() {
const getData = async function() {
const res = await fetch('/api/tasks')
const data = await res.json()
console.log('data:', data)
}
return (
<button onClick={getData}>click</button>
)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// app/api/tasks/route.ts
export function GET() {
// 操作数据库
const mockData = {
code: 0,
message: 'success',
data: {
a: '1',
b: '2'
}
}
return Response.json(mockData)
}

数据库操作会在后边说到 Prisma 的时候再引入,这里用 mock 数据来模拟一下

Server Action 是一个较新的特性,但是并不能很好的覆盖所有应用场景,所以个人不太推荐使用,这里也不展开说了。

基于以上内容,其实已经可以 Nextjs 的开发了,更全面的内容包括渲染、缓存、优化、配置等内容还是要参考官方文档来看,内容太丰富了,没有条件在这里继续展开了。

作者

胡兆磊

发布于

2024-05-09

更新于

2024-06-20

许可协议