开场白:路由,其实就像快递小哥
想象你家的门牌号就是 URL。
快递小哥(浏览器)拿着包裹(HTTP 请求)走到你家楼下,发现门牌号写着:
/shop/42/comment/7
他得知道:
42是谁家的店?7是哪条评论?
在 Next.js 里,动态路由就是给快递小哥一张「万能地址簿」;而 Catch-all 路由则是一张「不管写了啥都能送到的魔法地图」。今天,咱们就把这两张地图拆开揉碎,再加点香料炒一炒。
一、动态路由:把门牌号变成变量
1.1 文件即路由,括号即变量
在 pages 或 app 目录下,只要文件名用 [] 包起来,Next.js 就会把它当成变量。
pages/
└── product/
└── [id].js ← /product/42 会匹配到这儿
代码示例(pages/product/[id].js):
import { useRouter } from 'next/router';
export default function Product() {
const router = useRouter();
// 就像拆快递:router.query.id === '42'
const { id } = router.query;
return (
<section>
<h1>商品编号:{id}</h1>
<p>商品描述:这是一只会写代码的咖啡杯,编号 {id}。</p>
{/* 偷偷告诉你,编号 42 是宇宙终极答案 */}
</section>
);
}
1.2 多级动态路由:把门牌号拆成楼-单元-室
pages/
└── shop/
└── [shopId]/
└── comment/
└── [commentId].js
匹配示例:
/shop/42/comment/7 → shopId = '42', commentId = '7'
底层小剧场:
Next.js 在构建时会把这些 [xxx] 编译成正则分组,运行时再用 RegExp 把 URL 拆成对象,存进 router.query。
就像把 "42/7" 用 / 切成数组,再按位赋值,简单粗暴却十分高效。
二、Catch-all 路由:不管来多少层,我全接住
2.1 语法:三个点,法力无边
把文件名写成 [...slug].js,那它就是 Catch-all。
slug 只是一个变量名,你爱叫 banana 也没人拦着。
pages/
└── docs/
└── [...slug].js
匹配示例:
/docs → slug: []
/docs/intro → slug: ['intro']
/docs/intro/install → slug: ['intro', 'install']
/docs/intro/install/docker → slug: ['intro', 'install', 'docker']
代码示例(pages/docs/[...slug].js):
import { useRouter } from 'next/router';
export default function Docs() {
const router = useRouter();
const { slug = [] } = router.query; // 默认空数组,防止 404 时炸掉
// 把路径数组拼成面包屑
const breadcrumb = slug.join(' / ');
return (
<main>
<h1>文档中心</h1>
<nav style={{ color: '#888' }}>{breadcrumb}</nav>
<article>
<h2>当前页面:{slug[slug.length - 1] || '首页'}</h2>
<p>这里是文档正文,懒得写了,假装有很多字。</p>
</article>
</main>
);
}
2.2 可选 Catch-all:在末尾加 ? 号
文件名写成 [[...slug]].js(双层中括号 + 三个点)。
区别:URL 可以完全不提供 slug,也能匹配到。
pages/
└── blog/
└── [[...slug]].js
匹配示例:
/blog → slug: undefined(不是空数组,而是直接没有)
/blog/2025 → slug: ['2025']
/blog/2025/08 → slug: ['2025', '08']
底层冷知识:
Next.js 在路由排序时,把可选 Catch-all 的优先级调到最低,防止它「吞噬」其他更具体的路由。
就像你排队买奶茶,可选 Catch-all 是那个永远说「我都可以」的佛系顾客,站在最后。
三、动态路由 vs Catch-all:一张对比表
| 特性 | 动态路由 [id] | Catch-all [...slug] | 可选 Catch-all [[...slug]] |
|---|---|---|---|
| 匹配层级 | 固定 1 级 | 任意层级 ≥ 1 | 任意层级 ≥ 0 |
| 额外参数 | 无 | slug 是数组 | slug 可能是 undefined |
| 优先级 | 高 | 中 | 低 |
| 常见场景 | 详情页 | 文档、分类、多级菜单 | 博客归档、文件浏览器 |
四、实战小彩蛋:用 Catch-all 做「伪静态」
需求:把 /blog/2025/08/15/hello-world 渲染成静态页面,但又不想写死每一年、月、日。
方案:
pages/blog/[...slug].js负责渲染。getStaticPaths在构建时读取 Markdown 文件名,生成路径数组。
伪代码:
// pages/blog/[...slug].js
export async function getStaticPaths() {
const posts = await getAllMarkdownPosts(); // ['2025/08/15/hello-world.md', ...]
const paths = posts.map(file => ({
params: { slug: file.replace('.md', '').split('/') }
}));
return { paths, fallback: 'blocking' };
}
export async function getStaticProps({ params }) {
const filePath = params.slug.join('/');
const content = await readMarkdown(filePath);
return { props: { content } };
}
底层原理:
Next.js 会把 getStaticPaths 返回的数组序列化成 json 文件,放到 .next/server/pages 里。
运行时,当请求 /blog/2025/08/15/hello-world,Next.js 用哈希表直接查文件,O(1) 复杂度,快到飞起。
五、结语:路由的尽头,是哲学
动态路由像变量,Catch-all 像通配符,它们共同编织出 URL 的无限可能。
当你在浏览器地址栏敲下最后一个 / 时,服务器里正有无数正则表达式在赛跑,只为把最合适的组件送到你面前。
「路由即人生,条条大路通罗马,但 Catch-all 表示:罗马是啥?我先到了再说。」
附录:一张图看懂匹配优先级
(用 ASCII 画一下,凑合看)
URL: /shop/42/comment/7
Next.js 路由匹配顺序 ↓
├─ /shop/[shopId]/comment/[commentId].js ← 命中!
├─ /shop/[...slug].js ← 没轮到
└─ /[...slug].js ← 更没戏
彩蛋问答
Q:为什么 [...slug].js 不能放在根目录 /[...slug].js?
A:可以,但你会收获一个「什么都吃」的怪兽,连 /api/hello 都会被它吞掉,然后你的 API 就 404 了。
结论:Catch-all 也要讲武德,别把同事的路由吃光了。
全文完。祝你编码愉快,404 常伴左右,但永远能跳回来。