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。

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

相关推荐
桂月二二4 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
CodeClimb5 小时前
【华为OD-E卷 - 第k个排列 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
hunter2062066 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb6 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角6 小时前
CSS 颜色
前端·css
九酒6 小时前
从UI稿到代码优化,看Trae AI 编辑器如何帮助开发者提效
前端·trae
浪浪山小白兔7 小时前
HTML5 新表单属性详解
前端·html·html5
lee5767 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm
2401_897579657 小时前
AI赋能Flutter开发:ScriptEcho助你高效构建跨端应用
前端·人工智能·flutter
光头程序员8 小时前
grid 布局react组件可以循数据自定义渲染某个数据 ,或插入某些数据在某个索引下
javascript·react.js·ecmascript