Next.js 的路由为什么这么奇怪?

Next.js 是 React 的全栈框架,主打服务端渲染,也就是 SSR(Server Side Rendering)。

它有一套非常强大但也很奇怪的路由机制。

这套路由机制是什么样的?为什么又说很奇怪呢?

我们试一下就知道了。

先创建个 Next.js 项目:

lua 复制代码
npx create-next-app@latest

执行 create-next-app,输入一些信息,Next.js 项目就创建好了。

进入项目,执行 npm run dev,把它跑起来:

浏览器访问可以看到这个页面,就代表跑成功了:

在项目下可以看到 src 下有个 app 目录,下面有 page.tsx:

我们添加几个目录:

guang/liu/page.tsx

javascript 复制代码
export default function Liu() {
  return <div>666</div>
}

guang/shuai/page.tsx

javascript 复制代码
export default function Shuai() {
    return <div>帅</div>
}

然后浏览器访问下:

可以看到,添加了几个目录,就自动多了几个对应的路由。

这就是 Next.js 的基于文件系统的路由。

刚学了 page.tsx 是定义页面的,那如果多个页面有公共部分呢?

比如这种菜单和导航:

肯定不是每个页面一份。

这种定义在 layout.tsx 里。

app/layout.tsx 是定义最外层的布局:

也就是 html 结构,还有 title、description 等信息:

在网页的 html 源码里可以看到这些:

我们改一下试试:

不止是根路由可以定义 layout,每一级都可以:

javascript 复制代码
export default function Layout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div>
        <div>
            左侧菜单
        </div>
        <div>{children}</div>
    </div>
  )
}

我们给 guang/shuai 这个页面添加了布局,效果是这样的:

Next.js 会自动在 page.tsx 组件的外层包裹 layout.tsx 组件。

有的同学可能会注意到有个渐变背景,这个是 global.css 里定义的,我们把它去掉:

然后继续看:

我们可以使用 Link 组件在不同路由之间导航:

有的同学说,这些都很正常啊。

那接下来看点不那么正常的:

如果我希望定义 /dong/111/xxx/222 (111、222 是路径里的参数)这样的路由的页面呢?

应该如何写?

这样:

javascript 复制代码
interface Params {
    params: {
        param1: string;
        param2: string;
    }
}
export default function Xxx(
    { params }: Params
) {
    return <div>
        <div>xxx</div>
        <div>参数:{JSON.stringify(params)}</div>
    </div>
}

路径中的参数的部分使用 [xxx] 的方式命名。

Next 会把路径中的参数取出来传入组件里:

这种叫做动态路由。

那如果我希望 /dong2/a/b/c 和 /dong2/a/d/e 都渲染同一个组件呢?

这样写:

javascript 复制代码
interface Params {
    params: {
        dong: string;
    }
}
export default function Dong2(
    { params }: Params
) {
    return <div>
        <div>dong2</div>
        <div>参数:{JSON.stringify(params)}</div>
    </div>
}

...dong\] 的语法就是用来定义任意层级路由的,叫做 catch-all 的路由。 ![](https://file.jishuzhan.net/article/1719705197363597314/674465259237ccacb32f5a0f9d7da5b8.webp) ![](https://file.jishuzhan.net/article/1719705197363597314/dd637f4e3769673d120893f5ae6ce868.webp) 可以看到,/dong2 下的任意的路由,都会渲染这个组件。 那我直接访问 /dong2 呢? ![](https://file.jishuzhan.net/article/1719705197363597314/a242823e111cddfc293791bae779b07b.webp) 可以看到,404 了。 但这种也可以支持,再加一个中括号,改成 \[\[...dong\]\] 就好了: ![](https://file.jishuzhan.net/article/1719705197363597314/1ea3cc5b5509b4b3e9d3ae7f3cc9a5ba.webp) 这样 /dong2 也会渲染这个组件,只不过参数是空: ![](https://file.jishuzhan.net/article/1719705197363597314/9733cd88323504595acb90802662541e.webp) 这种 \[\[...dong\]\] 的路由叫做 optional catch-all。 可以看到,Next.js 项目的目录可不只是单纯的目录,都是有对应的路由含义的。 那如果我就是想加个单纯的目录,不包括在路由里呢? 这样写: ![](https://file.jishuzhan.net/article/1719705197363597314/534520850f9915f7eb019c87faacaae8.webp) 我在 dong 和 dong2 的外层添加了一个 (dongdong) 的目录,那之前的路由会变么? ![](https://file.jishuzhan.net/article/1719705197363597314/38e68e27ada02371bfcfcf89899afcae.webp) ![](https://file.jishuzhan.net/article/1719705197363597314/9f9178eb684529d2ec585aa71f7fbd59.webp) 试了下,依然没变。 也就是说只要在目录名外加上个 (),就不计入路由,只是分组用的,这叫做路由组。 现在,我们一个 layout 下渲染了一个 page。 那如果我想一个 layout 渲染多个 page 呢? 这样写: ![](https://file.jishuzhan.net/article/1719705197363597314/ddffb1ff30d9e95b7afceb68990636f9.webp) guang2 下有 3 个 page,page.tsx、@aaa/page.tsx、@bbb/page.tsx 分别会以 children、aaa、bbb 的参数传入 layout.tsx layout.tsx ```javascript export default function Layout({ children, aaa, bbb }: { children: React.ReactNode, aaa: React.ReactNode, bbb: React.ReactNode }) { return (

{children}
{aaa}
{bbb}
) } ``` page.tsx ```javascript export default function Page() { return
page
} ``` @aaa/page.tsx ```javascript export default function Aaa() { return
aaa
} ``` @bbb/page.tsx ```javascript export default function Bbb() { return
bbb
} ``` 渲染出来是这样的: ![](https://file.jishuzhan.net/article/1719705197363597314/21fab44135a0ebe494f82168d9d5873c.webp) 可以看到,在 layout 里包含了 3 个 page 的内容,都渲染出来了,这种叫做平行路由。 有的同学会问,那 /guang2/@aaa 可以访问么? ![](https://file.jishuzhan.net/article/1719705197363597314/dcc2bcb8df38f97f6453cde676b44833.webp) 是不可以的。 此外,Next.js 还有一个很强大的路由机制: 之前有这样一个路由: ![](https://file.jishuzhan.net/article/1719705197363597314/78487f5822eda42c8d47828a0918ddfa.webp) 我们在它平级定义个路由: ![](https://file.jishuzhan.net/article/1719705197363597314/1db616c29590bc954c09c775022b7832.webp) ```javascript import Link from "next/link"; export default function Ccc() { return
to 666
ccc
} ``` 点击链接跳转到 /guang/liu ![](https://file.jishuzhan.net/article/1719705197363597314/b33674e64f65ebabb309464256c33827.webp) 这没啥问题。 但如果我在 ccc 下加一个 (..)liu 的目录: ![](https://file.jishuzhan.net/article/1719705197363597314/465479d9f61d0bac06688ab766fcfcf6.webp) 这时候再试一下: ![](https://file.jishuzhan.net/article/1719705197363597314/c4fac39b15a1a2ac4a727cd033878d0b.webp) 可以看到,这次渲染的 Liu 组件就被替换了,但要是刷新的话还是之前的组件。 很多同学会有疑惑,这个有啥用? 举个场景的例子就明白了。 比如一个表格,点击每个条目,都会跳出编辑弹窗,这个编辑页面可以分享,分享出去打开的是完整的编辑页面。 再比如登录,一些页面点击登录会弹出登录弹窗,但如果把这个登录链接分享出去,打开的是完整的登录页面。 也就是说在不同场景下,可以重写这个 url 渲染的组件,这个就是拦截路由的用处。 用法也很简单,因为要拦截的是上一级的 /guang/liu 的路由,所以前面就要加一个 (..) ![](https://file.jishuzhan.net/article/1719705197363597314/83e4c97bd472972fe2cf7fc33941c020.webp) 同理,还有 (.)xx 代表拦截当前目录的路由,(..)(..)xx 拦截上一级的上一级的路由,(...)xxx 拦截根路由。 这个拦截路由,在特定场景下很有用。 这些就是页面相关的路由机制,是不是还挺强大的? 当然,这些路由机制不只是页面可以用,Next.js 还可以用来定义 Get、Post 等接口。 只要把 page.tsx 换成 route.ts 就好了: ![](https://file.jishuzhan.net/article/1719705197363597314/a511208838e1fa7e916cba8494a421b1.webp) ```javascript import { NextResponse, type NextRequest } from 'next/server' const data: Record = { 1: { name: 'guang', age: 20 }, 2: { name: 'dong', age: 25 } } export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url) const id = searchParams.get('id'); return NextResponse.json(!id ? null : data[id]) } ``` 我们定义了 /guang3 的路由对应的 get 方法,根据 id 来取数据。 访问下: ![](https://file.jishuzhan.net/article/1719705197363597314/af2d4ac0c3a71a48219981f8ddc715b9.webp) ![](https://file.jishuzhan.net/article/1719705197363597314/03a5afdc4035b62d7a7b1d271349da29.webp) 前面学的那些路由,都可以用来 route.ts 上。 比如这样: ![](https://file.jishuzhan.net/article/1719705197363597314/da84e5fcd168f19820a32d3ab7bd8cc6.webp) \[id\] 定义动态路由参数,而 \[...yyy\] 是匹配任意的路由。 route.ts 的 GET 方法里,同样是通过 params 来取: ```javascript import { NextResponse, type NextRequest } from 'next/server' interface Params { params: { id: string; yyy: string; } } export async function GET(request: NextRequest, { params }: Params) { return NextResponse.json({ id: params.id, yyy: params.yyy }) } ``` ![](https://file.jishuzhan.net/article/1719705197363597314/862e9c07d3e29d7f10ec66f82e3837b9.webp) 感受到为啥 Next.js 被叫做全栈框架,而不是 SSR 框架了么? 因为它除了可以用来渲染 React 组件外,还可以定义接口。 这样,我们就把 Next.js 的路由机制过了一遍。 这种路由机制叫做 app router,也就是最顶层是 app 目录: ![](https://file.jishuzhan.net/article/1719705197363597314/da10e9fb2746ecb35982795a00539ed4.webp) 之前还有种 page router,最顶层目录是 page。 这俩只不过是两种文件、目录命名规则而已,我们只学 app router 就好了,它是最新的路由机制。 总结下我们学了什么: **aaa/bbb/page.tsx** 可以定义 /aaa/bbb 的路由。 **aaa/\[id\]/bbb/\[id2\]/page.tsx** 中的 \[id\] 是动态路由参数,可以在组件里取出来。 **aaa/\[...xxx\]/page.tsx** 可以匹配 /aaa/xxx/xxx/xxx 的任意路由,叫做 catch-all 的动态路由。但它不匹配 /aaa **aaa/\[\[...xxx\]\]/page.tsx** 同上,但匹配 /aaa,叫做 optional catch-all 的动态路由。 **aaa/(xxx)/bbb/page.tsx** 中的 (xxx) 只是分组用,不参与路由,叫做路由组 **aaa/@xxx/page.tsx** 可以在 layout.tsx 里引入多个,叫做平行路由 **aaa/(..)/bbb/page.js** 可以拦截 /bbb 的路由,重写对应的组件,但是刷新后依然渲染原组件,叫做拦截路由。 这些路由机制确实看起来挺奇怪的,它会导致 Next.js 的项目看起来这样: ![](https://file.jishuzhan.net/article/1719705197363597314/8a4649865befe82e1940db157b3e02f2.webp) 相比这种基于文件系统的路由,大家可能更熟悉 React Router 那种编程式路由: ![](https://file.jishuzhan.net/article/1719705197363597314/7560f60b744fefcc45459c582f1bb403.webp) Next.js 这种声明式的路由其实熟悉了还是很方便的。 不需要单独再维护路由了,目录就是路由,一目了然。 而且这些看似奇怪的语法,细想一下也很正常: 比如 \[xxx\],一般匹配 url 中的参数都是这种语法。 而 \[...xxx\] 只是在其中加个一个 ...,这个 ... 在 js 里就是任意参数的意思,所以用来匹配任意路由。 再加一个中括号 \[\[...xxx\]\] 代表可以不带参数,这个也是很自然的设计。 (.)xx、(..)xxx 这里的 . 和 .. 本来就是文件系统里的符号,用来做拦截路由也挺自然的。 路由组 (xxx) 加了个括号来表示分组,平行路由 @xxx 加了 @ 来表示可以引入多个 page,都是符合直觉的设计。 所以说,Next.js 基于文件系统实现这套路由机制,用的这些奇怪的语法,其实都是挺合理的设计。 ## 总结 我们学习了 Next.js 的路由机制,它是基于文件系统来定义接口或页面的路由。 Next.js 的路由机制挺强大的,支持的功能很多。 比如这样: ![](https://file.jishuzhan.net/article/1719705197363597314/e15e55c6de97b9189d0c8ff2e06c9ade.webp) 有动态路由参数 \[xx\]、catch-all 的动态路由 \[...xxx\]、optional catch-all 的动态路由 \[\[...xxx\]\]、路由组 (xxx)、平行路由 @xxx、拦截路由 (..)xxx。 这些语法乍看比较奇怪,但是细想一下,都是挺合理的设计。

相关推荐
chenqi6 分钟前
WebGPU和WebLLM:在浏览器中解锁端侧大模型的未来
前端·人工智能
Lingxing8 分钟前
Vue组件树:从设计到实现的全方位指南 🚀
前端·vue.js
玖玖passion9 分钟前
leader:请你用Protobuf进行数据交互🥲
前端
Linruoxin12 分钟前
为什么给 body 设置背景会直接铺满整个视口?
前端·css
Jenlybein15 分钟前
Vue3 权限控制:利用动态路由与自定义指令
前端·vue.js
codelang2 小时前
Cline + MCP 开发实战
前端·后端
好_快3 小时前
Lodash源码阅读-memoizeCapped
前端·javascript·源码阅读
好_快3 小时前
Lodash源码阅读-toString
前端·javascript·源码阅读
好_快3 小时前
Lodash源码阅读-memoize
前端·javascript·源码阅读
excel3 小时前
webpack 核心编译器 十四 节
前端