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/profile

1.2 路由生成规则

Nuxt.js的路由生成遵循以下规则:

  1. 文件映射:每个.vue文件映射到一个路由
  2. 目录映射:每个目录也映射到一个路由段
  3. index.vue:目录中的index.vue文件映射到该目录的根路径
  4. 动态路由:使用[参数名].vue[参数名]目录表示动态路由
  5. 嵌套路由:通过目录嵌套实现路由嵌套

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 基本嵌套路由

创建嵌套路由需要遵循以下步骤:

  1. 创建一个目录,名称对应父路由的路径
  2. 在该目录中创建一个index.vue文件,作为父路由的页面
  3. 在该目录中创建其他.vue文件或子目录,作为子路由
src/pages/
├── dashboard/           # 父路由: /dashboard
│   ├── index.vue        # 父路由页面
│   ├── profile.vue      # 子路由: /dashboard/profile
│   └── settings.vue     # 子路由: /dashboard/settings

6.2 嵌套路由组件

在父路由页面中,需要使用&lt;NuxtPage&gt;组件来显示子路由的内容:

<!-- 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/posts

7. 路由守卫

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会按照以下优先级顺序选择路由:

  1. 静态路由(如/about.vue
  2. 动态路由(如/user/[id].vue
  3. 全匹配路由(如/[...slug].vue,用于捕获所有未匹配的路径)

10. 实战演练:构建博客路由系统

10.1 步骤1:创建基本目录结构

# 创建博客相关目录
mkdir -p src/pages/blog src/pages/admin/blog

10.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 问题:嵌套路由不显示

原因

  • 父路由页面中没有包含&lt;NuxtPage&gt;组件
  • 目录结构不符合嵌套路由要求

解决方案

  • 在父路由页面中添加&lt;NuxtPage&gt;组件
  • 确保目录结构正确,子路由文件应该放在父路由对应的目录中

9.3 问题:路由守卫不执行

原因

  • 路由守卫文件命名不正确
  • 路由守卫配置错误

解决方案

  • 全局路由守卫文件名应以global.开头
  • 页面路由守卫应使用definePageMeta函数配置

10. 小结

在本教程中,我们学习了Nuxt.js的页面创建和路由系统,包括:

  1. 基于文件系统的路由:了解了Nuxt.js如何根据文件和目录结构自动生成路由
  2. 页面创建方法:学习了如何创建基本页面和页面组件的结构
  3. 路由导航:掌握了使用NuxtLink组件和编程式导航的方法
  4. 路由参数传递:学习了如何使用查询参数和路径参数传递数据
  5. 动态路由:掌握了创建和使用动态路由的方法
  6. 嵌套路由:学习了如何创建和使用嵌套路由
  7. 路由守卫:了解了如何使用全局和页面路由守卫
  8. 路由元信息:学习了如何定义和使用路由元信息
  9. 实战演练:通过构建博客路由系统巩固了所学知识

Nuxt.js的基于文件系统的路由系统是其最强大的特性之一,它使得路由管理变得简单直观,无需手动维护复杂的路由配置。通过本教程的学习,您已经掌握了Nuxt.js路由系统的核心概念和使用方法,可以开始构建复杂的单页应用了。

在接下来的教程中,我们将深入学习Nuxt.js的组件开发和使用,这是构建可维护、可复用的应用的关键部分。

« 上一篇 Nuxt.js基本目录结构与文件组织 下一篇 » Nuxt.js组件开发与使用