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,后者是扩展了前者功能的类型。在实际开发中,笔者使用的是后者。同理,响应对象的类型有 Response 和 NextResponse。
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
中。
- 如果是单个查询字符串:
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'
// ...
}
- 如果是多个查询字符串:
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