Nuxt.js页面创建与路由系统
1. 基于文件系统的路由
Nuxt.js最强大的特性之一是其基于文件系统的路由系统。与传统的Vue应用需要手动配置路由不同,Nuxt.js会根据pages目录的结构自动生成路由配置。
1.1 路由生成原理
当Nuxt.js应用启动时,它会扫描src/pages目录,并根据文件和目录的结构生成对应的路由配置。这种方式使得路由管理变得简单直观,无需手动维护复杂的路由配置文件。
src/pages/
├── index.vue # 映射到 /
├── about.vue # 映射到 /about
├── blog/ # 映射到 /blog
│ ├── index.vue # 映射到 /blog
│ └── [id].vue # 映射到 /blog/:id
└── user/ # 映射到 /user
├── index.vue # 映射到 /user
└── [id]/ # 映射到 /user/:id
├── index.vue # 映射到 /user/:id
└── profile.vue # 映射到 /user/:id/profile1.2 路由生成规则
Nuxt.js的路由生成遵循以下规则:
- 文件映射:每个
.vue文件映射到一个路由 - 目录映射:每个目录也映射到一个路由段
- index.vue:目录中的
index.vue文件映射到该目录的根路径 - 动态路由:使用
[参数名].vue或[参数名]目录表示动态路由 - 嵌套路由:通过目录嵌套实现路由嵌套
2. 页面创建方法
2.1 基本页面创建
创建页面非常简单,只需要在src/pages目录下创建一个.vue文件即可。
2.1.1 创建首页
<!-- src/pages/index.vue -->
<template>
<div class="home">
<h1>首页</h1>
<p>欢迎来到我的Nuxt.js应用</p>
</div>
</template>
<style scoped>
.home {
padding: 2rem;
}
</style>2.1.2 创建关于页面
<!-- src/pages/about.vue -->
<template>
<div class="about">
<h1>关于我们</h1>
<p>这是关于页面</p>
</div>
</template>
<style scoped>
.about {
padding: 2rem;
}
</style>2.2 页面组件结构
一个标准的Nuxt.js页面组件通常包含以下部分:
<template>
<!-- 模板部分,定义页面的HTML结构 -->
</template>
<script setup>
// 脚本部分,使用组合式API
// 可以包含asyncData、fetch等特殊方法
</script>
<style scoped>
/* 样式部分,定义页面的样式 */
</style>3. 路由导航
在Nuxt.js中,有多种方式可以实现路由导航:
3.1 使用NuxtLink组件
NuxtLink是Nuxt.js提供的专用导航组件,类似于Vue Router的router-link。
<template>
<div>
<h1>导航示例</h1>
<nav>
<NuxtLink to="/">首页</NuxtLink>
<NuxtLink to="/about">关于我们</NuxtLink>
<NuxtLink to="/blog/1">博客文章1</NuxtLink>
</nav>
</div>
</template>3.2 使用编程式导航
除了使用NuxtLink组件,还可以使用编程式导航来实现路由跳转。
<template>
<div>
<h1>编程式导航示例</h1>
<button @click="goToHome">返回首页</button>
<button @click="goToAbout">前往关于页</button>
</div>
</template>
<script setup>
const router = useRouter();
const goToHome = () => {
router.push('/');
};
const goToAbout = () => {
router.push('/about');
};
</script>3.3 导航参数
NuxtLink组件和编程式导航都支持传递参数:
3.3.1 NuxtLink传递参数
<template>
<div>
<h1>传递参数示例</h1>
<NuxtLink :to="{ path: '/user', query: { id: 1, name: '张三' } }">
查看用户信息
</NuxtLink>
</div>
</template>3.3.2 编程式导航传递参数
<template>
<div>
<h1>编程式传递参数示例</h1>
<button @click="viewUser">查看用户信息</button>
</div>
</template>
<script setup>
const router = useRouter();
const viewUser = () => {
router.push({
path: '/user',
query: { id: 1, name: '张三' }
});
};
</script>4. 路由参数传递
4.1 查询参数
查询参数是最常见的参数传递方式,使用?后面的键值对表示:
<!-- 传递查询参数 -->
<NuxtLink to="/search?keyword=nuxt&page=1">搜索</NuxtLink>
<!-- 在页面中获取查询参数 -->
<script setup>
const route = useRoute();
const keyword = route.query.keyword; // 获取keyword参数
const page = route.query.page; // 获取page参数
</script>4.2 路径参数
路径参数是通过动态路由实现的,使用[参数名].vue或[参数名]目录:
<!-- 创建动态路由页面 -->
<!-- src/pages/user/[id].vue -->
<template>
<div class="user">
<h1>用户详情</h1>
<p>用户ID: {{ userId }}</p>
</div>
</template>
<script setup>
const route = useRoute();
const userId = route.params.id; // 获取路径参数
</script>
<!-- 导航到动态路由 -->
<NuxtLink to="/user/1">用户1</NuxtLink>
<NuxtLink to="/user/2">用户2</NuxtLink>5. 动态路由
动态路由是指路径中包含可变部分的路由,用于处理如用户详情、文章详情等需要根据ID或其他参数显示不同内容的场景。
5.1 基本动态路由
创建动态路由非常简单,只需要在pages目录下创建一个以[参数名].vue命名的文件即可:
<!-- src/pages/post/[id].vue -->
<template>
<div class="post">
<h1>文章详情</h1>
<p>文章ID: {{ postId }}</p>
<div v-if="post">
<h2>{{ post.title }}</h2>
<p>{{ post.content }}</p>
</div>
</div>
</template>
<script setup>
const route = useRoute();
const postId = route.params.id;
const { data: post } = await useAsyncData('post', () => {
return $fetch(`/api/post/${postId}`);
});
</script>5.2 多个动态参数
一个路由可以包含多个动态参数:
<!-- src/pages/[category]/[id].vue -->
<template>
<div class="item">
<h1>项目详情</h1>
<p>分类: {{ category }}</p>
<p>项目ID: {{ id }}</p>
</div>
</template>
<script setup>
const route = useRoute();
const category = route.params.category;
const id = route.params.id;
</script>5.3 可选动态参数
在Nuxt.js 3中,可以通过在参数名后添加?来创建可选的动态参数:
<!-- src/pages/blog/[slug]?.vue -->
<template>
<div class="blog">
<h1 v-if="slug">博客文章: {{ slug }}</h1>
<h1 v-else>博客首页</h1>
</div>
</template>
<script setup>
const route = useRoute();
const slug = route.params.slug;
</script>6. 嵌套路由
嵌套路由是指一个路由包含子路由的情况,用于构建复杂的页面结构。
6.1 基本嵌套路由
创建嵌套路由需要遵循以下步骤:
- 创建一个目录,名称对应父路由的路径
- 在该目录中创建一个
index.vue文件,作为父路由的页面 - 在该目录中创建其他
.vue文件或子目录,作为子路由
src/pages/
├── dashboard/ # 父路由: /dashboard
│ ├── index.vue # 父路由页面
│ ├── profile.vue # 子路由: /dashboard/profile
│ └── settings.vue # 子路由: /dashboard/settings6.2 嵌套路由组件
在父路由页面中,需要使用<NuxtPage>组件来显示子路由的内容:
<!-- src/pages/dashboard/index.vue -->
<template>
<div class="dashboard">
<h1>仪表盘</h1>
<nav>
<NuxtLink to="/dashboard/profile">个人资料</NuxtLink>
<NuxtLink to="/dashboard/settings">设置</NuxtLink>
</nav>
<div class="content">
<NuxtPage /><!-- 显示子路由内容 -->
</div>
</div>
</template>
<style scoped>
.dashboard {
padding: 2rem;
}
nav {
margin-bottom: 2rem;
}
nav a {
margin-right: 1rem;
}
.content {
margin-top: 2rem;
padding: 1rem;
border: 1px solid #ccc;
}
</style>6.3 嵌套动态路由
嵌套路由也可以包含动态参数:
src/pages/
├── users/ # 父路由: /users
│ ├── index.vue # 父路由页面
│ └── [id]/ # 动态子路由: /users/:id
│ ├── index.vue # 子路由页面: /users/:id
│ └── posts.vue # 子路由页面: /users/:id/posts7. 路由守卫
Nuxt.js提供了路由守卫功能,用于在路由切换前后执行逻辑,如认证检查、权限验证等。
7.1 全局路由守卫
全局路由守卫会在所有路由切换时执行,创建方式是在middleware目录下创建一个以global.开头的文件:
// src/middleware/global/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
// 检查用户是否登录
const user = useUserStore().user;
if (!user && to.path !== '/login') {
return navigateTo('/login');
}
});7.2 页面路由守卫
页面路由守卫只在特定页面的路由切换时执行,可以在页面组件中定义:
<!-- src/pages/dashboard.vue -->
<template>
<div class="dashboard">
<h1>仪表盘</h1>
</div>
</template>
<script setup>
definePageMeta({
middleware: 'auth' // 使用auth中间件
});
</script>8. 路由元信息
路由元信息是指附加到路由上的额外信息,用于存储如页面标题、权限要求等数据。
8.1 定义路由元信息
在页面组件中,可以使用definePageMeta函数来定义路由元信息:
<!-- src/pages/admin.vue -->
<template>
<div class="admin">
<h1>管理后台</h1>
</div>
</template>
<script setup>
definePageMeta({
title: '管理后台',
layout: 'admin',
middleware: 'admin',
requiresAuth: true,
permissions: ['admin']
});
</script>8.2 访问路由元信息
可以通过useRoute composable来访问当前路由的元信息:
<template>
<div>
<h1>{{ route.meta.title }}</h1>
</div>
</template>
<script setup>
const route = useRoute();
console.log(route.meta); // 访问路由元信息
</script>9. 路由优先级
当多个路由规则可能匹配同一个URL时,Nuxt.js会按照以下优先级顺序选择路由:
- 静态路由(如
/about.vue) - 动态路由(如
/user/[id].vue) - 全匹配路由(如
/[...slug].vue,用于捕获所有未匹配的路径)
10. 实战演练:构建博客路由系统
10.1 步骤1:创建基本目录结构
# 创建博客相关目录
mkdir -p src/pages/blog src/pages/admin/blog10.2 步骤2:创建页面文件
10.2.1 博客首页
<!-- src/pages/blog/index.vue -->
<template>
<div class="blog-index">
<h1>博客首页</h1>
<div class="blog-list">
<div v-for="post in posts" :key="post.id" class="blog-item">
<h2><NuxtLink :to="`/blog/${post.id}`">{{ post.title }}</NuxtLink></h2>
<p>{{ post.excerpt }}</p>
<p class="date">{{ post.date }}</p>
</div>
</div>
</div>
</template>
<script setup>
const { data: posts } = await useAsyncData('posts', () => {
return $fetch('/api/posts');
});
</script>
<style scoped>
.blog-index {
padding: 2rem;
}
.blog-list {
margin-top: 2rem;
}
.blog-item {
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid #eee;
}
.date {
font-size: 0.8rem;
color: #666;
}
</style>10.2.2 博客文章详情页
<!-- src/pages/blog/[id].vue -->
<template>
<div class="blog-post">
<div v-if="post">
<h1>{{ post.title }}</h1>
<p class="date">{{ post.date }}</p>
<div class="content" v-html="post.content"></div>
<NuxtLink to="/blog" class="back-link">返回博客首页</NuxtLink>
</div>
<div v-else>
<h1>文章不存在</h1>
<NuxtLink to="/blog">返回博客首页</NuxtLink>
</div>
</div>
</template>
<script setup>
const route = useRoute();
const id = route.params.id;
const { data: post } = await useAsyncData('post', () => {
return $fetch(`/api/post/${id}`);
});
</script>
<style scoped>
.blog-post {
padding: 2rem;
}
.date {
font-size: 0.8rem;
color: #666;
margin-bottom: 2rem;
}
.content {
line-height: 1.6;
margin-bottom: 2rem;
}
.back-link {
display: inline-block;
margin-top: 2rem;
padding: 0.5rem 1rem;
background-color: #f0f0f0;
border-radius: 4px;
text-decoration: none;
color: #333;
}
.back-link:hover {
background-color: #e0e0e0;
}
</style>10.2.3 博客管理页面
<!-- src/pages/admin/blog/index.vue -->
<template>
<div class="admin-blog">
<h1>博客管理</h1>
<NuxtLink to="/admin/blog/create" class="create-button">创建新文章</NuxtLink>
<div class="blog-list">
<div v-for="post in posts" :key="post.id" class="blog-item">
<h2>{{ post.title }}</h2>
<p class="date">{{ post.date }}</p>
<div class="actions">
<NuxtLink :to="`/admin/blog/edit/${post.id}`" class="edit-button">编辑</NuxtLink>
<button @click="deletePost(post.id)" class="delete-button">删除</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
definePageMeta({
middleware: 'admin'
});
const { data: posts, refresh: refreshPosts } = await useAsyncData('admin-posts', () => {
return $fetch('/api/admin/posts');
});
const deletePost = async (id) => {
if (confirm('确定要删除这篇文章吗?')) {
await $fetch(`/api/admin/post/${id}`, {
method: 'DELETE'
});
refreshPosts();
}
};
</script>
<style scoped>
.admin-blog {
padding: 2rem;
}
.create-button {
display: inline-block;
margin-bottom: 2rem;
padding: 0.5rem 1rem;
background-color: #4CAF50;
color: white;
border-radius: 4px;
text-decoration: none;
}
.blog-list {
margin-top: 2rem;
}
.blog-item {
margin-bottom: 2rem;
padding: 1rem;
border: 1px solid #eee;
border-radius: 4px;
}
.date {
font-size: 0.8rem;
color: #666;
}
.actions {
margin-top: 1rem;
}
.edit-button {
display: inline-block;
padding: 0.3rem 0.6rem;
background-color: #2196F3;
color: white;
border-radius: 4px;
text-decoration: none;
margin-right: 0.5rem;
}
.delete-button {
padding: 0.3rem 0.6rem;
background-color: #f44336;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>10.2.4 创建文章页面
<!-- src/pages/admin/blog/create.vue -->
<template>
<div class="admin-create">
<h1>创建新文章</h1>
<form @submit.prevent="createPost">
<div class="form-group">
<label for="title">标题</label>
<input type="text" id="title" v-model="post.title" required>
</div>
<div class="form-group">
<label for="excerpt">摘要</label>
<textarea id="excerpt" v-model="post.excerpt" required></textarea>
</div>
<div class="form-group">
<label for="content">内容</label>
<textarea id="content" v-model="post.content" required></textarea>
</div>
<button type="submit" class="submit-button">创建</button>
</form>
</div>
</template>
<script setup>
definePageMeta({
middleware: 'admin'
});
const post = reactive({
title: '',
excerpt: '',
content: ''
});
const router = useRouter();
const createPost = async () => {
await $fetch('/api/admin/post', {
method: 'POST',
body: post
});
router.push('/admin/blog');
};
</script>
<style scoped>
.admin-create {
padding: 2rem;
}
.form-group {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
}
input, textarea {
width: 100%;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
textarea {
min-height: 150px;
}
.submit-button {
padding: 0.5rem 1rem;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>10.2.5 编辑文章页面
<!-- src/pages/admin/blog/edit/[id].vue -->
<template>
<div class="admin-edit">
<h1>编辑文章</h1>
<div v-if="post">
<form @submit.prevent="updatePost">
<div class="form-group">
<label for="title">标题</label>
<input type="text" id="title" v-model="post.title" required>
</div>
<div class="form-group">
<label for="excerpt">摘要</label>
<textarea id="excerpt" v-model="post.excerpt" required></textarea>
</div>
<div class="form-group">
<label for="content">内容</label>
<textarea id="content" v-model="post.content" required></textarea>
</div>
<button type="submit" class="submit-button">更新</button>
</form>
</div>
</div>
</template>
<script setup>
definePageMeta({
middleware: 'admin'
});
const route = useRoute();
const id = route.params.id;
const router = useRouter();
const { data: post } = await useAsyncData('post', () => {
return $fetch(`/api/admin/post/${id}`);
});
const updatePost = async () => {
await $fetch(`/api/admin/post/${id}`, {
method: 'PUT',
body: post.value
});
router.push('/admin/blog');
};
</script>
<style scoped>
.admin-edit {
padding: 2rem;
}
.form-group {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
}
input, textarea {
width: 100%;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
textarea {
min-height: 150px;
}
.submit-button {
padding: 0.5rem 1rem;
background-color: #2196F3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>8. 路由配置
虽然Nuxt.js会自动生成路由配置,但在某些情况下,可能需要自定义路由配置。
8.1 基础路由配置
在nuxt.config.ts文件中,可以配置路由的基本选项:
export default defineNuxtConfig({
routeRules: {
// 路由规则配置
'/admin/**': {
auth: true
},
'/api/**': {
cors: true
}
}
});8.2 自定义路由
在Nuxt.js 3中,可以通过创建app/router.options.ts文件来自定义路由配置:
// app/router.options.ts
export default defineRouterOptions({
routes: (_routes) => {
// 在这里可以修改或添加路由
return _routes;
}
});9. 常见问题与解决方案
9.1 问题:动态路由参数获取不到
原因:
- 路由参数名称与文件命名不一致
- 使用了错误的方式获取路由参数
解决方案:
- 确保文件命名与参数名称一致(如
[id].vue对应route.params.id) - 使用
useRoute()composable来获取路由参数
9.2 问题:嵌套路由不显示
原因:
- 父路由页面中没有包含
<NuxtPage>组件 - 目录结构不符合嵌套路由要求
解决方案:
- 在父路由页面中添加
<NuxtPage>组件 - 确保目录结构正确,子路由文件应该放在父路由对应的目录中
9.3 问题:路由守卫不执行
原因:
- 路由守卫文件命名不正确
- 路由守卫配置错误
解决方案:
- 全局路由守卫文件名应以
global.开头 - 页面路由守卫应使用
definePageMeta函数配置
10. 小结
在本教程中,我们学习了Nuxt.js的页面创建和路由系统,包括:
- 基于文件系统的路由:了解了Nuxt.js如何根据文件和目录结构自动生成路由
- 页面创建方法:学习了如何创建基本页面和页面组件的结构
- 路由导航:掌握了使用
NuxtLink组件和编程式导航的方法 - 路由参数传递:学习了如何使用查询参数和路径参数传递数据
- 动态路由:掌握了创建和使用动态路由的方法
- 嵌套路由:学习了如何创建和使用嵌套路由
- 路由守卫:了解了如何使用全局和页面路由守卫
- 路由元信息:学习了如何定义和使用路由元信息
- 实战演练:通过构建博客路由系统巩固了所学知识
Nuxt.js的基于文件系统的路由系统是其最强大的特性之一,它使得路由管理变得简单直观,无需手动维护复杂的路由配置。通过本教程的学习,您已经掌握了Nuxt.js路由系统的核心概念和使用方法,可以开始构建复杂的单页应用了。
在接下来的教程中,我们将深入学习Nuxt.js的组件开发和使用,这是构建可维护、可复用的应用的关键部分。