Next.js 开发指南 路由篇 | 动态路由、路由组、平行路由和拦截路由
前言
实际项目开发的时候,有的路由场景会比较复杂,比如数据库里的文章有很多,我们不可能一一去定义路由,此时该怎么办?组织代码的时候,有的路由是用于移动端,有的路由是用于 PC 端,该如何组织?如何有条件的渲染页面,比如未授权的时候显示登录框?如何让同一个路由根据情况不同展示不同的内容?
本篇我们会一一解决这些问题,在此篇,你将会感受到 App Router 强大的路由功能。
1. 动态路由(Dynamic Routes)
有的时候,你并不能提前知道路由的地址,就比如根据 URL 中的 id 参数展示该 id 对应的文章内容,文章那么多,我们不可能一一定义路由,这个时候就需要用到动态路由。
1.1. [folderName]
使用动态路由,你需要将文件夹的名字用方括号括住,比如 [id]、[slug]。这个路由的名字会作为 paramprop 传给布局(layout)、 页面(page)、 路由处理程序(route)以及 generateMetadata(用于生成页面元数据) 函数。
举个例子,我们在 app/blog 目录下新建一个名为 [slug] 的文件夹,在该文件夹新建一个 page.js 文件,代码如下:
// app/blog/[slug]/page.js
export default function Page({ params }) {
return <div>My Post: {params.slug}</div>
}
效果如下:
当你访问 /blog/a的时候,params 的值为 { slug: 'a' }。
当你访问 /blog/yayu的时候,params 的值为 { slug: 'yayu' }。
以此类推。
1.2. [...folderName]
在命名文件夹的时候,如果你在方括号内添加省略号,比如 [...folderName],这表示捕获所有后面所有的路由片段。
也就是说,app/shop/[...slug]/page.js会匹配 /shop/clothes,也会匹配 /shop/clothes/tops、/shop/clothes/tops/t-shirts等等。
举个例子,app/shop/[...slug]/page.js的代码如下:
// app/shop/[...slug]/page.js
export default function Page({ params }) {
return <div>My Shop: {JSON.stringify(params)}</div>
}
效果如下:
当你访问 /shop/a的时候,params 的值为 { slug: ['a'] }。
当你访问 /shop/a/b的时候,params 的值为 { slug: ['a', 'b'] }。
当你访问 /shop/a/b/c的时候,params 的值为 { slug: ['a', 'b', 'c'] }。
以此类推。
1.3. [[...folderName]]
在命名文件夹的时候,如果你在双方括号内添加省略号,比如 [[...folderName]],这表示可选的捕获所有后面所有的路由片段。
也就是说,app/shop/[[...slug]]/page.js会匹配 /shop,也会匹配 /shop/clothes、 /shop/clothes/tops、/shop/clothes/tops/t-shirts等等。
它与上一种的区别就在于,不带参数的路由也会被匹配(就比如 /shop)
举个例子,app/shop/[[...slug]]/page.js的代码如下:
// app/shop/[[...slug]]/page.js
export default function Page({ params }) {
return <div>My Shop: {JSON.stringify(params)}</div>
}
效果如下:
当你访问 /shop的时候,params 的值为 {}。
当你访问 /shop/a的时候,params 的值为 { slug: ['a'] }。
当你访问 /shop/a/b的时候,params 的值为 { slug: ['a', 'b'] }。
当你访问 /shop/a/b/c的时候,params 的值为 { slug: ['a', 'b', 'c'] }。
以此类推。
2. 路由组(Route groups)
在 app目录下,文件夹名称通常会被映射到 URL 中,但你可以将文件夹标记为路由组,阻止文件夹名称被映射到 URL 中。
使用路由组,你可以将路由和项目文件按照逻辑进行分组,但不会影响 URL 路径结构。路由组可用于比如:
- 按站点、意图、团队等将路由分组
- 在同一层级中创建多个布局,甚至是创建多个根布局
那么该如何标记呢?把文件夹用括号括住就可以了,就比如 (dashboard)。
举些例子:
2.1. 按逻辑分组
将路由按逻辑分组,但不影响 URL 路径:
你会发现,最终的 URL 中省略了带括号的文件夹(上图中的(marketing)和(shop))。
2.2. 创建不同布局
借助路由组,即便在同一层级,也可以创建不同的布局:
在这个例子中,/account 、/cart、/checkout 都在同一层级。但是 /account和 /cart使用的是 /app/(shop)/layout.js布局和app/layout.js布局,/checkout使用的是 app/layout.js
2.3. 创建多个根布局
创建多个根布局:
创建多个根布局,你需要删除掉 app/layout.js 文件,然后在每组都创建一个 layout.js文件。创建的时候要注意,因为是根布局,所以要有 <html> 和 <body> 标签。
这个功能很实用,比如你将前台购买页面和后台管理页面都放在一个项目里,一个 C 端,一个 B 端,两个项目的布局肯定不一样,借助路由组,就可以轻松实现区分。
再多说几点:
- 路由组的命名除了用于组织之外并无特殊意义。它们不会影响 URL 路径。
- 注意不要解析为相同的 URL 路径。举个例子,因为路由组不影响 URL 路径,所以
(marketing)/about/page.js和(shop)/about/page.js都会解析为/about,这会导致报错。 - 创建多个根布局的时候,因为删除了顶层的
app/layout.js文件,访问/会报错,所以app/page.js需要定义在其中一个路由组中。 - 跨根布局导航会导致页面完全重新加载,就比如使用
app/(shop)/layout.js根布局的/cart跳转到使用app/(marketing)/layout.js根布局的/blog会导致页面重新加载(full page load)。
3. 平行路由(Parallel Routes)
平行路由可以使你在同一个布局中同时或者有条件的渲染一个或者多个页面(类似于 Vue 的插槽功能)。
3.1. 条件渲染
举个例子,在后台管理页面,同时展示团队(team)和数据分析(analytics)页面:
平行路由的使用方式就是将文件夹以 @作为开头进行命名,这个文件夹下面的 page.js 将会自动注入文件夹同级 layout 的 props 中。
注:从这张图还可以看出,children prop 其实就是一个隐式的插槽,/app/page.js相当于 app/@children/page.js。
除了让它们同时展示,你也可以根据条件判断展示:
在这个例子中,在布局中获取用户的登录状态,如果登录,显示 dashboard,没有登录,显示 login。这样做的一大好处就在于代码完全分离。
3.2. 独立错误处理和加载
平行路由可以让你为每个路由定义独立的错误处理和加载界面:
此外,平行路由跟路由组一样,不会影响 URL。比如 /@team/members 对应的地址是 /members。
3.3. 新约定文件 default.js
为了让大家更好的理解平行路由,我们写一个示例代码。项目结构如下:
app
├─ layout.js
├─ page.js
├─ about
│ └─ page.js
├─ @team
│ ├─ page.js
│ └─ member
│ └─ page.js
├─ @analytics
└─ page.js
其中 app/layout.js代码如下:
// app/layout.js
export default function RootLayout({ children, team, analytics }) {
return (
<html>
<body>
<h1>root layout</h1>
{children}
{team}
{analytics}
</body>
</html>
)
}
app/page.js代码如下:
// app/page.js
export default function Page() {
return <h1>Hello, App!</h1>
}
app/@team/page.js代码如下:
// app/@team/page.js
export default function Page() {
return <h1>Hello, Team!</h1>
}
app/@analytics/page.js代码如下:
// app/@analytics/page.js
export default function Page() {
return <h1>Hello, Analytics!</h1>
}
此时访问 /,效果如下:
app/about/page.js代码如下:
// app/about/page.js
export default function Page() {
return <h1>Hello, About!</h1>
}
此时访问 /about,效果如下:
结果出现了 404 错误。因为路由匹配,此时根布局里 team 和 anaylytics都为空。
为了解决这一问题,Next.js 添加了 default.js 文件,在 @team 和 @anaylytics下都添加一个 default.js:
// app/@team/default.js
export default function Page() {
return <h1>Hello, Team Default!</h1>
}
// app/@anaylytics/default.js
export default function Page() {
return <h1>Hello, Analytics Default!</h1>
}
此时访问 /跟以前一样,访问 /about则会出现:
3.4. 用途:实现 Modal
在实际开发中,平行路由可以用于渲染弹窗(Modal)。
我们想要实现的效果是,当跳转到 /login 的时候,渲染 Modal。
写个示例代码。项目目录如下:
app
├─ layout.js
├─ page.js
└─ @auth
├─ page.js
├─ default.js
└─ login
└─ page.js
app/layout.js代码如下:
// app/layout.js
import './globals.css';
import Link from 'next/link'
export default function RootLayout({ children, auth }) {
return (
<html>
<body>
<div><Link href="/login">Open Auth Modal</Link></div>
<div><Link href="/">Back To Home</Link></div>
<h1>/app/layout.js</h1>
{children}
{auth}
</body>
</html>
)
}
app/page.js 代码如下:
// app/page.js
export default function Page() {
return <h1>/app/page.js</h1>
}
如果没有 @auth下的代码,此时访问 /,效果应该是:
考虑到我们写的是一个 Modal 效果,当我们访问 / 的时候,Modal 应该是不被渲染的。当我们访问其他地址如/about的时候,Modal 也不应该被渲染。所以app/@auth/page.js和 app/@auth/default.js都应该 return 一个 null。
两个文件代码如下:
// app/@auth/page.js
export default function Page() {
return null
}
// app/@auth/default.js
export default function Default() {
return null
}
app/@auth/login/page.js代码如下:
'use client'
// app/@auth/login/page.js
import { useRouter } from 'next/navigation'
export default function Page() {
const router = useRouter()
return (
<div style={{width: '200px', height: '100px', backgroundColor: "red", position: "fixed", top: "20px", left: "220px"}}>
<span onClick={() => router.back()}>Close Modal</span>
<h1>Modal Content</h1>
</div>
)
}
最终效果如下:
当我们点击 Open Auth Modal的时候,路由跳转 /login,显示弹窗。点击弹窗里的 Close Modal,路由跳回 /,弹窗关闭。点击 Back To Home,从 /login 跳到 /,弹窗也会关闭。
之所以能实现这样一个功能,借助的就是平行路由的功能。当跳转到 /login 的时候,app/@auth/login/page.js 会作为 app/layout.js 中的 auth 参数传入,于是展示了弹窗。当跳转到 /的时候,展示 app/@auth/page.js,此时 return null,所以关闭了弹窗。
但是你可能发现一个问题,那就是当我们刷新 /login页面的时候,会出现 404 错误。刷新后的结果如下:
为什么会出现这样一个内容呢?
经过排查,这个 404 提示来自于 app/layout.js 中的 children。如果你把 {children}这行代码删除,就不会展示这个错误。
...
export default function RootLayout({ children, auth }) {
return (
...
<h1>/app/layout.js</h1>
{children}
{auth}
...
)
}
其实你把 children 理解为另外一个插槽就方便理解了。/app/page.js相当于 app/@children/page.js。
当访问 /login的时候,只匹配了 /@auth/login/page.js这个插槽,但是 /@children/page.js 就没有匹配到了。
当重新刷新的时候,Next.js 会首先尝试渲染不匹配插槽的 default.js 文件,如果不可用,再渲染 404。
所以解决这个问题也很简单,在 app下新建一个 default.js文件,也 return null 就可以了:
export default function Default() {
return null
}
此时再刷新 /login 页面,就没有 404 错误了:
4. 拦截路由(Intercepting Routes)
拦截路由允许你在当前布局内加载应用其他部分的路由。
4.1 效果展示
让我们直接看个案例,打开 dribbble.com 这个网站,你可以看到很多美图:
现在点击任意一张图片:
此时页面弹出了一层 Modal,Modal 中展示了该图片的具体内容。如果你想要查看其他图片,点击右上角的关闭按钮,关掉 Modal 即可继续浏览。值得注意的是,此时路由地址也发生了变化,它变成了这张图片的具体地址。如果你喜欢这张图片,直接复制或者分享当前的地址给朋友即可。
而当你的朋友打开时,其实不再需要以 Modal 的形式展现,直接展示这张图片的具体内容即可。现在刷新下该页面,你会发现页面的样式不同了:
在这个样式里没有 Modal,就是这张图片的内容。
你看同样一个路由地址,却展示了不同的内容。这就是拦截路由的效果。如果你在 dribbble.com 想要访问 dribbble.com/shots/xxxxx,此时会拦截 dribbble.com/shots/xxxxx 这个路由地址,以 Modal 的形式展现。而当直接访问 dribbble.com/shots/xxxxx 时,则是原本的样式。
示意图如下:
这是另一个拦截路由的 Demo 演示:nextjs-app-route-interception.vercel.app/
4.2 实现方式
那么这个效果该如何实现呢?在 Next.js 中,实现拦截路由需要你在命名文件夹的时候以 (..) 开头,其中:
(.)表示匹配同一层级(..)表示匹配上一层级(..)(..)表示匹配上上层级。(...)表示匹配根目录
但是要注意的是,这个匹配的是路由的层级而不是文件夹的层级,就比如路由组、平行路由这些不会影响 URL 的文件夹就不会被计算层级。
看个例子:
/feed/(..)photo对应的路由是 /feed/photo,要拦截的路由是 /photo,两者只差了一个层级,所以使用 (..)。
我们写个 demo 来实现这个效果,目录结构如下:
app
├─ layout.js
├─ page.js
├─ data.js
├─ default.js
├─ @modal
│ ├─ default.js
│ └─ (.)photo
│ └─ [id]
│ └─ page.js
└─ photo
└─ [id]
└─ page.js
每个文件代码都很简单。先 Mock 一下图片的数据,app/data.js代码如下:
export const photos = [
{id: '1', src: "http://placekitten.com/200/200"},
{id: '2', src: "http://placebear.com/200/200"}
]
app/page.js代码如下:
import Link from 'next/link'
import {photos} from './data';
export default function Home() {
return (
<main className="container">
{photos.map(({ id, src }) => (
<Link key={id} href={`/photo/${id}`}>
<img width="100" src={src} />
</Link>
))}
</main>
)
}
app/layout.js 代码如下:
export default function Layout({ children, modal }) {
return (
<html>
<body>
{children}
{modal}
</body>
</html>
)
}
此时访问 /,效果如下(图片就 2 张,你就假设这是个图片列表……):
现在我们再来实现下单独访问图片地址时的效果:
app/photo/[id]/page.js代码如下:
import {photos} from '../../data';
export default function PhotoPage({ params: { id } }) {
const photo = photos.find((p) => p.id === id)
return (
<img style={{width: '50%', display: 'block', marginLeft: 'auto', marginRight: 'auto'}} src={photo.src} />
)
}
访问 /photo/1,效果如下:
现在我们开始实现拦截路由,为了和单独访问图片地址时的样式区分,我们声明另一种样式效果。app/@modal/(.)photo/[id]/page.js代码如下:
import {photos} from '../../../data';
export default function PhotoModal({ params: { id } }) {
const photo = photos.find((p) => p.id === id)
return (
<div className="modal">
<img style={{width: '200', position: 'fixed', top: '120px'}} src={photo.src} />
</div>
)
}
两个 default.js的代码都是:
export default function Default() {
return null
}
最终的效果如下:
你可以看到,在 /路由下,访问 /photo/1,路由会被拦截,采用 @modal/(.)photo/[id]/page.js 的样式。该示例代码仓库地址为:github.com/mqyqingfeng…
小结
恭喜你,完成了本节内容的学习!
这一节我们介绍了动态路由、路由组、平行路由、拦截路由,它们的共同特点就需要对文件名进行修饰。其中动态路由用来处理动态的链接,路由组用来组织代码,平行路由和拦截路由则是处理实际开发中会遇到的场景问题。平行路由和拦截路由初次理解的时候可能会有些难度,但只要你跟着文章中的 demo 手写一遍,相信你一定能够快速理解和掌握!
- 初始篇 | Next.js CLI
- 路由篇 | App Router
- 路由篇 | 动态路由、路由组、平行路由和拦截路由
- 路由篇 | 路由处理程序和中间件
- 路由篇 | 国际化
- 数据获取篇 | 数据获取、缓存与重新验证
- 数据获取篇 | Server Actions 与表单
- 渲染篇 | 从 CSR、SSR、SSG、ISR 开始说起
- 渲染篇 | 服务端组件和客户端组件
- 渲染篇 | Streaming 和 Edge Runtime
- 缓存篇 | Caching
- 样式篇 | Tailwind CSS、CSS-in-JS 与 Sass
- 组件篇 | Images
- 组件篇 | Font
- 组件篇 | Link 和 Script
- 优化篇 | 懒加载
- 配置篇 | TypeScript 和 ESLint
- 配置篇 | 环境变量、路径别名与 src 目录
- 配置篇 | MDX
- 配置篇 | 草稿模式和内容安全策略
- 配置篇 | 路由段配置项
- 部署篇 | 静态导出
- Metadata 篇 | 基于配置
- Metadata 篇 | 基于文件
- API 篇 | next.config.js(上)
- API 篇 | next.config.js(下)
- API 篇 | 请求相关的常用函数与方法
- API 篇 | 常用函数与方法
- 实战篇 | React Notes | 项目介绍与创建
- 实战篇 | React Notes | 侧边栏笔记列表
- 实战篇 | React Notes | 笔记预览界面
- 实战篇 | React Notes | 笔记编辑界面
- 实战篇 | React Notes | 笔记搜索
- 实战篇 | React Notes | 国际化
- 实战篇 | React Notes | Auth
- 实战篇 | React Notes | 文件上传
- 实战篇 | React Notes | 部署(一)
- 实战篇 | React Notes | 部署(二)
- 实战篇 | 博客 | 项目创建
- 实战篇 | 博客 | 博客后台
- 实战篇 | 博客 | MDX
- 实战篇 | 博客 | Server Actions
- 实战篇 | 博客 | 渲染原理
- 实战篇 | App | 需求分析
- 实战篇 | App | 数据库设计
- 实战篇 | App | 项目创建
- 实战篇 | App | 移动端处理
- 实战篇 | App | 接口开发
- 实战篇 | App | 数据请求
- 实战篇 | App | 构建部署
- 源码篇 | 源码架构
- 源码篇 | 调试代码
- 源码篇 | 路由实现
- 源码篇 | 渲染原理
- 源码篇 | 手写 SSR
- 源码篇 | mini-next
- 源码篇 | mini-next
- 源码篇 | mini-next
- 源码篇 | mini-next
- 面试篇 | 常见面试题及解析
- 面试篇 | 常见面试题及解析
- 面试篇 | 常见面试题及解析
Next.js 开发指南 路由篇 | 动态路由、路由组、平行路由和拦截路由的更多相关文章
- Node.js开发指南中的例子(mysql版)
工作原因需要用到nodejs,于是找到了<node.js开发指南>这本书来看看,作者BYVoid 为清华大学计算机系的高材生,年纪竟比我还小一两岁,中华地广物博真是人才辈出,佩服. 言归正 ...
- 学习Nodejs:《Node.js开发指南》微博项目express2迁移至express4过程中填的坑
<Node.js开发指南>项目地址https://github.com/BYVoid/microblog好不容易找到的基础版教程,但书中是基于express2的,而现在用的是express ...
- 《Three js开发指南》 PDF
电子版仅供预览及学习交流使用,下载后请24小时内删除,支持正版,喜欢的请购买正版书籍:<Three js开发指南> pdf下载地址:链接: https://pan.baidu.com/s/ ...
- 《node.js开发指南》partial is not defined的解决方案
由于ejs的升级,<node.js开发指南>中使用的 partial 函数已经摒弃,使用foreach,include代替 原来的代码是: <%- partial('listitem ...
- Quartz.net官方开发指南系列篇
Quartz.NET是一个开源的作业调度框架,是OpenSymphony 的 Quartz API的.NET移植,它用C#写成,可用于winform和asp.net应用中.它提供了巨大的灵活性而不牺牲 ...
- 《Node.js开发指南》知识整理
Node.js简介 Node是一个可以让JavaScript运行在服务器端的平台,抛弃了传统平台依靠多线程来实现高并发的设计思路,而采用单线程.异步式I/O.事件驱动式的程序设计模型. 安装和配置No ...
- javacv教程文档手册开发指南汇总篇
本章作为javacv技术栈系列文章汇总 前言 写了不少关于javacv的文章,不敢说精通 ,只能说对javacv很熟悉.虽然偶尔也提交pull request做做贡献,但是javacv包含的库实在太多 ...
- Node.js 开发指南笔记
第一章:node简介 介绍了node是什么:node.js是一个让javascript运行在服务器端的开发平台, node能做些什么:[书上的] 具有复杂逻辑的网站 基于社交网络的大规模Web应用 W ...
- Three.js开发指南---使用构建three.js的基本组件(第二章)
.gui本章的主要内容 1 场景中使用哪些组件 2 几何图形和材质如何关联 3 正投影相机和透视相机的区别 一,Three所需要的基本元素 场景scene:一个容器,用来保存并跟踪所有我们想渲染的物体 ...
- NODE.JS开发指南学习笔记
1.Node.js是什么 Node.js是一个让JS运行在服务器端的开发平台,它可以作为服务器向用户提供服务.Node.js中的javascript只是Core javascript,或者说是ECMA ...
随机推荐
- stat函数详解
Linux系统函数之文件系统管理 stat函数 作用:获取文件信息 include <sys/types.h> #include <sys/stat.h> #include & ...
- MySQL innoDB 间隙锁产生的死锁问题
背景 线上经常偶发死锁问题,当时处理一张表,也没有联表处理,但是有两个mq入口,并且消息体存在一样的情况,频率还不是很低,这么一个背景,我非常容易怀疑到,两个消息同时近到这一个事务里面导致的,但是是偶 ...
- Linux系列教程——Linux文件查找、Linux压缩打包、Linux软件管理
@ 目录 1 Linux文件查找 1.find查找概述 2.find查找示例 1.find名称查找 2.find大小查找 3.find类型查找 4.find时间查找 5.find用户查找 6.find ...
- 【短道速滑十】非局部均值滤波的指令集优化和加速(针对5*5的搜索特例,可达到单核1080P灰度图 28ms/帧的速度)。
非局部均值滤波(Non Local Means)作为三大最常提起来的去燥和滤波算法之一(双边滤波.非局部均值.BM3D),也是有着很多的论文作为研究和比较的对象,但是也是有着致命的缺点,速度慢,严重的 ...
- 区间检测(range)
区间检测(range) 时间限制: 1 Sec 内存限制: 128 MB 题目描述 给定一个长度为n的序列,进行m次检测,每次检测某个区间中,是否有重复的数. 输入 第一行,两个整数n和m,表示序列 ...
- ciscn_2019_c_1 题解
main函数如下: int __cdecl main(int argc, const char **argv, const char **envp) { int v4; // [rsp+Ch] [rb ...
- HTTP协议中四种交互方法学习
一.Get Get用于获取信息,注意,他只是获取.查询数据,也就是说它不会修改服务器上的数据.而根据HTTP规范, 获取信息的过程是安全和幂等的.GET请求的数据会附在URL之后,以"?&q ...
- oauth2单点登录集成
单点登陆 概念: 单点登录其实就是在多个系统之间建立链接, 打通登录系统, 让同一个账号在多个系统中通用 举个例子: 登录Gmail的时候可以用账号密码登录, 也可以用google账号登录, 而使用g ...
- 自定义MyBatis拦截器更改表名
by emanjusaka from https://www.emanjusaka.top/archives/10 彼岸花开可奈何 本文欢迎分享与聚合,全文转载请留下原文地址. 自定义MyBati ...
- 使用openpyxl库读取Excel文件数据
在Python中,我们经常需要读取和处理Excel文件中的数据.openpyxl是一个功能强大的库,可以轻松地实现Excel文件的读写操作.本文将介绍如何使用openpyxl库读取Excel文件中的数 ...