创建自定义路由

为了理解 Nuxt 中路由的工作原理,我们首先应该理解它在 Vue 中的工作方式。然后我们才能理解如何在我们的 Nuxt 应用程序中实现它。在传统的 Vue 应用程序中,自定义路由是通过 Vue Router 创建的。所以,让我们首先了解一下 Vue Router 是什么。

Vue Router 简介

Vue Router 是一个 Vue 插件,它允许你为单页应用程序 (SPA) 创建强大的路由,以便在页面之间导航而无需刷新页面。一个简单的用例是,例如,如果我们想要一个 User 组件用于所有用户,但具有不同的用户 ID。你可以像这样使用这个组件:

const User = {
  template: '<div>User {{ $route.params.id }}</div>'
}

const router = new VueRouter({
  routes: [
    { path: '/user/:id', component: User }
  ]
})

在这个例子中,任何以 /user 开头并跟随一个 ID 的路由(例如,/user/1/user/2)都将被定向到 User 组件,该组件将使用该 ID 渲染模板。只有在安装了 Vue 插件后才能实现这一点,所以让我们在下一节中了解如何在 Vue 应用程序中安装它,然后再学习它在 Nuxt 应用程序中是如何工作的。

有关 Vue Router 的更多信息,请访问 https://router.vuejs.org/

安装 Vue Router

Vue 中,你必须显式地安装 Vue Router 才能在传统的 Vue 应用程序中创建路由。即使你使用 Vue CLI(我们将在第 11 章 “编写路由中间件和服务端中间件” 中介绍),你也必须选择 “Manually select features”(手动选择特性),然后从提示你选择的选项中选取 Router,以选择你需要的特性。因此,让我们在本节中看看如何手动安装它。有两种安装 Vue Router 的选项:

  • 你可以使用 npm

    $ npm install vue-router

    然后,在应用程序根目录中,通过 Vue.use() 显式地导入 vue-router

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    Vue.use(VueRouter)
  • 或者,你可以使用 CDN 或直接下载:

    <script src="/path/to/vue.js"></script>
    <script src="/path/to/vue-router.js"></script>

如果你使用 CDN,只需在 Vue 核心库之后添加 vue-router,其余的安装将自动完成。完成 Vue Router 的安装后,你就可以使用它来创建路由了。

使用 Vue Router 创建路由

好的,如果你使用 CDN 选项,首先在你的项目根目录下创建一个 .html 文件,包含以下基本的 HTML 结构,并在 <head> 标签中引入 CDN 链接:

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Vue App</title>
    <script src="https://unpkg.com/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
</head>
<body>
    <!-- 在这里添加应用内容 -->
    <div id="app">
        <!-- 你的Vue应用内容 -->
    </div>

    <script>
        // 在这里添加Vue和Vue Router的初始化代码
    </script>
</body>
</html>

之后,你可以通过以下步骤快速启动 Vue Router

  1. <body> 标签中创建应用的基础标记:

    <div id="app">
      <h1>Hello App!</h1>
      <p>
        <router-link to="/about">About</router-link>
        <router-link to="/contact">Contact</router-link>
      </p>
      <router-view></router-view>
    </div>
    <script type="text/javascript">
    //...
    </script>

    <router-link> 组件用于指定目标路径,并且将会渲染为带有 href 属性的 <a> 标签,而 <router-view> 组件用于渲染请求的内容,也就是我们将在下一步创建的 Vue 组件。

  2. <script> 标签中定义两个 Vue 组件:

    const About = { template: '<div>About</div>' }
    const Contact = { template: '<div>Contact</div>' }
  3. 创建一个名为 routes 的常量变量,并将 Vue 组件添加到 component 属性中,该属性的路径与 <router-link> 中的链接相匹配:

    const routes = [
      { path: '/about', component: About },
      { path: '/contact', component: Contact }
    ]
  4. 使用 new 操作符创建一个 router 实例,并将 routes 常量传入:

    const router = new VueRouter({
      routes
    })

    请注意,前面代码块中的 routeES6/ES2015routes: routes 的简写形式(简写属性名)。有关简写属性名的更多信息,请访问 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer

  5. 使用 new 操作符创建一个 Vue 实例,并将 router 实例传入,然后将 #app 元素挂载到根实例上:

    const app = new Vue({
      router
    }).$mount('#app')
  6. 在你的浏览器中运行该应用,你应该会在屏幕上看到 AboutContact 链接。当你导航到 /about/contact 时,你应该会看到它们的组件按预期成功渲染在你的屏幕上。

    你可以在我们的 GitHub 仓库的 /chapter-4/vue/vue-route/basic.html 中找到前面应用程序的代码,并在你喜欢的浏览器中运行它以查看其工作方式。

现在,让我们探讨一下 Nuxt 如何为我们通过 Vue Router 生成上述路由。在 Nuxt 中创建路由的过程更简单,因为 vue-routerNuxt 中是开箱即用的。这意味着从技术上讲,你可以跳过传统 Vue 应用程序中前面的安装步骤。你还可以跳过前面 JavaScript 步骤中的第 3 到第 5 步。Nuxt 将扫描 /pages/ 目录中的 .vue 文件树,并为你自动生成路由。因此,让我们探讨一下 Nuxt 如何为你创建和处理路由。我们将首先从创建基本路由开始。

创建基本路由

通过简单地将具有固定文件名的 .vue 文件添加到 /pages/ 目录,即可创建基本路由。你还可以通过将 .vue 文件组织到不同的文件夹中来创建子路由。

以下面的示例为例:

pages/
--| users/
-----| index.vue
-----| john-doe.vue
--| index.vue

然后,Nuxt 将为你生成以下路由,而无需你编写任何代码:

router: {
  routes: [
    {
      name: 'index',
      path: '/',
      component: 'pages/index.vue'
    },
    {
      name: 'users',
      path: '/users',
      component: 'pages/users/index.vue'
    },
    {
      name: 'users-john-doe',
      path: '/users/john-doe',
      component: 'pages/users/john-doe.vue'
    }
  ]
}

你可以在我们的 GitHub 仓库的 /chapter-4/nuxtuniversal/routing/basic-routes/ 中找到这个示例应用程序。

你应该在上一章中已经熟悉这些基本路由。这种类型的路由适用于顶级页面,例如 /about/contact/posts。但是,如果你的每个顶级页面都有多个子页面,并且这些子页面会随着时间的推移动态增加,那么你应该使用动态路由来处理这些子页面的路由。让我们在下一节中了解如何创建动态路由。

创建动态路由

当使用下划线时,Nuxt 会生成动态路由。在更复杂的应用中,动态路由非常有用且不可避免。因此,如果你想创建动态路由,只需创建一个以带前缀下划线开头的文件(或目录),后跟文件(或目录)的名称的 .vue 文件(或目录)。看下面的例子:

pages/
--| _slug/
-----| index.vue
--| users/
-----| _id.vue
--| index.vue

然后,你将从 Nuxt 获得以下路由,而无需编写任何代码:

router: {
  routes: [
    {
      name: 'index',
      path: '/',
      component: 'pages/index.vue'
    },
    {
      name: 'users-id',
      path: '/users/:id?',
      component: 'pages/users/_id.vue'
    },
    {
      name: 'slug',
      path: '/:slug',
      component: 'pages/_slug/index.vue'
    }
  ]
}

你可以在我们 GitHub 仓库的 /chapter-4/nuxtuniversal/routing/dynamic-routes/ 中找到这个示例应用。

动态路由非常适合共享相同布局的页面。例如,如果你的 /about/contact 路由使用相同的布局(这种情况不太可能),那么前面动态路由示例代码中的 /_slug/ 目录就是一个不错的选择。因此,就像 /users 路由下的子页面共享相同的布局一样,/_id.vue 文件的方法非常适合这种情况。

除了使用这种(简单的)动态路由为 /users 路由下的子页面创建子路由之外,我们还可以为它们使用更复杂的动态路由——嵌套路由。这种情况是指当渲染子页面时,你不希望父布局被子布局完全替换;换句话说,当你想在父布局内部渲染子页面时。让我们在下一节中了解如何实现这一点。

创建嵌套路由

从本质上讲,由嵌套组件生成的路由称为 嵌套路由。在某些情况下,您可能希望组合嵌套在其他组件(父组件)内的组件(子组件),并且您希望将这些子组件渲染在父组件的特定视图中,而不是让子组件替换父组件。

要在 Vue 应用程序中实现这一点,您需要在父组件中插入一个 <router-view> 组件来放置子组件。例如,假设您有一个 Users 父组件,并且当调用特定用户时,您希望将该用户的详细内容加载到此父组件内部。那么,您可以按照以下步骤为他们创建嵌套路由:

  1. 创建一个父组件:

    const Users = {
      template: `
        <div class="user">
          <h2>Users</h2>
          <router-link to="/user/1">1</router-link>
          <router-link to="/user/2">2</router-link>
          <router-link to="/user/3">3</router-link>
          <router-view></router-view>
        </div>
      `
    }

    如果将上述代码放在图表中,可以如下进行可视化解释:

    +-----------------------+
    |       users           |
    |   +---------------+   |
    |   |    1, 2, 3    |   |
    |   +---------------+   |
    |   +---------------+   |
    |   | <router-view> |   |
    |   +---------------+   |
    +-----------------------+
  2. 创建一个将显示单个用户内容或信息的子组件:

    const User = { template: '<div>User {{ $route.params.id }}</div>' }
  3. 使用 children 属性创建嵌套路由,如下所示:

    const routes = [
      {
        path: '/users',
        component: Users,
        children: [
          {
            path: ':id',
            component: User,
            name: 'user-id'
          }
        ]
      }
    ]
  4. 在将路由器注入 Vue 根实例之前,定义路由器实例并传入上述嵌套路由,如下所示:

    const router = new VueRouter({
      routes
    })
    
    const app = new Vue({
      router
    }).$mount('#app')

    然后,当点击子链接时,例如子链接 1/users/1 将被动态生成为它的路由,产生以下可视化结果:

    /users/1
    +-----------------------+
    |       users           |
    |   +---------------+   |
    |   |    1, 2, 3    |   |
    |   +---------------+   |
    |   +---------------+   |
    |   |    User 1     |   |
    |   +---------------+   |
    +-----------------------+
  5. 但是我们还没有完成,因为当还没有调用任何用户时,我们仍然需要处理 /users 中的空视图。因此,要解决这个问题,您将创建一个索引子组件,如下所示:

    const Index = { template: '<div>Users Index</div>' }
  6. 将上述索引组件添加到 children 块中,并在 path 键上使用空字符串 ''

    const routes = [
      {
        path: '/users',
        component: Users,
        children: [
          {
            path: '',
            component: Index,
            name: 'user-index'
          },
          //...
        ]
      }
    ]
  7. 所以现在,如果您在浏览器中导航到 /users,您应该会得到以下结果:

    /users
    +-----------------------+
    |       users           |
    |   +---------------+   |
    |   |    1, 2, 3    |   |
    |   +---------------+   |
    |   +---------------+   |
    |   |  Users Index  |   |
    |   +---------------+   |
    +-----------------------+

您可以看到,children 选项只是另一个路由配置对象数组,就像 routes 常量本身一样。因此,您可以根据需要保持视图的嵌套。但是,为了更好地维护,我们应该避免过深的嵌套,以使我们的应用程序尽可能简单。

您可以在我们的 GitHub 存储库的 /chapter-4/vue/vueroute/nested-route.html 中找到上述示例代码。

Nuxt 中也是一样的;您可以通过使用 Vue Router 的子路由来创建嵌套路由。如果要定义嵌套路由的父组件,您只需要创建一个与包含子视图的目录同名的 Vue 文件。看下面的例子:

pages/
--| users/
-----| _id.vue
-----| index.vue
--| users.vue

Nuxt 将自动生成以下路由:

router: {
  routes: [
    {
      path: '/users',
      component: 'pages/users.vue',
      children: [
        {
          path: '',
          component: 'pages/users/index.vue',
          name: 'users'
        },
        {
          path: ':id',
          component: 'pages/users/_id.vue',
          name: 'users-id'
        }
      ]
    }
  ]
}

您可以看到,Nuxt 生成的路由与您在 Vue 应用程序中创建的路由相同。请注意,在 Nuxt 中,我们在父组件(.vue 文件)中包含 <nuxt-child/>,而在 Vue 中,我们像前面的 User 示例一样,在父组件中包含 <router-view></router-view>。让我们通过一个与我们在 Vue 应用程序中做的类似的练习来更好地理解这一点:

  1. 创建一个带有 <nuxt-child/> 组件的父组件:

    // pages/users.vue
    <template>
      <div>
        <h1>Users</h1>
        <nuxt-child/>
      </div>
    </template>
  2. 创建一个索引子组件来保存用户列表:

    // pages/users/index.vue
    <template>
      <ul>
        <li v-for="user in users" v-bind:key="user.id">
          <nuxt-link :to="`users/${user.id}`">
            {{ user.name }}
          </nuxt-link>
        </li>
      </ul>
    </template>
    <script>
    import axios from 'axios'
    export default {
      async asyncData () {
        let { data } = await
        axios.get('https://jsonplaceholder.typicode.com/users')
        return { users: data }
      }
    }
    </script>

    请注意,我们将在本章的后续部分介绍 asyncData 方法,并在第 5 章 “添加 Vue 组件” 中介绍 axios,因此在此阶段不必担心它们。

  3. 创建一个包含返回子索引页面链接的单个子组件:

    // pages/users/_id.vue
    <template>
      <div v-if="user">
        <h2>{{ user.name }}</h2>
        <nuxt-link class="button" to="/users">
          Users
        </nuxt-link>
      </div>
    </template>
    
    <script>
    import axios from 'axios'
    export default {
      async asyncData ({ params }) {
        let { data } = await
        axios.get('https://jsonplaceholder.typicode.com/users/'
        + params.id)
        return { user: data }
      }
    }
    </script>

您可以看到,Nuxt 通过使用 children 属性(如前面 Vue 应用程序示例的步骤 3 所示)避免了您在 Vue 应用程序中必须配置嵌套路由的操作。

因此,在这个 Nuxt 应用程序中,当在其后渲染子页面时,users.vue 中的 <h1>Users</h1> 元素将始终可见。包含列表元素的 <ul> 元素将始终被子页面替换。如果父信息在子页面中保持不变,这非常有用,因为您不必在每次渲染子页面时都重新请求父信息。

您可以在我们的 GitHub 存储库的 /chapter-4/nuxtuniversal/routes/nested-routes/ 中找到此示例应用程序。

由于存在用于 “升级” 基本路由的动态路由,您可能会问,嵌套路由是否存在动态路由?从技术上讲,这是可能的,它们被称为 动态嵌套路由。因此,让我们在下一节中了解更多关于它们的信息。

创建动态嵌套路由

我们已经了解了动态路由和嵌套路由的工作方式,因此,从理论上和技术上讲,可以将这两种选项结合起来创建动态嵌套路由,方法是在动态父级(例如 _topic)中拥有动态子级(例如 _subTopic)。以下示例结构最好地说明了这一点:

pages/
--| _topic/
-----| _subTopic/
--------| _slug.vue
--------| index.vue
-----| _subTopic.vue
-----| index.vue
--| _topic.vue
--| index.vue

Nuxt 将自动生成以下路由:

router: {
  routes: [
    {
      path: '/',
      component: 'pages/index.vue',
      name: 'index'
    },
    {
      path: '/:topic',
      component: 'pages/_topic.vue',
      children: [
        {
          path: '',
          component: 'pages/_topic/index.vue',
          name: 'topic'
        },
        {
          path: ':subTopic',
          component: 'pages/_topic/_subTopic.vue',
          children: [
            {
              path: '',
              component: 'pages/_topic/_subTopic/index.vue',
              name: 'topic-subTopic'
            },
            {
              path: ':slug',
              component: 'pages/_topic/_subTopic/_slug.vue',
              name: 'topic-subTopic-slug'
            }
          ]
        }
      ]
    }
  ]
}

你可以看到,路由变得更加复杂,这可能会使你的应用程序更难通过阅读和尝试理解文件目录树来开发,因为它非常抽象,如果它变得 “更大”,在某些时候可能会过于抽象。始终将我们的应用程序设计和构建得尽可能简单是一个好的实践。以下示例路由是这种类型路由的一个很好的例子:

  • 一些 /topic/ 的示例包括:

    /science
    /arts
  • 一些 /topic/subTopic/ 的示例包括:

    /science/astronomy
    /science/geology
    /arts/architecture
    /arts/performing-arts
  • 一些 /topic/subTopic/slug.vue 的示例包括:

    /science/astronomy/astrophysics
    /science/astronomy/planetary-science
    /science/geology/biogeology
    /science/geology/geophysics
    /arts/architecture/interior-architecture
    /arts/architecture/landscape-architecture
    /arts/performing-arts/dance
    /arts/performing-arts/music

    你可以在我们的 GitHub 存储库的 /chapter-4/nuxt-universal/routing/dynamic-nested-routes/ 中找到这种类型路由的示例应用程序。

创建动态路由和页面总是需要在路由中包含参数(换句话说,路由参数),以便我们可以将它们(无论是 ID 还是 slug)传递给动态页面进行处理。但是在处理和响应参数之前,验证它们是一个好主意。因此,让我们在下一个主题中看看如何验证路由参数。

验证路由参数

你可以在组件中使用 validate 方法来校验动态路由的参数,在任何进一步的数据处理或异步获取之前。这个校验应该总是返回 true 以继续导航;如果 Nuxt 得到一个 false 的布尔值,它会停止路由并立即抛出一个 404 错误页面。例如,你想确保 ID 必须是一个数字:

// pages/users/_id.vue
export default {
  validate ({ params }) {
    return /^\d+$/.test(params.id)
  }
}

因此,如果你请求 localhost:3000/users/xyz 这个页面,你将会得到一个带有 "This page could not be found" 消息的 404 页面。如果你想自定义 404 消息,你可以使用 throw 语句抛出一个带有 Error 对象的异常,如下所示:

// pages/users/_id.vue
export default {
  validate ({ params }) {
    let test = /^\d+$/.test(params.id)
    if (test === false) {
      throw new Error('User not found')
    }
    return true
  }
}

你也可以在 validate 方法中使用 async 进行 await 操作:

async validate({ params }) {
  // ...
}

你也可以在 validate 方法中返回 Promise

validate({ params }) {
  return new Promise(...)
}

你可以在我们的 GitHub 仓库的 /chapter-4/nuxt-universal/routing/validate-route-params/ 中找到前面 ID 校验的示例应用。

校验路由参数是处理无效或未知路由的一种方式,但另一种处理方式是使用 _.vue 文件来捕获它们。所以,让我们在下一节中了解如何使用它。

使用 _.vue 文件处理未知路由

除了使用 validate 方法抛出通用的 404 页面之外,你还可以使用 _.vue 文件来抛出一个自定义的错误页面。让我们通过以下步骤来探索这是如何工作的:

  1. /pages/ 目录下创建一个空的 _.vue 文件,如下所示:

    pages/
    --| _.vue
    --| index.vue
    --| users.vue
    --| users/
    -----| _id.vue
    -----| index.vue
  2. 将任何自定义内容添加到这个 _.vue 文件中,如下所示:

    // pages/_.vue
    <template>
      <div>
        <h1>未找到</h1>
        <p>抱歉,您查找的页面未找到。</p>
      </div>
    </template>
  3. 启动应用程序并导航到以下路由,你将看到 Nuxt 将调用这个 _.vue 文件来处理任何不匹配正确路由的请求:

    • /company

    • /company/careers

    • /company/careers/london

    • /users/category/subject

    • /users/category/subject/type

  4. 如果你想在特定的层级(例如,仅在 /users 路由中)抛出一个更具体的 404 页面,那么在 /users/ 文件夹中创建另一个 _.vue 文件,如下所示:

    pages/
    --| _.vue
    --| index.vue
    --| users.vue
    --| users/
    -----| _.vue
    -----| _id.vue
    -----| index.vue
  5. 为这个 _.vue 文件添加自定义内容,如下所示:

    // pages/users/_.vue
    <template>
      <div>
        <h1>用户未找到</h1>
        <p>抱歉,您查找的用户未找到。</p>
      </div>
    </template>
  6. 再次导航到以下路由,你将看到 Nuxt 不再为不匹配的请求调用 /pages/_.vue 文件:

    • /users/category/subject

    • /users/category/subject/type 相反,Nuxt 现在调用 /pages/users/_.vue 文件来处理它们。

你可以在我们的 GitHub 仓库的 /chapter-4/nuxt-universal/routing/unknown-routes/ 中找到这个示例应用程序。

我们希望到目前为止,你应该知道如何以各种适合你应用程序的方式创建路由,但是路由和页面在 Nuxt 中是密不可分且不可分割的,因此你还需要知道如何创建 Nuxt 页面,它们是自定义视图。你将在下一个主题中学习如何做到这一点。