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 的路由。

可以看到,/dong2 下的任意的路由,都会渲染这个组件。

那我直接访问 /dong2 呢?

可以看到,404 了。

但这种也可以支持,再加一个中括号,改成 [[...dong]] 就好了:

这样 /dong2 也会渲染这个组件,只不过参数是空:

这种 [[...dong]] 的路由叫做 optional catch-all。

可以看到,Next.js 项目的目录可不只是单纯的目录,都是有对应的路由含义的。

那如果我就是想加个单纯的目录,不包括在路由里呢?

这样写:

我在 dong 和 dong2 的外层添加了一个 (dongdong) 的目录,那之前的路由会变么?

试了下,依然没变。

也就是说只要在目录名外加上个 (),就不计入路由,只是分组用的,这叫做路由组。

现在,我们一个 layout 下渲染了一个 page。

那如果我想一个 layout 渲染多个 page 呢?

这样写:

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 (
    <div>
        <div>{children}</div>
        <div>{aaa}</div>
        <div>{bbb}</div>
    </div>
  )
}

page.tsx

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

@aaa/page.tsx

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

@bbb/page.tsx

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

渲染出来是这样的:

可以看到,在 layout 里包含了 3 个 page 的内容,都渲染出来了,这种叫做平行路由。

有的同学会问,那 /guang2/@aaa 可以访问么?

是不可以的。

此外,Next.js 还有一个很强大的路由机制:

之前有这样一个路由:

我们在它平级定义个路由:

javascript 复制代码
import Link from "next/link";

export default function Ccc() {
    return <div>
        <div>
            <Link href="/guang/liu">to 666</Link>
        </div>
        <div>ccc</div>
    </div>
}
  

点击链接跳转到 /guang/liu

这没啥问题。

但如果我在 ccc 下加一个 (..)liu 的目录:

这时候再试一下:

可以看到,这次渲染的 Liu 组件就被替换了,但要是刷新的话还是之前的组件。

很多同学会有疑惑,这个有啥用?

举个场景的例子就明白了。

比如一个表格,点击每个条目,都会跳出编辑弹窗,这个编辑页面可以分享,分享出去打开的是完整的编辑页面。

再比如登录,一些页面点击登录会弹出登录弹窗,但如果把这个登录链接分享出去,打开的是完整的登录页面。

也就是说在不同场景下,可以重写这个 url 渲染的组件,这个就是拦截路由的用处。

用法也很简单,因为要拦截的是上一级的 /guang/liu 的路由,所以前面就要加一个 (..)

同理,还有 (.)xx 代表拦截当前目录的路由,(..)(..)xx 拦截上一级的上一级的路由,(...)xxx 拦截根路由。

这个拦截路由,在特定场景下很有用。

这些就是页面相关的路由机制,是不是还挺强大的?

当然,这些路由机制不只是页面可以用,Next.js 还可以用来定义 Get、Post 等接口。

只要把 page.tsx 换成 route.ts 就好了:

javascript 复制代码
import { NextResponse, type NextRequest } from 'next/server'

const data: Record<string, any> = {
    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 来取数据。

访问下:

前面学的那些路由,都可以用来 route.ts 上。

比如这样:

[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
    })
}

感受到为啥 Next.js 被叫做全栈框架,而不是 SSR 框架了么?

因为它除了可以用来渲染 React 组件外,还可以定义接口。

这样,我们就把 Next.js 的路由机制过了一遍。

这种路由机制叫做 app router,也就是最顶层是 app 目录:

之前还有种 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 的项目看起来这样:

相比这种基于文件系统的路由,大家可能更熟悉 React Router 那种编程式路由:

Next.js 这种声明式的路由其实熟悉了还是很方便的。

不需要单独再维护路由了,目录就是路由,一目了然。

而且这些看似奇怪的语法,细想一下也很正常:

比如 [xxx],一般匹配 url 中的参数都是这种语法。

而 [...xxx] 只是在其中加个一个 ...,这个 ... 在 js 里就是任意参数的意思,所以用来匹配任意路由。

再加一个中括号 [[...xxx]] 代表可以不带参数,这个也是很自然的设计。

(.)xx、(..)xxx 这里的 . 和 .. 本来就是文件系统里的符号,用来做拦截路由也挺自然的。

路由组 (xxx) 加了个括号来表示分组,平行路由 @xxx 加了 @ 来表示可以引入多个 page,都是符合直觉的设计。

所以说,Next.js 基于文件系统实现这套路由机制,用的这些奇怪的语法,其实都是挺合理的设计。

总结

我们学习了 Next.js 的路由机制,它是基于文件系统来定义接口或页面的路由。

Next.js 的路由机制挺强大的,支持的功能很多。

比如这样:

有动态路由参数 [xx]、catch-all 的动态路由 [...xxx]、optional catch-all 的动态路由 [[...xxx]]、路由组 (xxx)、平行路由 @xxx、拦截路由 (..)xxx。

这些语法乍看比较奇怪,但是细想一下,都是挺合理的设计。

相关推荐
迷雾漫步者1 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-1 小时前
验证码机制
前端·后端
燃先生._.2 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖3 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235243 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240254 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar4 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人5 小时前
前端知识补充—CSS
前端·css
GISer_Jing5 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试