前言
在 Vue 中,我们会使用到 Vue Router 来实现切换页面或路由的需求,而在 Nuxt 3 中,预设是没有使用路由相关依赖,直至建立了 pages
目录,Nuxt 将会自动加载 Vue Router 来管理路由,并且具有一定的规则需要遵循,以下将介绍页面目录与路由之间的关系。
基于文件的路由 (File-based Routing)
在 Nuxt 3 项目的 pages
目录下,当我们建立了一个页面文件,就会以该文件建立出相对应的路由,Nuxt 3 基于文件的路由,也使用了代码拆分将每个页面需要的代码梳理出来,并以动态加载的方式加载最小所需要的代码。因为是以目录结构与文件命名方式来约定,也称之为约定式路由。
建立第一个页面
Nuxt 3 的 pages
目录,是用来建立页面并放置的目录,当项目下有存在 pages
目录,Nuxt 将会自动加载 Vue Router 来实现路由效果,目录下的文件通常是 Vue 的组件,也允许具有 .vue、 .js、 .jsx、 .ts 或 .tsx 副档名的文件。
当我们建立 ./pages/index.vue
,文件内容如下,则表示路由 /
对应到这个页面文件,我们只需要建立文件,路由的配置将会由 Nuxt 3 自动产生。
<template>
<div class="bg-white py-24">
<div class="flex flex-col items-center">
<h1 class="text-6xl font-semibold text-gray-800">这里是首页</h1>
</div>
</div>
</template>
若你还记得 Vue Router 中的 <router-view />
,这是路由需要的进入点,同样的在 Nuxt 3 我们需要使用 <NuxtPage />
来显示我们建立的路由页面,这里非常重要,否则路由及页面将无法正确运行显示
。
修改 ./app.vue
,文件内容如下:
<template>
<div>
<NuxtPage />
</div>
</template>
接着我们在浏览器浏览 /
路由,如 http://localhost:3000/
,就可以看到我们在 ./pages/index.vue 页面内写的标题文字「这是首页」!
多个路由页面
在实务上,通常一个网站会有多个页面,并分别对应到不同的路由,接下来我们尝试建立 About 与 Contact 两个页面。
建立多个路由页面
建立 ./pages/about.vue
,内容如下:
<template>
<div class="bg-white py-24">
<div class="flex flex-col items-center">
<h1 class="text-6xl font-semibold text-yellow-400">大家好!我是 土豆</h1>
<p class="my-8 text-3xl text-gray-600">这里是 /about</p>
</div>
</div>
</template>
建立 ./pages/contact.vue
,内容如下:
<template>
<div class="bg-white py-24">
<div class="flex flex-col items-center">
<h1 class="text-6xl font-semibold text-rose-400">如果没事不要找我 </h1>
<p class="my-8 text-3xl text-gray-600">这里是 /contact</p>
</div>
</div>
</template>
接着我们在浏览器分别浏览 /about
或 /contact
,就可以看到我们路由效果啰!
可以发现所建立的文件名称
,最终就会自动对应产生
出 /about 及 /contact 路由。
自动产生的路由
如果你有兴趣想看看 Nuxt 自动产生出来的路由配置长什么样子,可以使用 npm run build
或 npx nuxt build
来建构出 .output
目录,并打开 .output/server/chunks/app/server.mjs
,搜寻 const _routes =
或刚刚建立的文件名称 about.vue
,就可以找到下面这一段代码:
这段代码与 Vue 中的路由配置非常相像,其实这就是 Nuxt 3 检测到 pages
目录,自动帮我们加载 Vue Router 与依据 pages
目录下的文件结构,自动产生出所需的路由配置。
建立路由链接
在 Vue Router 我们可以使用 <router-link>
来建立路由链接,以此来导航至其他页面,而在 Nuxt 3 的路由中,则是使用 <NuxtLink>
来建立路由链接来进行页面的跳转,我们尝试在首页新增几个路由链接来进行页面导航。
调整 ./pages/index.vue
,内容如下:
<template>
<div class="bg-white py-24">
<div class="flex flex-col items-center">
<h1 class="text-6xl font-semibold text-gray-800">这里是首页</h1>
<div class="my-4 flex space-x-4">
<NuxtLink to="/about">前往 About</NuxtLink>
<NuxtLink to="/contact">前往 Contact</NuxtLink>
</div>
</div>
</div>
</template>
接着我们在浏览器浏览首页,点击「前往 About」或「前往 Contact」就可以看见路由导航效果啰!
使用 <NuxtLink>
时,可以就把它想像为<router-link>
的替代品,像 to
这个 Pros 控制路由位置的用法基本上一样,其他更多的 Pros 用法及说明可以参考官网的文件。
如果想要使用像 Vue Router 提供的 router.push
方法于 Vue 中直接呼叫来导航至其他页面,在 Nuxt 中你可以使用 navigateTo
,参数可以参考官方文件。
约定式路由中的 index.vue
在开头有提到,Nuxt 3 提供了一个基于文件的路由,从上的例子你或许能发现,基本上文件名称就是对应着路由名称,但 index.vue
比较特别,它所对应的是路由 /
。
index.[ext] 这个效果和特性,其实是与 Node.js 底层核心有关,在此就不赘述。
举例来说,我可以在 pages 下建立一个 docs.vue
表示对应路由 /docs
,也可以将文件放置在 docs 目录下并重新命名为 index.vue
即 ./pages/docs/index.vue,这样也可以透过 /docs
浏览到相同的页面。
所以当 index.vue
存在于 pages 目录下,已经位于网站页面的第一层,所以我们浏览 http://localhost:3000/ 就可以做出首页的效果。
带参数的动态路由匹配
在实务上,我们可能需要将路径作为参数传递给同一个组件,例如,我们有一个 users
页面组件,在 /users/ryan
或 /users/jennifer
路径,都能匹配到同一个 users 组件,并将 ryan
或 jennifer
当作参数传递给 users 页面组件使用,那么我们就需要动态路由来做到这件事。
在 Vue 3 使用 Vue Router 我们可能会写出如下路由配置:
{
name: "users",
path: "/users/:id",
component: "./pages/users.vue",
}
这样我们就能达到进入 /users/ryan
路由将 ryan 当作 id
参数传入 users 组件中,路径参数用冒号 :
表示,这个被匹配的参数 (params),会在组件中可以使用 useRoute() 与 route.params.id 取得。
在 Nuxt 3 中,我们要实现这个效果,需要将文件名称添加中括号 []
,其中放入欲设定的参数名称,譬如下面的目录结构与文件名称。
./pages/
└── users/
└── [id].vue
建立 ./pages/users/[id].vue
文件,内容如下:
<template>
<div class="bg-white py-24">
<div class="flex flex-col items-center">
<h1 class="text-3xl text-gray-600">这里是 Users 动态路由页面</h1>
<p class="my-8 text-3xl text-gray-600">
匹配到的 Id: <span class="text-5xl font-semibold text-blue-600">{{ id }}</span>
</p>
</div>
</div>
</template>
<script setup>
const route = useRoute()
const { id } = route.params
</script>
我们在 script 就可以从 route.params
拿到我们所设定的参数名称 id
,并将其在 template 中渲染出来。浏览 http://localhost:3000/users/ryan ,看看效果,Nuxt 3 就能匹配到使用者的 id
参数 ryan
,并传入users 页面组件。
你也可以在 template 直接使用 {{ $route.params.id }}
来渲染出 id
参数。
匹配所有层级的路由
如果你需要匹配某个页面下的所有层级的路由,你可以在参数前面加上 ...
,例如,[...slug].vue
,这将匹配该路径下的所有路由。
建立 ./catch-all/[...slug].vue
文件:
./pages/
└── catch-all/
└── [...slug].vue
./catch-all/[...slug].vue
文件内容如下:
<template>
<div class="bg-white py-24">
<div class="flex flex-col items-center">
<h1 class="text-4xl text-gray-800">这是 catch-all/... 下的页面</h1>
<p class="mt-8 text-3xl text-gray-600">匹配到的 Params:</p>
<p class="my-4 text-5xl font-semibold text-violet-500">{{ $route.params.slug }}</p>
<span class="text-xl text-gray-400">每个数组元素对应一个层级</span>
</div>
</div>
</template>
我们可以输入 /catch-all/hello
及 /catch-all/hello/world
,路由的参数 slug
就会是一个数组,数组的每个元素对应每一个层级。
建立 404 Not Found 页面
Nuxt 3 提供一个配置来处理 404 Not Found
的页面,当我们建立 ./pages/[...slug].vue
页面, Nuxt 3 所有未匹配的路由,将会交由这个页面组件做处理,并同时使用 setResponseStatus(404)
函数设定 404 HTTP Status Code
。
./pages/[...slug].vue
<template>
<div class="bg-white py-24">
<div class="flex flex-col items-center">
<h1 class="text-8xl font-semibold text-red-500">404</h1>
<p class="my-8 text-3xl text-gray-800">Not Found</p>
<p class="my-8 text-xl text-gray-800">真的是找不到这个页面啦 >///<</p>
</div>
</div>
</template>
<script setup>
setResponseStatus(404)
</script>
/omg
这个是不存在的页面,未匹配的路由就会交由 ./pages/[...slug].vue 页面来处理。
建立多层的目录结构
如果理解了动态路由的中括号 []
用法,那我们就可以建立更复杂的页面目录结构:
./pages/
└── posts/
├── [postId]/
│ ├── comments/
│ │ └── [commentId].vue
│ └── index.vue
├── index.vue
└── top-[number].vue
这四个 Vue 页面的参考代码如下:
./pages/posts/index.vue
<template>
<div class="bg-white py-24">
<div class="flex flex-col items-center">
<h1 class="text-4xl text-gray-600">这是 Posts 首页</h1>
<div class="my-4 flex space-x-4">
<NuxtLink to="/posts/8"> 前往指定的文章 </NuxtLink>
<NuxtLink to="/posts/8/comments/1">前往指定的文章留言</NuxtLink>
<NuxtLink to="/posts/top-3">前往 Top 3</NuxtLink>
<NuxtLink to="/posts/top-10">前往 Top 10</NuxtLink>
</div>
</div>
</div>
</template>
./pages/posts/top-[number].vue
<template>
<div class="bg-white py-24">
<div class="flex flex-col items-center">
<h1 class="text-3xl text-gray-600">这是 posts/top-[number] 的页面</h1>
<p class="my-8 text-3xl text-gray-600">
匹配到的 Top Number: <span class="text-5xl font-semibold text-rose-500">{{ number }}</span>
</p>
</div>
</div>
</template>
<script setup>
const route = useRoute()
const { number } = route.params
</script>
./pages/posts/[postId]/index.vue
<template>
<div class="bg-white py-24">
<div class="flex flex-col items-center">
<h1 class="text-3xl text-gray-600">这是 posts/[postId] 的页面</h1>
<p class="my-8 text-3xl text-gray-600">
匹配到的 Post Id: <span class="text-5xl font-semibold text-blue-600">{{ postId }}</span>
</p>
</div>
</div>
</template>
<script setup>
const route = useRoute()
const { postId } = route.params
</script>
./pages/posts/[postId]/comments/[commentId].vue
<template>
<div class="bg-white py-24">
<div class="flex flex-col items-center">
<h1 class="text-3xl text-gray-600">这是 posts/[postId]/comments/[commentId] 的页面</h1>
<p class="my-8 text-3xl text-gray-600">
匹配到的 Post Id: <span class="text-5xl font-semibold text-blue-600">{{ postId }}</span>
</p>
<p class="my-8 text-3xl text-gray-600">
匹配到的 Comment Id:
<span class="text-5xl font-semibold text-purple-400">{{ commentId }}</span>
</p>
</div>
</div>
</template>
<script setup>
const route = useRoute()
const { postId, commentId } = route.params
</script>
来看看实际路由及匹配效果。
为了方便理解,整理了以下表格,来表示页面结构及期望匹配的模式与参数:
./pages/posts/index.vue
匹配模式 | 匹配路径 | 匹配参数 (Params) |
---|---|---|
/posts | /posts | 无 |
./pages/posts/top-[number].vue
匹配模式 | 匹配路径 | 匹配参数 (Params) |
---|---|---|
/posts/top-:number | /posts/top-3 | { number: 3 } |
/posts/top-:number | /posts/top-5 | { number: 5 } |
./pages/posts/[postId]/index.vue
匹配模式 | 匹配路径 | 匹配参数 (Params) |
---|---|---|
/posts/:postId | /posts/8 | { postId:8 } |
./pages/posts/[postId]/comment/[commentId].vue
匹配模式 | 匹配路径 | 匹配参数 (Params) |
---|---|---|
/posts/:postId/comments/:commentId | /posts/8/comments/1 | { postId: 8, commentId: 1 } |
到这里应该对于如何使用文件名称与目录结构,来制作动态路由与匹配参数有一些概念了。
巢状路由 (Nested Routes)
巢状路由 (Nested Routes) 或称嵌套路由,顾名思义,当我们想要在一个页面镶嵌另一个页面时,就需要巢状路由来帮助我们。
例如,我们想要在 docs 页面组件中显示 doc-1 或 doc-2 页面组件,并在切换 doc-1 或 doc-2 页面时,只是在 docs 下的嵌套页面进行切换。
/docs/doc-1 /docs/doc-2
+------------------+ +-----------------+
| docs | | docs |
| +--------------+ | | +-------------+ |
| | doc-1 | | +------------> | | doc-2 | |
| | | | | | | |
| +--------------+ | | +-------------+ |
+------------------+ +-----------------+
在 Vue 3 使用 Vue Router 实作上述巢状路由时,即 docs
页面要能显示 doc-1
,我们在路由配置可能就会写 path: '/docs'
与 children
,并在 children
加入 path: '/doc-1'
,其中 docs
页面包含 <router-view />
,最终浏览路由路径 /docs/doc-1
就可以看到嵌套页面的效果。
{
path: '/docs',
component: () => import('./pages/docs.vue')
children: [
{
path: 'doc-1',
component: () => import('./pages/docs/doc-1.vue')
}
]
}
而在 Nuxt 3 页面的约定式路由机制下,我们即是透过目录结构与页面组件实做出嵌套路由的效果。 举例来说,当我们建立了下面的目录页面结构:
这里需要注意,一定要有 docs.vue 与 docs 同名的目录
./pages/
├── docs/
│ ├── doc-1.vue
│ └── doc-2.vue
└── docs.vue
页面组件的参考代码如下:
./pages/docs.vue
<template>
<div class="bg-white">
<div class="my-6 flex flex-col items-center">
<h1 class="text-3xl font-semibold text-gray-800">这里是 Docs</h1>
<div class="my-4 flex space-x-4">
<NuxtLink to="/docs/doc-1">前往 Doc 1</NuxtLink>
<NuxtLink to="/docs/doc-2">前往 Doc 2</NuxtLink>
</div>
</div>
<div class="border-b-2 border-gray-100" />
<div class="flex flex-col items-center">
<NuxtPage />
</div>
</div>
</template>
./pages/doc-1.vue
<template>
<div class="flex flex-col items-center">
<p class="my-8 text-3xl text-blue-500">这是我的第一份文件</p>
</div>
</template>
./pages/doc-2.vue
<template>
<div class="flex flex-col items-center">
<p class="my-8 text-3xl text-green-500">这是我的第二份文件</p>
</div>
</template>
Nuxt 3 在自动生成路由时,实际上帮我们做出了类似这样子的路由结构:
{
name: "docs",
path: "/docs",
component: "./pages/docs.vue",
children: [
{
name: "docs-first-doc",
path: "doc-1",
component: "./pages/docs/doc-1.vue",
}
],
}
一定要记得在 docs 页面加上 <NuxtPage />
,来作为显示巢状页面的容器,接着分别浏览 /docs
、 /docs/doc-1
与 /docs/doc-2
,可以发现在两个页面中上方的皆有显示标题「这里是 Docs」,该文字是由 docs.vue
组件提供的标题文字,而页面下方则是 doc-1 与 doc-2 子页面显示的地方,以此就可以实现巢状路由效果啰!
小结
这篇介绍了关于 Nuxt 3 的页面与路由,这里的重点要记得,通过目录文件的结构与名称及中括号 []
我们就可以完成多数路由的情境,确实方便很多,也足以应付大部分实务上的需求,如果真的需要手动建立路由规则可以在参考官方或等待释出更好解决方案。
感谢大家的阅读,也欢迎大家给予建议 :)
如果对这个 Nuxt 3 系列感兴趣,可以关注一下小编,也欢迎分享给喜欢或正在学习 Nuxt 3 的伙伴。