Next.js 14 App Router 海底捞针式的翻找再见!(上)

1. 前言

Next.js 14 提供了新的 App Router 作为默认的路由方案,文件夹的嵌套结构决定了路由的渲染或请求的返回处理。太棒了这逻辑清晰,再也不必手动配置 router 了,开发一路畅通了,家人们。

这简直就是危言耸听!那文档翻起来无异于海底捞针。

本文分为上下两篇,上篇主要以路由为核心,下篇补充网络请求以及各杂项。

2. 路由页面

例如有以下路由页面:

访问路径 页面组件 名称
/ app/page.js 主页
/blogs app/blogs 文章列表
/blogs/1 app/blogs/[id]/page.js 文章详情(使用id)
/blogs/hello-next app/blogs/[slug]/page.js 文章详情(使用slug)
/login app/(auth)/login/page.js 登录
/forget-password app/(auth)/forget-password 忘记密码

登录和忘记密码使用(auth)进行逻辑分组,不影响路径访问。

在路由渲染方面,还提供了诸如 layout.js、loading.js、error.js、not-found.js 以配置布局页面、加载页面、错误反馈页面、404页面。

3. 路由处理器

除了页面渲染,还有路由处理器(Route Handler)。笔者在使用这部分特性时摔过不少跟头,它不如 Pages Router 的 API Routes 那么直观。

假设有以下 API 接口:

  • GET api/v1/blogs
  • GET api/v1/blogs/1
  • POST api/v1/login { email, password }
  • GET api/v1/blogs?q=xxx
  • GET api/v1/blogs?q=xxx&&start=1&&end=2

在 route.js 中定义路由处理器:

tsx 复制代码
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) {}

每一个方法都有 request 参数,从中可以解析出所需的内容。请求对象的类型可以是 Request 或者 NextRequest,后者是扩展了前者功能的类型。在实际开发中,笔者使用的是后者。同理,响应对象的类型有 ResponseNextResponse

3.1 body

body 数据藏在 request.json() 中。

tsx 复制代码
// app/api/login/route.ts
// POST api/v1/login { email, password }
export async function POST(request: NextRequest) {
  // 1. 获取 body 内容
  const body = await request.json();
  const {email, password} = body
  
  // 2. 请求数据
  // ...
  
  // 3. 返回响应体
  return NextResponse.json({
    success: true,
    msg: '登录成功',
    data: null
  }, {
    status: 200,
	  headers: {
		  'Set-Cookies': `token=${token};path=/;max-age=86400;HttpOnly`,
	  }
  })
}

3.2 query && pathname

查询字符串藏在 request.nextUrl.searchParams 中。

  1. 如果是单个查询字符串:
tsx 复制代码
// app/api/blog/route.ts
// GET api/v1/blogs?q=xxx
export function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams
  const q = searchParams.get('q') // 'xxx'
  // ...
}
  1. 如果是多个查询字符串:
tsx 复制代码
// app/api/blog/route.ts
// GET api/v1/blogs?q=xxx&&start=1&&end=2
export function GET(request: NextRequest) {
  const searchParams = request.nextUrl.searchParams
  const q = searchParams.get('q') // 'xxx'
  const start = searchParams.get('start') // '1'
  const end = searchParams.get('end') // '2'
  // ...
}

pathname 藏在 request.nextUrl.pathname 下:

tsx 复制代码
// 如果访问 /home,该值为 /home
request.nextUrl.pathname

3.3 params

如果想要获取路由的动态参数 [id] 或 [slug],咋办?

这些数据就藏在路由处理器中的第二个参数中:

tsx 复制代码
// app/api/blog/[id]/route.ts
// GET api/v1/blogs/1
export async function GET(request: NextRequest, { params }: {params: {id: string}}) {
  const id = params.id
  // ...
}

// app/api/blog/[slug]/route.ts
// GET api/v1/blogs/hello-next
export async function GET(request: NextRequest, { params }: {params: {slug: string}}) {
  const slug = params.slug
  // ...
}

3.4 cookies

cookie 在用户登录时在响应豹纹中返回给前端,这在《图解HTTP》书中有形象的描述:

对应的豹纹如下:

可以看到,响应豹纹中通过 Set-Cookie 设置了 sid,以后客户端请求中自动携带了 Cookie,里面就放着 sid 数据。同理,cookie 里可以放 token。

有的接口需要把 token 信息传给服务器,因此获取 cookie 就变得尤为重要。在请求头和响应头中都可以获取到 Cookie,这里以请求头为例,利用 request.cookies.get('token')

tsx 复制代码
// app/api/blog/route.ts
// GET api/v1/blogs
export async function GET(request: NextRequest) {
  const response = await fetch(url, {
    method: 'GET',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${request.cookies.get('token')?.value}`,
    },
  });
  // 除了 200-299 之间的状态码都会视为失败
	if (response.ok) {
		const data = await response.json();
		return NextResponse.json({data});
	}
  return NextResponse.json('请求失败', {status: 500});
}

如上,拿到 token 的值设置到请求头参数 Authorization 中。

next/headers 可以得到 cookie 方法, 删除的方式如下:

tsx 复制代码
// app/api/logout/route.ts
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';

export async function DELETE() {
  // 设置过期时间为0来删除cookie
  cookies().set('token', '', { maxAge: 0 });

  return NextResponse.json({
    success: true,
    msg: '登出成功',
  });
}

3.5 headers

在 3.1 中可以看到,在返回 json 数据时可以设置 headers。

如果想要获取 headers 信息(只读),从 next/headers 可以得到 headers 方法:

tsx 复制代码
import { headers } from 'next/headers'

export async function GET(request: Request) {
  const headersList = headers()
  // ...
}

3.6 重定向

tsx 复制代码
import { redirect } from 'next/navigation'
 
export async function GET(request: Request) {
  redirect('https://nextjs.org/')
}

4. 在服务端组件中获取 URL 参数

在路由处理器中,可以从 request 中拿到很多东西,而到了服务端组件是没有 request 对象的。

在 page.js 中呈现的是某一路由的组件,从 props 中可以解构出 params 和 searchParams:

tsx 复制代码
// app/blog/[slug]/page.tsx
export default function Page({
  params,
  searchParams,
}: {
  params: { slug: string }
  searchParams: { [key: string]: string | string[] | undefined }
}) {
  return <h1>My Page</h1>
}

5. 在客户端组件中获取 URL 参数

以上是在服务端组件中获取参数的方式,而在客户端组件中利用客户端 hook 获取。

5.1 useRouter()

控制路由跳转、重定向等。

tsx 复制代码
'use client'
 
import { useRouter } from 'next/navigation'

export default function Page() {
  const router = useRouter()
 
  // ...
}
  • router.push(href: string, { scroll: boolean }) :对提供的路由执行客户端导航。在浏览器的历史堆栈中添加一个新条目。(可以用<Link> 组件代替。)
  • router.replace(href: string, { scroll: boolean }): 执行指向所提供路由的客户端导航,但不在浏览器历史堆栈中添加新条目。
  • router.refresh():刷新当前路由。向服务器发出新请求、重新获取数据请求并重新渲染服务器组件。客户端将合并更新的 React 服务器组件有效载荷,而不会丢失未受影响的客户端 React(如useState)或浏览器状态(如滚动位置)。
  • router.prefetch(href: string): 预取所提供的路由,以加快客户端转换。
  • router.back():返回浏览器历史堆栈中的前一个路由。
  • router.forward():向前导航至浏览器历史堆栈中的下一页。

5.2 usePathname()

获取 URL 上的 pathname,跟路径后面那一串,不包含查询字符串。

tsx 复制代码
'use client'
 
import { usePathname } from 'next/navigation'
 
export default function ExampleClientComponent() {
  const pathname = usePathname()
  
  // URL -> `/dashboard?search=my-project`
  // `pathname` -> 'dashboard'
  
  // URL -> `/dashboard/overview`
  // `pathname` -> '/dashboard/overview'
  
  // ...
}

5.3 useSearchParams()

获取 URL 上的查询字符串。

tsx 复制代码
'use client'
 
import { useSearchParams } from 'next/navigation'
 
export default function SearchBar() {
  const searchParams = useSearchParams()
 
  const search = searchParams.get('search')
 
  // URL -> `/dashboard?search=my-project`
  // `search` -> 'my-project'
  
  // ...
}

5.4 useParams()

获取 URL 上的路由参数。

tsx 复制代码
'use client'
 
import { useParams } from 'next/navigation'
 
export default function ExampleClientComponent() {
  const params = useParams<{ tag: string; item: string }>()
 
  // Route -> /shop/[tag]/[item]
  // URL -> /shop/shoes/nike-air-max-97
  // `params` -> { tag: 'shoes', item: 'nike-air-max-97' }
 
  // ...
}

5.5 redirect()

在客户端组件中,无法直接使用 redirect(path, type) 方法,它的使用范围是:Server Components, Route Handlers 以及 Server Actions

使用 useRouter() 相关方法代替或者这个🌰:nextjs.org/docs/app/ap...

6. 服务端组件和客户端组件的组合模式

服务端组件和客户端组件可以嵌套组合使用,但有所限制,它们各自的使用时机也不同。

6.1 服务端组件和客户端组件的使用时机

如果你想... 服务端组件 客户端组件
请求数据
直接获取后端资源
保持服务器中的敏感数据(获取 token、API key等等)
在服务器上保留大量依赖关系/减少客户端 JavaScript
添加交互性和事件监听器(onClick()、onChange()等)
使用状态和生命周期副作用(useState()、useReducer()、useEffect()等)
使用浏览器专用 API
使用依赖于状态、副作用或浏览器专用 API 的自定义钩子
使用 React Class 组件

6.2 不支持的模式:服务端组件作为模块引入客户端组件

下面代码中,将服务端组件作为模块嵌入客户端组件是不可行的:

tsx 复制代码
'use client'
 
// You cannot import a Server Component into a Client Component.
import ServerComponent from './Server-Component'
 
export default function ClientComponent({
  children,
}: {
  children: React.ReactNode
}) {
  const [count, setCount] = useState(0)
 
  return (
    <>
      <button onClick={() => setCount(count + 1)}>{count}</button>
 
      // ❌ 错误
      <ServerComponent />
    </>
  )
}

6.3 支持的模式:服务端组件放入客户端插槽

作为 props,比如从插槽中将服务端组件插入客户端则是正确的:

tsx 复制代码
// You can pass a Server Component as a child or prop of a Client Component.
import ClientComponent from './client-component'
import ServerComponent from './server-component'
 
// Pages in Next.js are Server Components by default
export default function Page() {
  return (
    <ClientComponent>
      // ✅ 正确
      <ServerComponent />
    </ClientComponent>
  )
}

🌰(全局主题设置):nextjs.org/docs/app/bu...


技术交流:

  • 公众号:见嘉 Being Dev
  • v:with_his_x
相关推荐
excel4 分钟前
前端必备:从能力检测到 UA-CH,浏览器客户端检测的完整指南
前端
前端小巷子11 分钟前
Vue 3全面提速剖析
前端·vue.js·面试
悟空聊架构18 分钟前
我的网站被攻击了,被干掉了 120G 流量,还在持续攻击中...
java·前端·架构
CodeSheep19 分钟前
国内 IT 公司时薪排行榜。
前端·后端·程序员
尖椒土豆sss23 分钟前
踩坑vue项目中使用 iframe 嵌套子系统无法登录,不报错问题!
前端·vue.js
遗悲风24 分钟前
html二次作业
前端·html
江城开朗的豌豆27 分钟前
React输入框优化:如何精准获取用户输入完成后的最终值?
前端·javascript·全栈
CF14年老兵28 分钟前
从卡顿到飞驰:我是如何用WebAssembly引爆React性能的
前端·react.js·trae
画月的亮31 分钟前
前端处理导出PDF。Vue导出pdf
前端·vue.js·pdf
江城开朗的豌豆37 分钟前
拆解Redux:从零手写一个状态管理器,彻底搞懂它的魔法!
前端·javascript·react.js