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
相关推荐
前端小小王23 分钟前
React Hooks
前端·javascript·react.js
迷途小码农零零发33 分钟前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
娃哈哈哈哈呀1 小时前
vue中的css深度选择器v-deep 配合!important
前端·css·vue.js
旭东怪1 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
ekskef_sef3 小时前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端
sunshine6413 小时前
【CSS】实现tag选中对钩样式
前端·css·css3
真滴book理喻4 小时前
Vue(四)
前端·javascript·vue.js
蜜獾云4 小时前
npm淘宝镜像
前端·npm·node.js
dz88i84 小时前
修改npm镜像源
前端·npm·node.js
Jiaberrr4 小时前
解锁 GitBook 的奥秘:从入门到精通之旅
前端·gitbook