NuxtJS-Web-开发实用指南-二-

58 阅读27分钟

NuxtJS Web 开发实用指南(二)

原文:zh.annas-archive.org/md5/95454EEF6B1A13DFE0FAD028BE716A19

译者:飞龙

协议:CC BY-NC-SA 4.0

第二部分:视图、路由、组件、插件和模块

在本节中,我们将开始添加路由、页面、模板、组件、插件、模块和 Vue 表单,使我们的 Nuxt 应用程序变得更加复杂和有趣。

本节包括以下章节:

  • 第四章,添加视图、路由和过渡

  • 第五章,添加 Vue 组件

  • 第六章,编写插件和模块

  • 第七章,添加 Vue 表单

添加视图、路由和过渡

在前一章中,您为前端 UI 框架和库创建了一些简单的页面、路由,甚至布局,但它们只是非常基本的。因此,在本章中,我们将深入研究每一个,以及 Nuxt 中的模板。您将自定义默认模板和布局,并创建自定义模板。您还将学习如何自定义全局 meta 标签,并将特定的 meta 标签添加到应用程序的各个页面。如果信息是有用的。您将为页面过渡创建 CSS 和 JavaScript 过渡和动画。因此,在本章结束时,您将能够通过本章学到的知识以及在上一章中学到的知识,交付一个简单但完全功能的 Web 应用程序或网站(带有一些虚拟数据)。

本章我们将涵盖的主题如下:

  • 创建自定义路由

  • 创建自定义视图

  • 创建自定义过渡

第四章:创建自定义路由

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

介绍 Vue Router

Vue Router 是一个 Vue 插件,允许您在单页面应用程序(SPA)中创建强大的路由,而无需刷新页面即可在页面之间导航。一个快速的用法是,例如,如果我们想要一个用于所有用户但具有不同用户 ID 的User组件。您可以如下使用此组件:

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

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

在这个例子中,任何以/user开头的路由后跟一个 ID(例如,/user/1user/2)将被定向到User组件,该组件将使用该 ID 呈现模板。这仅在安装了 Vue 插件时才可能,因此让我们看看如何在下一节为 Vue 应用程序安装它,然后学习它在 Nuxt 应用程序中的工作原理。

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

安装 Vue Router

在 Vue 中,您必须显式安装 Vue Router 以在传统的 Vue 应用程序中创建路由。即使您使用 Vue CLI(我们将在第十一章中介绍编写路由中间件和服务器中间件),您也必须选择手动选择功能以从提示您选择的选项中选择 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>
    <script src="https://unpkg.com/vue/dist/vue.js"></script>
    <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
  </head>
  <body>
    //...
  </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 组件。

  1. <script>块中定义两个 Vue 组件:
const About = { template: '<div>About</div>' }
const Contact = { template: '<div>Contact</div>' }
  1. 创建一个名为routes的常量变量,并将 Vue 组件添加到component属性中,该属性与<router-link>中的链接匹配:
const routes = [
  { path: '/about', component: About },
  { path: '/contact', component: Contact }
]
  1. 使用new运算符创建路由实例,并传入routes常量:
const router = new VueRouter({
  routes
})

请注意,上述代码块中的route是 ES6/ES2015 中routes: routes的简写形式(简写属性名称)。有关简写属性名称的更多信息,请访问developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer

  1. 使用new运算符创建 Vue 实例,并传入router实例,然后将#app元素挂载到根实例:
const app = new Vue({
  router
}).$mount('#app')
  1. 在浏览器中运行应用程序,然后您应该在屏幕上看到关于和联系链接。当您导航到/about/contact时,您应该看到它们的组件如预期般成功呈现在屏幕上。

您可以在我们的 GitHub 存储库中的/chapter-4/vue/vue-route/basic.html中找到前面应用的代码,并在您喜欢的浏览器中运行它,以查看它的工作原理。

现在,让我们探索 Nuxt 如何通过 Vue Router 为我们生成前面的路由。在 Nuxt 中创建路由的过程更简单,因为vue-router在 Nuxt 中已经预装。这意味着从技术上讲,您可以跳过传统 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/nuxt-universal/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/nuxt-universal/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> | |
| +---------------+ |
+-------------------+
  1. 创建一个子组件,用于显示单个用户的内容或信息:
const User = { template: '<div>User {{ $route.params.id }}</div>' }
  1. 使用children属性创建嵌套路由,如下所示:
const routes = [
  {
    path: '/users',
    component: Users,
    children: [
      {
        path: ':id',
        component: User,
        name: 'user-id'
      }
    ]
  }
]
  1. 定义路由实例并传入前面的嵌套路由,然后将路由器注入 Vue 根实例:
const router = new VueRouter({
  routes
})

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

然后,当单击子链接时,前面的代码将生成以下可视化内容;例如,子编号1/users/1将动态生成为其路由:

/users/1
+-------------------+
| users             |
| +---------------+ |
| | 1, 2, 3       | |
| +---------------+ |
| +---------------+ |
| | User 1        | |
| +---------------+ |
+-------------------+
  1. 但我们还没有完成,因为当尚未调用任何用户时,我们仍然需要处理/users中的空视图。因此,为了解决这个问题,您将创建一个索引子组件,如下所示:
const Index = { template: '<div>Users Index</div>' }
  1. children块中添加前面的索引组件,使用空字符串''作为path键的值:
const routes = [
  {
    path: '/users',
    component: Users,
    children: [
      {
        path: '',
        component: Index,
        name: 'user-index'
      },
      //...
    ]
  }
]
  1. 现在,如果你在浏览器中导航到/users,你应该会得到以下结果:
/users
+-------------------+
| users             |
| +---------------+ |
| | 1, 2, 3       | |
| +---------------+ |
| +---------------+ |
| | Users Index   | |
| +---------------+ |
+-------------------+

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

你可以在我们的 GitHub 仓库的/chapter-4/vue/vue-route/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 中,我们在父组件中包含<router-view></router-view>,就像前面的User示例一样。让我们通过一个练习更好地理解这一点,就像我们在 Vue 应用中做的那样:

  1. 创建一个带有<nuxt-child/>组件的父组件:
// pages/users.vue
<template>
  <div>
    <h1>Users</h1>
    <nuxt-child/>
  </div>
</template>
  1. 创建一个索引子组件来保存用户列表:
// 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方法和第五章中的axios,所以现阶段不用担心它们。

  1. 创建一个单独的子组件,其中包含返回到子索引页面的链接:
// 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 已经帮你省去了在 Vue 应用中配置嵌套路由的步骤,使用了children属性(如前面 Vue 应用示例中的步骤 3所示)。

因此,在这个 Nuxt 应用中,当子页面渲染后,users.vue中的<h1>Users</h1>元素将始终可见。包含列表元素的<ul>元素将始终被子页面替换。如果父级信息在子页面中是持久的,这将非常有用,因为在子页面渲染时不需要重新请求父级信息。

你可以在我们的 GitHub 仓库的/chapter-4/nuxt-universal/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以继续前进;如果得到false布尔值,Nuxt 将停止路由并立即抛出 404 错误页面。例如,您希望确保 ID 必须是一个数字:

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

因此,如果您使用localhost:3000/users/xyz请求页面,您将收到一个带有“此页面找不到”消息的 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方法中使用return承诺:

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

你可以在/chapter-4/nuxt-universal/routing/validate-route-params/中找到前面示例应用程序的 ID 验证。

在我们的 GitHub 存储库中。

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

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

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

  1. /pages/目录中创建一个空的_.vue文件,如下所示:
pages/
--| _.vue
--| index.vue
--| users.vue
--| users/
-----| _id.vue
-----| index.vue
  1. 为这个_.vue文件添加任何自定义内容,如下所示:
// pages/_.vue
<template>
  <div>
    <h1>Not found</h1>
    <p>Sorry, the page you are looking for is not found.</p>
  </div>
</template>
  1. 启动应用程序并导航到以下路由,你会发现 Nuxt 将调用这个_.vue文件来处理这些请求,这些请求不匹配正确的路由级别:
/company
/company/careers
/company/careers/london
/users/category/subject
/users/category/subject/type
  1. 如果你想在特定级别上抛出一个更具体的 404 页面 - 例如,仅在/users路由中 - 那么在/users/文件夹中创建另一个_.vue文件,如下所示:
pages/
--| _.vue
--| index.vue
--| users.vue
--| users/
-----| _.vue
-----| _id.vue
-----| index.vue
  1. 为这个_.vue文件添加自定义内容,如下所示:
// pages/users/_.vue
<template>
  <div>
    <h1>User Not found</h1>
    <p>Sorry, the user you are looking for is not found.</p>
  </div>
</template>
  1. 再次导航到以下路由,你会发现 Nuxt 不再调用这个/pages/_.vue文件来处理不匹配的请求:
/users/category/subject
/users/category/subject/type

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

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

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

创建自定义视图

你在上面创建的自定义路由中的每个路由都将落在一个“页面”上,该页面具有我们希望在前端显示的所有 HTML 标记和内容。从软件架构的角度来看,这些 HTML 标记和内容,包括元信息、图像和字体,都是你的应用程序的视图或呈现层。在 Nuxt 中,我们可以轻松地创建和自定义我们的视图。让我们来了解一下 Nuxt 视图的组成部分以及如何自定义它。

了解 Nuxt 视图

Nuxt 中的视图结构包括应用程序模板、HTML 头部、布局和页面层。你可以使用它们来为应用程序路由创建视图。在一个更复杂的应用程序中,你可以使用来自 API 的数据填充它们,而在一个简单的应用程序中,你可以直接手动嵌入虚拟数据。我们将在接下来的章节中逐一介绍这些层。在深入了解之前,请花一点时间研究下面的图表,它将为你提供 Nuxt 视图的完整视图:

参考来源:nuxtjs.org/guide/views

你可以看到文档-HTML 文件是 Nuxt 视图的最外层,其后是布局、页面和可选的页面子层和 Vue 组件层。文档-HTML 文件是你的 Nuxt 应用程序的应用程序模板。让我们首先从这个最基本的层开始,学习如何在下一节中自定义它。

自定义应用程序模板

Nuxt 在幕后为你创建 HTML 应用程序模板,因此基本上你不必费心去创建它。然而,你仍然可以自定义它,比如添加脚本或样式,如果你想的话。默认的 Nuxt HTML 模板就是这么简单:

<!DOCTYPE html>
<html {{ HTML_ATTRS }}>
 <head>
 {{ HEAD }}
 </head>
 <body {{ BODY_ATTRS }}>
 {{ APP }}
 </body>
</html>

如果你想要更改或覆盖这个默认值,只需在根目录中创建一个app.html文件。看下面的例子:

// app.html
<!DOCTYPE html>
<!--[if IE 9]><html lang="en-US" class="lt-ie9 ie9" {{ HTML_ATTRS }}><![endif]-->
<!--[if (gt IE 9)|!(IE)]><!--><html {{ HTML_ATTRS }}><!--<![endif]-->
  <head>
    {{ HEAD }}
  </head>
  <body {{ BODY_ATTRS }}>
    {{ APP }}
  </body>
</html>

重新启动你的应用程序,你会看到你的自定义应用程序 HTML 模板已经替换了 Nuxt 的默认模板。

你可以在我们的 GitHub 存储库的/chapter-4/nuxt-universal/view/app-template/中找到这个例子。

接下来最接近 HTML 文档(即<html>元素)的一层是 HTML 头部,即<head>元素,其中包含重要的元信息以及页面的脚本和样式。我们不会直接在应用程序模板中添加或自定义这些数据,而是在 Nuxt 配置文件和/pages/目录中的文件中进行。因此,让我们在下一节中了解如何做到这一点。

创建自定义 HTML 头部

一个 HTML 的<head>元素由<title><style><link><meta>元素组成。手动添加这些元素可能是一项繁琐的任务。因此,Nuxt 会在你的应用程序中为你处理这些元素。在第二章中,开始使用 Nuxt,你学到了它们是由 Nuxt 从 JavaScript 对象中的数据生成的,这些数据是用花括号({})在 Nuxt 配置文件中编写的,如下所示:

// nuxt.config.js
export default {
  head: {
    title: 'Default Title',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: 'parent' }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
    ]
  }
}

在本主题中,我们对 Nuxt 配置文件中的meta块和/pages/目录中的页面感兴趣。Nuxt 使用 Vue Meta 插件来管理这些元属性。因此,要了解它在 Nuxt 中的工作原理,我们首先应该了解它在传统 Vue 应用程序中的工作原理。

介绍 Vue Meta

Vue Meta 是一个用于在 Vue 中管理和创建 HTML 元数据的 Vue 插件,具有内置的 Vue 响应性。您只需向任何 Vue 组件添加metaInfo特殊属性,它将自动呈现为 HTML 元标记,如下所示:

// Component.vue
export default {
  metaInfo: {
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' }
    ]
  }
}

上述的 JavaScript 代码块将被呈现为您页面中的以下 HTML 标签:

<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">

有关 Vue Meta 的更多信息,请访问vue-meta.nuxtjs.org/

您可以看到,您只需要在 JavaScript 对象中提供元数据。现在,让我们安装它并学习如何为 Vue 应用程序配置它。

安装 Vue Meta

像所有其他 Vue 插件一样,您可以通过以下步骤安装 Vue Meta 并将其连接到您的 Vue 应用程序中:

  1. 通过 npm 安装 Vue Meta:
$ npm i vue-meta

或者,您可以通过 CDN 使用<script>元素安装它,如下所示:

<script src="https://unpkg.com/vue-meta@1.5.8/lib/vue-meta.js"></script>
  1. 在您的主应用程序文件中使用 Vue Router 导入 Vue Meta,如果您正在编写一个 ES6 JavaScript 应用程序:
//main.js
import Vue from 'vue'
import Router from 'vue-router'
import Meta from 'vue-meta'

Vue.use(Router)
Vue.use(Meta)
export default new Router({
  //...
})
  1. 然后,您可以在任何 Vue 组件中使用它,如下所示:
// app.vue
var { data } = await axios.get(...)
export default {
  metaInfo () {
    return {
      title: 'Nuxt',
      titleTemplate: '%s | My Awesome Webapp',
      meta: [
        { vmid: 'description', name: 'description', content: 'My
          Nuxt portfolio' }
      ]
    }
  }
}

在这个例子中,因为我们使用axios来异步获取数据,我们必须使用metaInfo方法从异步数据中注入元信息,而不是使用metaInfo属性。您甚至可以使用titleTemplate选项为您的页面标题添加模板,就像前面的例子一样。接下来,我们将创建一个简单的 Vue 应用程序,并使用这个插件,以便您可以更全面地了解如何使用它。

在 Vue 应用程序中使用 Vue Meta 创建元数据

像往常一样,我们可以在单个 HTML 页面上启动和运行 Vue 应用程序。让我们开始吧:

  1. <head>块中包含 CND 链接:
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
<script src="https://unpkg.com/vue-meta@1.5.8/lib/vue-meta.js"></script>
  1. <script>块中创建以下带有元数据的组件:
const About = {
  name: 'about',
  metaInfo: {
    title: 'About',
    titleTemplate: null,
    meta: [
      { vmid: 'description', name: 'description', content: 'About 
        my Nuxt...' 
      }
    ]
  }
}
const Contact = {
  name: 'contact',
  metaInfo: {
    title: 'Contact',
    meta: [
      { vmid: 'description', name: 'description', content: 
       'Contact me...' }
    ]
  }
}
  1. 然后,在根实例中添加默认的元数据:
const app = new Vue({
  metaInfo: {
    title: 'Nuxt',
    titleTemplate: '%s | My Awesome Webapp',
    meta: [
      { vmid: 'description', name: 'description', content: 'My 
        Nuxt portfolio' 
      }
    ]
  },
  router
}).$mount('#app')

请注意,我们可以通过在子组件的titleTemplate选项中简单地添加null来覆盖组件中的默认元数据模板,就像前面的About组件一样。

您可以在我们的 GitHub 存储库的/chapter-4/vue/vue-meta/basic.html中找到此示例应用程序。

在此示例中,由于我们不使用axios异步获取数据,因此可以直接使用metaInfo属性,而不是使用metaInfo方法来使用异步数据注入 meta 信息。然后,当您在刚刚创建的路由周围导航时,您将看到页面标题和 meta 信息在浏览器中发生变化。在 Vue 应用程序中使用此插件非常容易,不是吗?现在,我们应该看看它在 Nuxt 应用程序中的工作原理。

自定义 Nuxt 应用程序中的默认 meta 标签

在 Nuxt 应用程序中创建和自定义 meta 信息更简单,因为 Vue Meta 默认包含在 Nuxt 中。这意味着您无需像在 Vue 应用程序中那样安装它。您只需在 Nuxt 配置文件中使用head属性来定义应用程序的默认<meta>标签,如下所示:

// nuxt.config.js
head: {
  title: 'Nuxt',
  titleTemplate: '%s | My Awesome Webapp',
  meta: [
    { charset: 'utf-8' },
    { name: 'viewport', content: 'width=device-width, initial-scale=1' },
    { hid: 'description', name: 'description', content: 'My 
      Nuxt portfolio' }
  ]
}

然而,Nuxt 和 Vue 应用程序之间的区别在于,在 Nuxt 中必须使用hid键,而在 Vue 中使用vmid。您应该始终使用hid来防止在子组件中定义它们(meta 标签)时重复 meta 标签的发生。另外,请注意,metaInfo键仅在 Vue 中使用,而title键在 Nuxt 中使用,用于添加我们的元信息。

这就是您如何为您的 Nuxt 应用程序添加和自定义标题和 meta 标签。但是,它们是全局添加的,这意味着它们适用于应用程序中的所有页面。那么,如何将它们特定地添加到页面并在 Nuxt 配置文件中覆盖全局的呢?让我们在下一节中找出答案。

为 Nuxt 页面创建自定义 meta 标签

如果要为特定页面添加自定义 meta 标签或在 Nuxt 配置文件中覆盖默认的 meta 标签,只需直接在该特定页面上使用head方法,该方法将返回一个包含titlemeta选项数据的 JavaScript 对象,如下所示:

// pages/index.vue
export default {
  head () {
    return {
      title: 'Hello World!',
      meta: [
        { hid: 'description', name: 'description', content: 'My 
          Nuxt portfolio' }
      ]
    }
  }
}

然后,您将获得此页面的输出:

<title data-n-head="true">Hello World! | My Awesome Webapp</title>
<meta data-hid="description" name="description" content="My Nuxt portfolio" data-n-head="true">

您可以在我们的 GitHub 存储库中的/chapter-4/nuxt-universal/view/html-head/中找到此示例应用程序。

所以,就是这样。这就是关于 Nuxt 中应用模板和 HTML 头的全部内容。Nuxt 视图中的下一个内部层是布局,我们将在下一节中指导您如何创建自定义布局。让我们开始吧。

创建自定义布局

布局是您的页面和组件的支柱。您可能希望在应用程序中拥有多个不同的布局。在使用npx create-nuxt-app脚手架工具安装应用程序时,/layouts/目录中会自动生成一个名为default.vue的布局。就像应用程序模板一样,您可以修改此默认布局或创建自己的自定义布局。

修改默认布局

默认布局始终用于没有特定或自定义布局的页面。如果您转到/layouts/目录并打开此布局,您应该看到其中只有三行代码用于呈现您的页面组件:

// layouts/default.vue
<template>
  <nuxt/>
</template>

让我们修改这个默认布局,如下所示:

// layouts/default.vue
<template>
  <div>
    <div>...add a navigation bar here...</div>
    <nuxt/>
  </div>
</template>

您应该看到您在那里添加的任何内容 - 例如,应用程序中所有页面上的导航栏。请注意,无论您是修改此布局还是创建新布局,都要确保在您想要 Nuxt 导入页面组件的地方有<nuxt/>组件。让我们在下一节中探讨如何创建自定义布局。

创建新的自定义布局

有时,我们需要更多的布局来构建更复杂的应用程序。我们可能需要为某些页面创建不同的布局。对于这种情况,您需要创建自定义布局。您可以使用.vue文件创建自定义布局,然后将它们放在/layouts/目录中。看下面的例子:

// layouts/about.vue
<template>
  <div>
    <div>...add an about navigation bar here....</div>
    <nuxt/>
  </div>
</template>

然后,您可以在页面组件中使用layout属性将此自定义布局分配给该页面,如下所示:

// pages/about.vue
export default {
  layout: 'about'
  // OR
  layout (context) {
    return 'about'
  }
}

现在,Nuxt 将使用/layouts/about.vue文件作为此页面组件的基本布局。但是用于显示未知和无效路由的错误页面的布局又是什么呢?让我们在下一节中找出这是如何制作的。

创建自定义错误页面

每个安装的 Nuxt 应用程序都带有一个默认错误页面,存储在@nuxt包中的/node_modules/目录中,Nuxt 用于显示错误,如 404、500 等。您可以通过在/layouts/目录中添加error.vue文件来自定义它。让我们通过以下步骤了解如何实现这一点:

  1. /layouts/目录中创建自定义错误页面,如下所示:
// layouts/error.vue
<template>
  <div>
    <h2 v-if="error.statusCode === 404">Page not found</h2>
    <h2 v-else>An error occurred</h2>
    <nuxt-link to="/">Home page</nuxt-link>
  </div>
</template>

<script>
export default {
  props: ['error']
}
</script>

请注意,错误页面是一个页面组件。起初,它似乎有违直觉和令人困惑,因为它放在/layouts/目录而不是/pages/目录中。但是,即使它在/layouts/目录中,它也应该被视为一个页面。

  1. 就像其他页面组件一样,您可以为此错误页面创建自定义布局,如下所示:
// layouts/layout-error.vue
<template>
  <div>
    <h1>Error!</h1>
    <nuxt/>
  </div>
</template>
  1. 然后,只需将layout-error添加到错误页面的layout选项中:
// layouts/error.vue
<script>
export default {
  layout: 'layout-error'
}
</script>
  1. 现在,如果您导航到任何以下未知路由,Nuxt 将调用此自定义错误页面和自定义错误布局:
/company
/company/careers
/company/careers/london
/users/category/subject
/users/category/subject/type

你可以在我们的 GitHub 存储库的/chapter-4/nuxt-universal/view/custom-layouts/404/中找到这个 404 示例。

就是这样。这就是 Nuxt 中关于布局的全部内容。Nuxt 视图中的下一个内部层是页面,您将在下一节中学习如何为您的应用程序创建自定义页面。所以,请继续阅读。

创建自定义页面

页面是 Nuxt 视图的一部分,就像应用程序模板、HTML 头部和布局一样,我们已经介绍过了。/pages/目录是您存储页面的地方。您将在这个目录中花费大部分时间来为您的 Nuxt 应用程序创建页面。但是,创建页面并不是什么新鲜事——在上一节中,我们在/layouts/目录中创建了一个简单的错误页面,并且在学习如何为我们的应用程序创建自定义路由时创建了许多页面。因此,当您想为特定路由创建自定义页面时,只需在/pages/目录中创建一个.vue文件;例如,我们可以创建以下页面:

pages/
--| index.vue
--| about.vue
--| contact.vue

但是,创建自定义页面需要更多。我们需要了解 Nuxt 中附带的页面属性和函数。尽管页面是 Nuxt 应用程序开发的重要部分,但在 Vue 应用程序开发中并没有强调,它们与 Vue 组件密切相关,并且与组件的工作方式有些不同。因此,要创建页面并充分利用它,我们首先需要了解 Nuxt 中的页面是什么。让我们找出来。

理解页面

页面本质上是一个 Vue 组件。它与标准 Vue 组件的区别在于仅在 Nuxt 中添加的属性和函数。我们使用这些特殊的属性和函数在呈现页面之前设置或获取数据,如下所示:

<template>
  <p>{{ message }}!</p>
</template>

<script>
export default {
  asyncData (context) {
    return { message: 'Hello World' }
  }
}
</script>

在前面的示例中,我们使用了一个名为asyncData的函数来设置消息键中的数据。这个asyncData函数是您在 Nuxt 应用程序中经常看到并经常使用的函数之一。让我们深入了解专门为 Nuxt 页面设计的属性和函数。

asyncData 方法

asyncData方法是页面组件中最重要的函数。Nuxt 总是在初始化页面组件之前调用这个函数。这意味着每次请求页面时,这个函数都会在页面渲染之前首先被调用。它以 Nuxt 上下文作为第一个参数,并且可以异步使用,如下所示:

<h1>{{ title }}</h1>

export default {
  async asyncData ({ params }) {
    let { data } = await axios.get(
    'https://jsonplaceholder.typicode.com/posts/' + params.id)
    return { title: data.title }
  }
}

在这个例子中,我们使用 ES6 解构赋值语法来解开 Nuxt 上下文中打包的属性,而这个特定的属性是params。换句话说,

{ params }context.params的简写。我们还可以使用解构赋值语法来解开从axios的异步结果中的data属性。请注意,如果在页面组件的data函数中设置了数据集,它将始终与asyncData中的数据合并。然后,合并后的数据可以在<template>块中使用。让我们创建一个简单的示例来演示asyncData如何与data函数合并:

<h1>{{ title }}</h1>

export default {
  data () {
    return { title: 'hello world!' }
  },
  asyncData (context) {
    return { title: 'hey nuxt!' }
  }
}

dataasynData方法返回的数据对象有两组,但是你将得到以下代码的输出:

<h1>hey nuxt!</h1>

你可以看到,如果它们都使用相同的数据键,asyncData函数中的数据将始终替换data函数中的数据。另外,请注意,我们不能在asyncData方法中使用this关键字,因为这个方法在页面组件初始化之前被调用。因此,你不能使用这个方法来更新数据,比如this.title = data.title。我们将在第八章中更详细地介绍asyncData添加服务器端框架

有关解构赋值的更多信息,请访问developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment

fetch 方法

fetch方法与asyncData方法的工作方式相同,只是它在createdVue 生命周期钩子之后被调用——换句话说,在组件初始化之后。与asyncData方法一样,它也可以异步使用;例如,你也可以使用它来设置页面组件中的数据。

// pages/users/index.vue
<li v-for="user in users" v-bind:key="user.id">
  <nuxt-link :to="`users/${user.id}`">
    {{ user.name }}
  </nuxt-link>
</li>

import axios from 'axios'
export default {
  data () {
    return { users: [] }
  },
  async fetch () {
    let { data } = await axios.get
    ('https://jsonplaceholder.typicode.com/users')
    this.users = data
  }
}

请注意,data方法必须与fetch方法一起使用来设置数据。由于它在页面组件初始化后调用,我们可以使用this关键字来访问data方法中的对象。我们还可以使用这个方法从页面组件中将数据设置到 Vuex 存储中,如下所示:

// pages/posts/index.vue
<li v-for="post in $store.state.posts" v-bind:key="post.id">
  <nuxt-link :to="`posts/${post.id}`">
    {{ post.title }}
  </nuxt-link>
</li>

import axios from 'axios'
export default {
  async fetch () {
    let { data } = await axios.get(
    'https://jsonplaceholder.typicode.com/posts')
    const { store } = this.$nuxt.context
    store.commit('setPosts', data)
  }
}

我们将在第十章中更详细地介绍与 Vuex 存储一起使用的fetch方法,添加一个 Vuex 存储

您可以在我们的 GitHub 存储库的/chapter-4/nuxt-universal/view/custom-pages/fecth-method/部分找到上述代码。

有关fetch方法的更多信息,请访问nuxtjs.org/api/pages-fetchnuxtjs.org/blog/understanding-how-fetch-works-in-nuxt-2-12/

头部方法

head方法用于在页面上设置<meta>标签,我们在创建自定义 HTML 头部部分中已经介绍过。它也可以与/components/目录中的组件一起使用。

布局属性

layout键(或属性)用于指定页面在/layouts/目录中的布局,我们在创建自定义布局部分中已经介绍过。

加载属性

loading属性允许您禁用默认的加载进度条或在特定页面上设置自定义加载进度条。我们在第二章中简要介绍过,因此我们知道可以在 Nuxt 配置文件中配置全局默认加载组件,如下所示:

// nuxt.config.js
export default {
  loading: {
    color: '000000'
  }
}

然而,由于我们在localhost上,并且不需要太多时间来处理数据,通常我们不会看到这个加载条在工作中。为了看到它的工作情况,让我们演示一下这个加载组件是如何工作和看起来的,通过延迟组件中数据的加载时间来演示(但请注意,这个演示不应该在生产环境中进行):

  1. /pages/目录中创建一个名为index.vue的页面,其中包含以下代码:
// pages/index.vue
<template>
  <div class="container">
    <p>Hello {{ name }}!</p>
    <NuxtLink to="/about">
      Go to /about
    </NuxtLink>
  </div>
</template>

<script>
export default {
  asyncData () {
    return new Promise((resolve) => {
      setTimeout(function () {
        resolve({ name: 'world' })
      }, 1000)
    })
  }
}
</script>
  1. /pages/目录中创建另一个名为about.vue的页面,其中包含以下代码:
// pages/about.vue
<template>
  <div class="container">
    <p>About Page</p>
    <NuxtLink to="/">
      Go to /
    </NuxtLink>
  </div>
</template>

<script>
export default {
  asyncData () {
    return new Promise((resolve) => {
      setTimeout(function () {
        resolve({})
      }, 1000)
    })
  }
}
</script>

在这两个页面中,我们使用setTimeout来延迟 1 秒钟的数据响应。因此,当在页面之间导航时,您应该在请求页面加载之前看到顶部出现黑色加载条。

您可以在我们的 GitHub 存储库中的/chapter-4/nuxt-universal/view/custom-pages/loading-page/中找到此示例。

  1. 当然,我们可以通过在/components/目录中创建组件来创建自定义加载栏或层。参考以下示例:
// components/loading.vue
<template>
  <div v-if="loading" class="loading-page">
    <p>Loading...</p>
  </div>
</template>

<script>
export default {
  data () {
    return { loading: false }
  },
  methods: {
    start () { this.loading = true },
    finish () { this.loading = false },
  }
}
</script>

<style scoped>
.loading-page {
  position: fixed;
  //...
}
</style>

请注意,自定义加载组件中必须暴露startfinish方法,以便 Nuxt 在路由更改时调用您的组件并使用这些方法(调用start方法)并加载(调用finish方法)。

因此,在这个组件中,加载元素始终是隐藏的,因为data方法中默认将loading属性设置为false。只有在路由更改时loading属性设置为true时,加载元素才会变为可见。然后在路由完成加载后,loading属性再次设置为false时,它会再次隐藏。

有关这些和其他可用方法的更多信息,请访问nuxtjs.org/api/configuration-loading

  1. 在 Nuxt 配置文件的loading属性中包含前面自定义组件的路径:
// nuxt.config.js
export default {
  loading: '~/components/loading.vue'
}

您可以在我们的 GitHub 存储库中的/chapter-4/nuxt-universal/view/custom-pages/loading-global-custom/中找到此示例。

  1. 我们还可以根据特定页面配置加载行为,如下所示:
// pages/about.vue
export default {
  loading: false
}
  1. 如果页面上loading键的值为false,它将自动停止调用this.$nuxt.$loading.finish()this.$nuxt.$loading.start()方法,这允许您在脚本中手动控制它们,如下所示:
// pages/about.vue
<span class="link" v-on:click="goToFinal">
  click here
</span>

export default {
  loading: false,
  mounted () {
    setTimeout(() => {
      this.$nuxt.$loading.finish()
    }, 5000)
  },
  methods: {
    goToFinal () {
      this.$nuxt.$loading.start()
      setTimeout(() => {
        this.$router.push('/final')
      }, 5000)
    }
  }
}

  1. 然后,在/pages/目录中创建final.vue页面:
// pages/final.vue
<template>
  <div class="container">
    <p>Final Page</p>
    <NuxtLink to="/">
      Go to /
    </NuxtLink>
  </div>
</template>

在这个例子中,您可以看到您可以使用this.$nuxt.$loading.finish()this.$nuxt.$loading.start()手动控制加载栏。加载栏在mounted方法中需要 5 秒钟才能完成。当您触发goToFinal方法时,加载栏立即开始,并且需要 5 秒钟才能将路由更改为/final

您可以在我们的 GitHub 存储库中的/chapter-4/nuxt-universal/view/custom-pages/loading-page/中找到此示例。

过渡属性

transition属性用于指定页面的过渡。您可以使用字符串、对象或带有此键的函数,如下所示:

// pages/about.vue
export default {
  transition: ''
  // or
  transition: {}
  // or
  transition (to, from) {}
}

我们将在本章后面的创建自定义过渡部分深入介绍transition属性。

scrollToTop 属性

scrollToTop键用于在呈现页面之前使嵌套路由中的页面从顶部开始。默认情况下,当您转到另一个页面时,Nuxt 会滚动到顶部,但在嵌套路由的子页面上,Nuxt 会保持在前一个子路由的相同滚动位置。因此,如果您希望告诉 Nuxt 在这些页面上滚动到顶部,则将scrollToTop设置为true,如下所示:

// pages/users/_id.vue
export default {
  scrollToTop: true
}

验证方法

validate方法是动态路由的验证器,我们已经在验证路由参数部分中进行了介绍。

中间件属性

middleware属性用于指定页面的中间件。分配的中间件将始终在页面呈现之前执行,如下所示:

// pages/secured.vue
export default {
  middleware: 'auth'
}

在此示例中,auth是您将在/middleware/目录中创建的中间件的文件名,如下所示:

// middleware/auth.js
export default function ({ route }) {
  //...
}

我们将在第十一章中深入讨论中间件,编写路由中间件和服务器中间件

所以,就是这样。您已经完成了关于 Nuxt 视图的部分,从应用程序模板、HTML 头部和布局到页面。干得好!我们将在下一章中讨论 Vue 组件。但现在,我们应该看的下一件事是在 Nuxt 中创建自定义页面之间的过渡,因为过渡和页面是密切相关的,就像您之前简要介绍过的页面transition属性一样。所以,让我们继续进行本章的最后一个主题,您将在其中了解如何创建自定义过渡。

创建自定义过渡

到目前为止,您已经成功为 Nuxt 应用程序创建了多个路由和页面,并在页面之间切换时添加了一个加载栏。这已经使得应用程序看起来相当不错。但这并不是您在 Nuxt 中可以做的全部。您可以在页面之间添加更多令人惊叹的效果和过渡。这就是页面中的transition属性(例如,/pages/about.vue)以及 Nuxt 配置文件中的pageTransitionlayoutTransition选项的用处。

我们可以通过 Nuxt 配置文件全局应用过渡,或者特定应用于某些页面。我们将指导您了解这个主题。但是,要理解 Nuxt 中过渡的工作原理,我们首先应该了解它在 Vue 中是如何工作的,然后我们可以学习如何在路由更改时在我们的页面上实现它。让我们开始吧。

理解 Vue 过渡

Vue 依赖于 CSS 过渡,并使用<transition>Vue 组件来包裹 HTML 元素或 Vue 组件以添加 CSS 过渡,如下所示:

<transition>
  <p>hello world</p>
</transition>

你可以看到这有多么容易 - 你可以像吃蛋糕一样轻松地用<transition>组件包裹任何元素。当这种情况发生时,Vue 会在适当的时间添加和移除以下 CSS 过渡类到该元素:

  • .v-enter.v-leave类定义了过渡开始之前你的元素的外观。

  • .v-enter-to.v-leave-to类是你的元素的“完成”状态。

  • .v-enter-active.v-leave-active类是元素的活动状态。

这些类是 CSS 过渡发生的地方。例如,在 HTML 页面中进行的过渡可能如下所示:

.element {
  opacity: 1;
  transition: opacity 300ms;
}
.element:hover {
  opacity: 0;
}

如果我们将前面的过渡转换为 Vue 上下文,我们将得到以下结果:

.v-enter,
.v-leave-to {
  opacity: 0;
}
.v-leave,
.v-enter-to {
  opacity: 1;
}
.v-enter-active,
.v-leave-active {
  transition: opacity 300ms;
}

我们可以将这些 Vue 过渡类简单地可视化为以下图表:

参考来源:vuejs.org/v2/guide/transitions.html

Vue 默认使用v-作为过渡类的前缀,但如果你想更改这个前缀,只需在<transition>组件上使用name属性来指定一个名称 - 例如,<transition name="fade">;然后,你可以"重构"你的 CSS 过渡类,如下所示:

.fade-enter,
.fade-leave-to {
  opacity: 0;
}
.fade-leave,
.fade-enter-to {
  opacity: 1;
}
.fade-enter-active,
.fade-leave-active {
  transition: opacity 300ms;
}

让我们将前面的过渡应用到一个简单的 Vue 应用程序中,步骤如下:

  1. 创建两个简单的路由,并用<transition>组件包裹<router-view>组件,如下所示:
<div id="app">
  <p>
    <router-link to="/about">About</router-link>
    <router-link to="/contact">Contact</router-link>
  </p>
  <transition name="fade" mode="out-in">
    <router-view></router-view>
  </transition>
</div>
  1. 添加一个带有前缀fade-的 CSS 过渡类的<style>块:
<style type="text/css">
  .fade-enter,
  //...
</style>

当你在浏览器上运行应用程序时,你会发现在切换路由时,路由组件淡入淡出需要 300 毫秒。

你可以在我们的 GitHub 存储库的/chapter-4/vue/transitions/basic.html中找到这个例子。

你可以看到,过渡需要一些 CSS 类来使其工作,但对于 Vue 应用程序来说,掌握它们并不难。现在,让我们看看如何在下一节中在 Nuxt 中应用过渡。

使用 pageTransition 属性进行过渡

在 Nuxt 中,不再需要<transition>组件。它会默认为您添加,所以您只需在/assets/目录或任何特定页面的<style>块中创建过渡效果。在 Nuxt 配置文件中使用pageTransition属性来设置页面过渡的默认属性。Nuxt 中过渡属性的默认值如下:

{
  name: 'page',
  mode: 'out-in'
}

因此,Nuxt 默认使用page-作为过渡类的前缀,而 Vue 使用v-作为前缀。Nuxt 中默认的过渡模式设置为out-in。让我们通过以下步骤来看看 Nuxt 中的过渡是如何实现的:为所有页面创建全局过渡,以及为特定页面创建局部过渡。

  1. /assets/目录中创建一个transition.css文件,并添加以下过渡效果:
// assets/css/transitions.css
.page-enter,
.page-leave-to {
  opacity: 0;
}
.page-leave,
.page-enter-to {
  opacity: 1;
}
.page-enter-active,
.page-leave-active {
  transition: opacity 300ms;
}
  1. 将前面的 CSS 过渡资源的路径添加到 Nuxt 配置文件中:
// nuxt.config.js
export default {
  css: [
    'assets/css/transitions.css'
  ]
}
  1. 请记住,默认前缀是page-,所以如果您想使用不同的前缀,可以在 Nuxt 配置文件中使用pageTransition属性来更改前缀:
// nuxt.config.js
export default {
  pageTransition: 'fade'
  // or
  pageTransition: {
    name: 'fade',
    mode: 'out-in'
  }
}
  1. 然后,在transitions.css中将所有默认类名的前缀更改为fade,如下所示:
// assets/css/transitions.css
.fade-enter,
.fade-leave-to {
  opacity: 0;
}

当路由改变时,这个例子将在所有页面上全局应用过渡效果。

  1. 然而,如果我们想要为特定页面应用不同的过渡效果,或者在页面中覆盖全局过渡效果,可以在该页面的transition属性中进行设置,如下所示:
// pages/about.vue
export default {
  transition: {
    name: 'fade-about',
    mode: 'out-in'
  }
}
  1. 然后,在transitions.css中为fade-about创建 CSS 过渡效果:
// assets/css/transitions.css
.fade-about-enter,
.fade-about-leave-to {
  opacity: 0;
}
.fade-about-leave,
.fade-about-enter-to {
  opacity: 1;
}
.fade-about-enter-active,
.fade-about-leave-active {
  transition: opacity 3s;
}

在这个例子中,淡入和淡出about页面需要 3 秒,而其他页面只需要 300 毫秒。

您可以在我们的 GitHub 存储库的/chapter-4/nuxt-universal/transition/page-transition-property/中找到这个特定页面的例子和全局例子。

您可以看到,Nuxt 再次为您解决了一些重复的任务,并为您提供了创建自定义前缀类名的灵活性。而且,您甚至可以创建布局之间的过渡效果!让我们在下一节中了解如何做到这一点。

使用layoutTransition属性进行过渡

CSS 过渡不仅适用于页面组件,还适用于布局。layoutTransition属性的默认值如下:

{
  name: 'layout',
  mode: 'out-in'
}

因此,默认情况下布局转换类的前缀是layout,默认转换模式是out-in。让我们看看如何通过以下步骤为所有布局创建全局转换:

  1. /layouts/目录中创建about.vueuser.vue布局,如下所示:
// layouts/about.vue
<template>
  <div>
    <p>About layout</p>
    //...
    <nuxt />
  </div>
</template>
  1. 将前述布局应用于/pages/目录中的about.vueusers.vue页面,如下所示:
// pages/about.vue
<script>
export default {
  layout: 'about'
}
</script>
  1. /assets/目录中创建transition.css文件,并将以下转换添加到其中:
// assets/css/transitions.css
.layout-enter, 
.layout-leave-to {
  opacity: 0;
}
.layout-leave,
.layout-enter-to {
  opacity: 1;
}
.layout-enter-active,
.layout-leave-active {
  transition: opacity .5s;
}
  1. 将前面的 CSS 转换资源的路径添加到 Nuxt 配置文件中:
// nuxt.config.js
export default {
  css: [
    'assets/css/transitions.css'
  ]
}
  1. 默认前缀是layout-,但如果您想使用不同的前缀,可以在 Nuxt 配置文件中使用layoutTransition属性进行更改:
// nuxt.config.js
export default {
  layoutTransition: 'fade-layout'
  // or
  layoutTransition: {
    name: 'fade-layout',
    mode: 'out-in'
  }
}
  1. transitions.css中将所有默认类名的前缀更改为fade-layout,如下所示:
// assets/css/transitions.css
.fade-layout-enter,
.fade-layout-leave-to {
  opacity: 0;
}

在此示例中,淡入淡出整个布局(包括导航)需要 0.5 秒。当您在使用不同布局的页面之间导航时,您将看到此转换,但不会在使用相同布局的页面上看到;例如,如果您在//contact之间导航,您将不会得到前面的布局转换,因为它们都使用相同的布局,即/layouts/default.vue

您可以在我们的 GitHub 存储库中的/chapter-4/nuxt-universal/transition/layout-transition-property/中找到此示例。

再次,您可以看到为布局创建转换非常容易,并且可以自定义其前缀类名称,就像页面转换一样。除了使用 CSS 转换来转换页面和布局之外,我们还可以使用 CSS 动画。因此,让我们在下一节中了解一下。

使用 CSS 动画进行转换

CSS 转换仅在两个状态之间执行动画:开始和结束。但是,当您需要更多中间状态时,应该使用 CSS 动画,以便通过在开始和结束状态之间添加多个百分比的关键帧来获得更多控制。请看以下示例:

@keyframes example {
  0% { // 1st keyframe or start state.
    background-color: red;
  }
  25% { // 2nd keyframe.
    background-color: yellow;
  }
  50% { // 3rd keyframe.
    background-color: blue;
  }
  100% { // 4th keyframe end state.
    background-color: green;
  }
}

0%是动画的起始状态,而100%是结束状态。您可以通过添加增量百分比(例如10%20%30%等)在这两个状态之间添加更多中间状态。但是,CSS 转换没有这种添加关键帧的能力。因此,我们可以说 CSS 转换是 CSS 动画的简单形式。

由于 CSS 过渡实际上是“实际上”是 CSS 动画,我们可以在 Vue/Nuxt 应用程序中应用 CSS 动画,就像我们应用 CSS 过渡一样。让我们通过以下步骤了解如何做到这一点:

  1. 将以下 CSS 动画代码添加到transitions.css文件中,就像您在上一节中所做的那样:
// assets/css/transitions.css
.bounce-enter-active {
  animation: bounce-in .5s;
}
.bounce-leave-active {
  animation: bounce-in .5s reverse;
}
@keyframes bounce-in {
  0% {
    transform: scale(0);
  }
  50% {
    transform: scale(1.5);
  }
  100% {
    transform: scale(1);
  }
}
  1. 在 Nuxt 配置文件中将全局默认的page-前缀更改为bounce-
// nuxt.config.js
export default {
  pageTransition: 'bounce'
}

一旦您添加了上述代码,请刷新您的浏览器,您将会看到在页面之间切换时页面会弹入和弹出。

您可以在我们的 GitHub 存储库的/chapter-4/nuxt-universal/transition/css-animations/中找到这个示例。

根据您想要进行动画的复杂程度和详细程度,以及您的 CSS 动画技能水平,您可以为页面和布局创建非常令人惊叹的过渡效果。您只需专注于编写代码并通过 Nuxt 配置文件注册它,然后 Nuxt 将负责在适当的时间添加和删除 CSS 动画类的其余工作。但 JavaScript 呢?我们可以使用 jQuery,或者任何其他 JavaScript 动画库来创建页面和布局的过渡动画吗?答案是肯定的,您可以。让我们在下一节中了解如何做到这一点。

使用 JavaScript 钩子进行过渡

除了使用 CSS 进行过渡,您还可以通过在 Vue 应用程序的<transition>组件中添加以下钩子来使用 JavaScript 进行过渡:

<transition
  v-on:before-enter="beforeEnter"
  v-on:enter="enter"
  v-on:after-enter="afterEnter"
  v-on:enter-cancelled="enterCancelled"
  v-on:before-leave="beforeLeave"
  v-on:leave="leave"
  v-on:after-leave="afterLeave"
  v-on:leave-cancelled="leaveCancelled"
>
  //..
</transition>

请注意,您也可以在开头不添加v-on的情况下声明钩子。因此,将钩子写为:before-enter与写v-on:before-enter是相同的。

然后,在 JavaScript 方面,您应该在methods属性中有以下默认方法,以对应上述的钩子:

methods: {
  beforeEnter (el) { ... },
  enter (el, done) {
    // ...
    done()
  },
  afterEnter (el) { ... },
  enterCancelled (el) { ... },
  beforeLeave (el) { ... },
  leave (el, done) {
    // ...
    done()
  },
  afterLeave (el) { ... },
  leaveCancelled (el) { ... }
}

您可以单独使用这些 JavaScript 钩子,也可以与 CSS 过渡一起使用。如果您单独使用它们,则必须在enterleave钩子(方法)中使用done回调,否则这两个方法将同步运行,并且您尝试应用的动画或过渡将立即结束。此外,如果它们单独使用,您还应该在<transition>包装器上使用v-bind:css="false",这样 Vue 将安全地忽略您的元素,以防万一您的应用程序中也有 CSS 过渡,但它正在用于其他元素。让我们通过以下步骤创建一个简单的 Vue 应用程序,使用这些 JavaScript 钩子:

  1. 将以下 CDN 链接添加到 HTML 的<head>块中:
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
  1. <body>块中添加应用标记和<transition>组件与钩子:
<div id="app">
  <p>
    <router-link to="/about">About</router-link>
    <router-link to="/contact">Contact</router-link>
  </p>
  <transition
    appear
    v-bind:css="false"
    v-on:before-enter="beforeEnter"
    v-on:enter="enter"
    v-on:leave="leave"
    v-on:after-leave="afterLeave">
    <router-view></router-view>
  </transition>
</div>
  1. 接下来使用以下方法在<script>块中协调前面的钩子:
const app = new Vue({
  name: 'App',
  methods: {
    beforeEnter (el) { $(el).hide() },
    enter (el, done) {
      $(el).fadeTo('slow', 1)
      done()
    },
    leave (el, done) {
      $(el).fadeTo('slow', 0, function () {
        $(el).hide()
      })
      done()
    },
    afterLeave (el) { $(el).hide() }
  }
}).$mount('#app')

在这个例子中,我们使用 jQuery 的fadeTo方法来控制过渡,而不是使用纯 CSS。当在路由组件之间切换时,您应该看到它们淡入淡出,就像.v-enter.v-leaveCSS 过渡一样。

您可以在我们的 GitHub 存储库的/chapter-4/vue/transition/js-hooks.html中找到这个例子。

在 Nuxt 中,我们不需要将 JavaScript 钩子定义为<transition>组件,只需要在 Nuxt 配置文件的pageTransition中定义 JavaScript 方法,以及在/pages/目录中的任何.vue文件中定义transition。让我们在 Nuxt 应用程序中创建一个快速示例,步骤如下:

  1. 在终端上通过 npm 安装 jQuery:
$ npm i jquery
  1. 由于我们在 Nuxt 配置文件和其他页面中使用 jQuery,我们可以通过 Nuxt 配置文件在 webpack 中全局加载 jQuery:
// nuxt.config.js
import webpack from 'webpack'

export default {
  build: {
    plugins: [
      new webpack.ProvidePlugin({
        $: "jquery"
      })
    ]
  }
}
  1. 在 Nuxt 配置文件的pageTransition选项中使用 jQuery 创建全局过渡:
// nuxt.config.js
export default {
  pageTransition: {
    mode: 'out-in',
    css: false,
    beforeEnter: el => { $(el).hide() },
    enter: (el, done) => {
      $(el).fadeTo(1000, 1)
      done()
    },
    //...
  }
}

当路由更改时,此示例将在所有页面全局应用过渡。此外,我们通过将css选项设置为false来关闭 CSS 过渡。

请注意,我们将 JavaScript 函数写为对象键的替代方法,以便与过渡组件中的属性钩子关联。

  1. /pages/目录中创建一个about.vue页面,并通过about.vue页面上的transition属性应用不同的过渡,以覆盖前面的全局过渡:
// pages/about.vue
export default {
  transition: {
    mode: 'out-in',
    css: false,
    beforeEnter: el => { $(el).hide() },
    enter: (el, done) => {
      $(el).fadeTo(3000, 1)
      done()
    },
    //...
  }
}

因此,在这个例子中,这个特定页面的过渡需要 3 秒钟,而其他页面只需要 1 秒钟。

请注意,如果在 Nuxt 配置文件中未加载 jQuery,则必须将 jQuery 导入.vue页面;例如,假设您只想在这个特定页面上设置过渡:

// pages/about.vue
import $ from 'jquery'

export default {
  transition: {
    beforeEnter (el) { $(el).hide() },
    //...
  }
}

一旦代码就位,刷新您的浏览器,您应该看到页面在页面路由更改时淡入淡出,就像 Vue 应用程序在页面之间切换时一样。

您可以在我们的 GitHub 存储库的/chapter-4/nuxt-universal/transition/js-hooks/中找到这个例子。

干得好;您已经完成了在 Nuxt 中创建过渡的部分!您可以看到 JavaScript 是在 Nuxt 应用程序中编写过渡和动画的另一种很好的方式。但在结束本章之前,让我们来看看我们在过渡部分一直看到的过渡模式。因此,让我们找出它们的用途。

请注意,尽管如今不鼓励使用 jQuery,但在本书中偶尔会使用它,因为它是 Foundation 的依赖项,您在上一章中了解过。因此,我们有时会重新使用它。或者,您可以使用 Anime.js 来制作 JavaScript 动画。有关此库的更多信息,请访问animejs.com/

理解过渡模式

您可能想知道mode="out-in"(在 Vue 中)或mode: 'out-in'(在 Nuxt 中)是什么意思-例如,在我们以前的 Vue 应用程序中,其中包含<div>about</div><div>contact</div>组件。它们存在是因为<div>about</div><div>contact</div>之间的过渡是同时渲染的。这是<transition>的默认行为:同时进入和离开。但有时,您可能不希望这种同时过渡,因此 Vue 提供了以下过渡模式的解决方案:

  • in-out模式

此模式用于让新元素首先过渡进入,直到其过渡完成,然后当前元素将过渡出去。

  • out-in模式

此模式用于让当前元素首先过渡出去,直到其过渡完成,然后新元素将过渡进入。

因此,您可以按以下方式使用这些模式:

  • 在 Vue.js 应用程序中,使用它们如下:
<transition name="fade" mode="out-in">
  //...
</transition>
  • 在 Nuxt 应用程序中,使用它们如下:
export default {
  transition: {
    name: 'fade',
    mode: 'out-in'
  }
}
  • 在 JavaScript hooks 中,使用它们如下:
export default {
  methods: {
    enter (el, done) {
      done() // equivalent to mode="out-in"
    },
    leave (el, done) {
      done() // equivalent to mode="out-in"
    }
  }
}

在创建 Nuxt/Vue 应用程序的自定义过渡方面,我们已经走了很长的路。您现在可以根据本章学到的知识制作一些体面的过渡和动画。因此,由于本书空间有限,我们不会进一步深入探讨这个主题,但是有关 Vue 过渡和动画的更多信息和进一步阅读,请访问vuejs.org/v2/guide/transitions.html。现在让我们总结一下您在本章学到的内容!

总结

在本章中,您学习了 Nuxt 中页面的概念以及如何为应用程序创建不同类型的路由。您学会了如何自定义默认应用程序模板和布局,以及如何创建新的布局和 404 页面。您学会了如何使用 CSS 过渡和动画,以及 JavaScript 钩子和方法,使应用程序页面之间的过渡变得有趣。如果您从头开始一直在跟随指南,那么现在您应该能够交付一个外观漂亮的小项目了。您可以在我们的 GitHub 存储库中的/chapter-4/nuxt-universal/sample-website/找到一个网站示例,该示例使用了我们在本章和之前章节中学到的知识。

在下一章中,我们将探索/components/目录。您将学习如何在 Nuxt 应用程序中使用它,以便通过更详细地了解 Vue 组件来完善本章中涵盖的布局和页面,包括从页面和布局组件向它们传递数据,创建单文件 Vue 组件,注册全局和本地 Vue 组件等。此外,您还将学习如何使用 mixin 编写可重用的代码,使用 Vue 风格指南中的命名约定来定义组件名称,以便使您的组件组织和标准化,以便更好地进行未来维护。所有这些知识和探索都是值得的。所以,让我们开始吧。

添加 Vue 组件

正如我们在上一章中所述,Vue 组件是 Nuxt 视图的可选部分。您已经了解了 Nuxt 视图的各种组成部分:应用程序模板、HTML 头部、布局和页面。但是,我们还没有涵盖 Nuxt 中最小的单位-Vue 组件。因此,在本章中,您将学习它的工作原理以及如何利用/components/创建自定义组件。然后,您将学习如何创建全局和本地组件,以及基本和全局 mixin,并了解一些用于开发 Vue 或 Nuxt 应用程序的命名约定。最令人兴奋的是,您将发现如何将数据从父组件传递到子组件,以及如何从子组件向父组件发出数据。

在本章中,我们将涵盖以下主题:

  • 理解 Vue 组件

  • 创建单文件 Vue 组件

  • 注册全局和本地组件

  • 编写基本和全局 mixin

  • 定义组件名称并使用命名约定

让我们开始吧!

第五章:理解 Vue 组件

我们在第二章中简要介绍了/components/目录,开始使用 Nuxt,但我们还没有亲自动手。到目前为止,我们知道如果使用 Nuxt 脚手架工具安装 Nuxt 项目,则该目录中有一个Logo.vue组件。该目录中的所有组件都是Vue 组件,就像/pages/目录中的页面组件一样。主要区别在于/components/目录中的这些组件不支持asyncData方法。让我们以/chapter-4/nuxt-universal/sample-website/中的copyright.vue组件为例:

// components/copyright.vue
<template>
  <p v-html="copyright"></p>
</template>

<script>
export default {
  data () {
    return { copyright: '&copy; Lau Tiam Kok' }
  }
}
</script>

让我们尝试用asyncData函数替换前面代码中的data函数,如下所示:

// components/copyright.vue
export default {
  asyncData () {
    return { copyright: '&copy; Lau Tiam Kok' }
  }
}

您将收到警告错误,浏览器控制台上会显示“属性或方法“copyright”未定义...”。那么,我们如何动态获取版权目的的数据呢?我们可以使用fetch方法直接在组件中使用 HTTP 客户端(例如axios)请求数据,如下所示:

  1. 在项目目录中通过 npm 安装axios包:
$ npm i axios
  1. 导入axios并在fetch方法中请求数据,如下所示:
// components/copyright.vue
import axios from 'axios'

export default {
  data () {
    return { copyright: null }
  },
  fetch () {
    const { data } = axios.get('http/path/to/site-info.json')
    this.copyright = data.copyright  
  }
}

这种方法可以正常工作,但是最好不要使用 HTTP 请求从有效负载中获取少量数据,最好是请求一次,然后将数据从父作用域传递到其子组件中,如下所示:

// components/copyright.vue
export default {
  props: ['copyright']
}

在前面的片段中,子组件是/components/目录中的copyright.vue文件。这个解决方案的奥妙就在于在组件中使用props属性。这样更简单、更整洁,因此是一个优雅的解决方案!但是,如果我们要理解它是如何工作的,以及如何使用它,我们需要了解 Vue 的组件系统。

什么是组件?

组件是单一的、自包含的、可重用的 Vue 实例,具有自定义名称。我们使用 Vue 的component方法来定义组件。例如,如果我们想定义一个名为post-item的组件,我们会这样做:

Vue.component('post-item', {
  data () {
    return { text: 'Hello World!' }
  },
  template: '<p>{{ text }}</p>'
})

做完这些之后,当使用new语句创建根 Vue 实例时,我们可以在 HTML 文档中将此组件用作<post-item>,如下所示:

<div id="post">
  <post-item></post-item>
</div>

<script type="text/javascript">
  Vue.component('post-item', { ... }
  new Vue({ el: '#post' })
</script>

所有组件本质上都是 Vue 实例。这意味着它们具有与new Vue相同的选项(datacomputedwatchmethods等),只是少了一些根特定的选项,比如el。此外,组件可以嵌套在其他组件中,并最终成为类似树状的组件。然而,当这种情况发生时,传递数据变得棘手。因此,在特定组件中直接使用fetch方法获取数据可能更适合这种情况。或者,您可以使用 Vuex 存储,您将在第十章中发现它,“添加 Vuex 存储”。

然而,我们将暂时搁置深度嵌套的组件,专注于本章中简单的父子组件,并学习如何在它们之间传递数据。数据可以从父组件传递到它们的子组件,也可以从子组件传递到父组件。但是我们如何做到这一点呢?首先,让我们找出如何从父组件向子组件传递数据。

使用 props 将数据传递给子组件

让我们通过创建一个名为user-item的子组件来创建一个小型的 Vue 应用,如下所示:

Vue.component('user-item', {
  template: '<li>John Doe</li>'
})

你可以看到它只是一个静态组件,没有太多功能;你根本无法抽象或重用它。只有在我们可以动态地将数据传递到模板内部的template属性中时,它才变得可重用。这可以通过props属性来实现。让我们对组件进行重构,如下所示:

Vue.component('user-item', {
  props: ['name'],
  template: '<li>{{ name }}</li>'
})

在某种意义上,props的行为类似于变量,我们可以使用v-bind指令为它们设置数据,如下所示:

<ol>
  <user-item
    v-for="user in users"
    v-bind:name="user.name"
    v-bind:key="user.id"
  ></user-item>
</ol>

在这个重构的组件中,我们使用v-bind指令将item.name绑定到name,如v-bind:name。组件内的 props 必须接受name作为该组件的属性。然而,在一个更复杂的应用程序中,我们可能需要传递更多的数据,为每个数据写多个 props 可能会适得其反。因此,让我们重构<user-item>组件,使其接受一个名为user的单个 prop:

<ol>
  <user-item
    v-for="user in users"
    v-bind:user="user"
    v-bind:key="user.id"
  ></user-item>
</ol>

现在,让我们再次重构组件代码,如下所示:

Vue.component('user-item', {
  props: ['user'],
  template: '<li>{{ user.name }}</li>'
})

让我们将我们在这里所做的事情放到一个单页 HTML 中,这样您就可以看到更大的图片:

  1. <head>块中包含以下 CDN 链接:
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  1. <body>块中创建以下标记:
<div id="app">
  <ol>
    <user-item
      v-for="user in users"
      v-bind:user="user"
      v-bind:key="user.id"
    ></user-item>
  </ol>
</div>
  1. 将以下代码添加到<script>块中:
Vue.component('user-item', {
  props: ['user'],
  template: '<li>{{ user.name }}</li>'
})

new Vue({
  el: '#app',
  data: {
    users: [
      { id: 0, name: 'John Doe' },
      { id: 1, name: 'Jane Doe' },
      { id: 2, name: 'Mary Moe' }
    ]
  }
})

在这个例子中,我们将应用程序分解成了更小的单元:一个子组件和一个父组件。然而,它们通过props属性进行绑定。现在,我们可以进一步完善它们,而不用担心它们相互干扰。

您可以在本书的 GitHub 存储库中的/chapter-5/vue/component/basic.html中找到这个示例代码。

然而,在一个真实而复杂的应用程序中,我们应该将这个应用程序分成更可管理的单独文件(单文件组件)。我们将在创建单文件 Vue 组件部分向您展示如何创建它们。但现在,让我们发现如何从子组件将数据传递给父组件。

监听子组件事件

到目前为止,您已经学会了如何使用props属性将数据传递给子组件。但是如何从子组件将数据传递给父组件呢?我们可以通过使用$emit方法和自定义事件来实现这一点,如下所示:

$emit(<event>)

您可以选择任何名称作为要在子组件中广播的自定义事件的名称。然后,父组件可以使用v-on指令来监听这个广播事件,并决定接下来要做什么。

v-on:<event>="<event-handler>"

因此,如果您正在发出一个名为done的自定义事件,那么父组件将使用v-on:done指令来监听这个done事件,然后是一个事件处理程序。这个事件处理程序可以是一个简单的 JavaScript 函数,比如v-on:done=handleDone。让我们创建一个简单的应用程序来演示这一点:

  1. 创建应用程序的标记,如下所示:
<div id="todos">
  <todo-item
    v-on:completed="handleCompleted"
  ></todo-item>
</div>
  1. 创建一个子组件,如下所示:
Vue.component('todo-item', {
  template: '<button v-on:click="clicked">Task completed</button>',
  methods: {
    clicked () {
      this.$emit('completed')
    }
  }
})
  1. 创建一个 Vue 根实例作为父级:
new Vue({
  el: '#todos',
  methods: {
    handleCompleted () {
      alert('Task Done')
    }
  }
})

在这个例子中,当clicked方法在子组件中触发时,子组件将发出一个completed事件。在这里,父组件通过v-on接收事件,然后在其端触发handleCompleted方法。

您可以在本书的 GitHub 存储库中的/chapter-5/vue/component/emit/emit-basic.html中找到这个例子。

通过事件发出值

然而,有时仅仅发出一个事件是不够的。在某些情况下,使用带有值的事件更有用。我们可以通过在$emit方法中使用第二个参数来实现这一点,如下所示:

$emit(<event>, <value>)

然后,当父组件监听事件时,可以以以下格式使用$event访问发出的事件的值:

v-on:<event>="<event-handler> = $event"

如果事件处理程序是一个方法,那么该值将是该方法的第一个参数,格式如下:

methods: {
  handleCompleted (<value>) { ... }
}

因此,现在,我们可以简单地修改前面的应用程序,如下所示:

// Child
clicked () {
  this.$emit('completed', 'Task done')
}

// Parent
methods: {
  handleCompleted (value) {
    alert(value)
  }
}

在这里,您可以看到在父组件和子组件之间传递数据是有趣且容易的。但是,如果您的子组件中有一个<input>元素,如何将输入字段中的值传递给父组件进行双向数据绑定呢?如果我们了解 Vue 中双向数据绑定的“底层”发生了什么,这并不难。我们将在下一节中学习这个知识点。

您可以在本书的 GitHub 存储库中的/chapter-5/vue/component/emit/value.html中找到这个简单的例子,以及在/chapter-5/vue/component/emit/emit-value-with-props.html中找到更复杂的例子。

使用 v-model 创建自定义输入组件

我们还可以使用组件创建自定义的双向绑定输入,其工作方式与v-model指令相同,用于向父组件发出事件。让我们创建一个基本的自定义输入组件:

<custom-input v-model="newTodoText"></custom-input>

Vue.component('custom-input', {
  props: ['value'],
  template: `<input v-on:input="$emit('input', $event.target.value)">`,
})

它是如何工作的?要理解这一点,我们需要了解v-model在幕后是如何工作的。让我们使用一个简单的v-model输入:

<input v-model="handler">

前面的<input>元素是以下内容的简写:

<input
  v-bind:value="handler"
  v-on:input="handler = $event.target.value"
>

因此,在我们的自定义输入中编写v-model="newTodoText"是以下内容的简写:

v-bind:value="newTodoText"
v-on:input="newTodoText = $event.target.value"

这意味着这个简写下面的组件必须在props属性中具有value属性,以便让数据从顶部传递下来。它必须发出一个带有$event.target.valueinput事件,以便将数据传递到顶部。

因此,在这个例子中,当用户在custom-input子组件中输入时,我们发出值,而父组件通过v-model="newTodoText"监听更改,并更新data对象中newTodoText的值:

<p>{{ newTodoText }}</p>

new Vue({
  el: '#todos',
  data: {
    newTodoText: null
  }
})

当你了解 Vue 中双向数据绑定的机制——v-model指令时,这就变得很合理了,不是吗?但是,如果你不想使用复选框输入和单选按钮元素的默认值呢?在这种情况下,你会想要将自定义的值发送到父组件中。我们将在下一节中学习如何做到这一点。

你可以在/chapter-5/vue/component/custom-inputs/basic.html找到这个简单的例子,在/chapter-5/vue/component/custom-inputs/props.html中找到一个更复杂的例子,这两个例子都可以在这本书的 GitHub 存储库中找到。

自定义输入组件中的模型定制

默认情况下,自定义输入组件中的模型使用value属性作为 prop,input作为事件。使用我们之前例子中的custom-input组件,可以写成如下形式:

Vue.component('custom-input', {
  props: {
    value: null
  },
  model: {
    prop: 'value', // <-- default
    event: 'input' // <-- default
  }
})

在这个例子中,我们不需要指定propevent属性,因为它们是该组件模型的默认行为。但是当我们不想对某些输入类型使用这些默认值时,这将变得很有用,比如复选框和单选按钮。

我们可能希望在这些输入中使用value属性来实现不同的目的,比如在提交的数据中与复选框的name一起发送特定的值,如下所示:

Vue.component('custom-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="changed"
      name="subscribe"
      value="newsletter"
    >
  `
  ,
  methods: {
    changed ($event) {
      this.$emit('change', $event.target.checked)
    }
  }
})

在这个例子中,我们想要将这两个数据发送到服务器:

name="subscribe"
value="newsletter"

我们还可以在使用JSON.stringify进行序列化后以 JSON 格式进行:

[{
  "name":"subscribe",
  "value":"newsletter"
}]

所以,假设我们在这个组件中没有设置以下自定义模型:

model: {
  prop: 'checked',
  event: 'change'
}

在这种情况下,我们只能将以下默认数据发送到服务器:

[{
  "name":"subscribe",
  "value":"on"
}]

你可以在这本书的 GitHub 存储库中的/chapter-5/vue/component/custom-inputs/checkbox.html中找到这个例子。

当你知道 Vue 组件底层是什么,并且可以通过一点努力进行定制时,这就变得合理了。/components/目录中的 Vue 组件与你刚刚学习的组件的工作方式相同。但在深入编写 Nuxt 应用程序的组件之前,你应该了解在使用v-for指令时为什么key属性很重要。让我们找出来。

理解v-for循环中的key属性

在这本书的许多先前的例子和练习中,你可能注意到了所有v-for循环中的key属性,如下所示:

<ol>
  <user-item
    v-for="user in users"
    v-bind:user="user"
    v-bind:key="user.id"
  ></user-item>
</ol>

也许你会想知道它是什么,它是用来做什么的。key属性是每个 DOM 节点的唯一标识,以便 Vue 可以跟踪它们的变化,从而重用和重新排序现有元素。使用index作为 key 属性的跟踪是 Vue 默认的行为,因此像这样使用index作为 key 属性是多余的:

<div v-for="(user, index) in users" :key="index">
  //...
</div>

因此,如果我们希望 Vue 准确地跟踪每个项目的标识,我们必须通过使用v-bind指令将每个 key 属性绑定到一个唯一的值,如下所示:

<div v-for="user in users" :key="user.id">
  //...
</div>

我们可以使用缩写:key来绑定唯一的值,就像前面的例子中所示的那样。还要记住,key是一个保留属性。这意味着它不能作为组件 prop 使用:

Vue.component('user-item', {
  props: ['key', 'user']
})

props属性中使用key将导致浏览器控制台中出现以下错误:

[Vue warn]: "key" is a reserved attribute and cannot be used as 
component prop.

当使用v-for与组件时,key属性是必需的。因此,最好在可能的情况下明确地使用keyv-for,无论您是否将其与组件一起使用。

为了演示这个问题,让我们创建一个 Vue 应用程序,我们将在其中使用index作为我们的key,并借助一点 jQuery 的帮助:

  1. <head>块中包含所需的 CDN 链接,以及一些 CSS 样式:
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="http://code.jquery.com/jquery-3.3.1.js"></script>
<style type="text/css">
  .removed {
    text-decoration: line-through;
  }
  .removed button {
    display: none;
  }
</style>
  1. <body>块中创建所需的应用程序 HTML 标记:
<div id="todo-list-example">
  <form v-on:submit.prevent="addNewTodo">
    <label for="new-todo">Add a todo</label>
    <input
      v-model="newTodoText"
      id="new-todo"
      placeholder="E.g. Feed the cat"
    >
    <button>Add</button>
  </form>
  <ul>
    <todo-item
      v-for="(todo, index) in todos"
      v-bind:key="index"
      v-bind:title="todo.title"
    ></todo-item>
  </ul>
</div>
  1. <script>块中创建所需的组件:
Vue.component('todo-item', {
  template: `<li>{{ title }} <button v-
   on:click="remove($event)">Remove</button></li>`,
  props: ['title'],
  methods: {
    remove: function ($event) {
      $($event.target).parent().addClass('removed')
    }
  }
})
  1. 创建所需的待办任务列表,如下所示:
new Vue({
  el: '#todo-list-example',
  data: {
    newTodoText: '',
    todos: [
      { id: 1, title: 'Do the dishes' },
      //...
    ],
    nextTodoId: 4
  },
  methods: {
    addNewTodo: function () {
      this.todos.unshift({
        id: this.nextTodoId++,
        title: this.newTodoText
      })
      this.newTodoText = ''
    }
  }
})

在这个例子中,我们通过在我们的todos数组上发生unshift来将一个新的待办任务添加到列表的顶部。我们通过向li元素添加removed类名来删除一个待办任务。然后,我们使用 CSS 为已删除的待办任务添加删除线,并隐藏删除按钮。

  1. 让我们移除洗碗。你会看到以下内容:
Do the dishes  (with a strike-through)
  1. 现在,添加一个名为喂猫的新任务。你会看到以下内容:
Feed the cat (with a strike-through)

这是因为喂猫现在已经占据了洗碗的索引,即 0。Vue 只是重用元素而不是渲染新元素。换句话说,无论对项目进行了何种更改,Vue 都将根据数组中的索引更新 DOM 元素。这意味着我们得到了一个意外的结果。

你可以在这本书的 GitHub 存储库中的/chapter-5/vue/component/key/using-index.html中找到这个例子。在浏览器上运行它,看看问题出在哪里。然后,将其与在/chapter-5/vue/component/key/using-id.html中使用id作为键的情况进行比较。你会发现你得到了正确的行为。

使用索引作为 key 的问题也可以通过以下伪代码来解释,其中正在生成一组数字,并为每个数字设置索引作为 key:

let numbers = [1,2,3]

<div v-for="(number, index) in numbers" :key="index">
  // Which turns into number - index
  1 - 0
  2 - 1
  3 - 2
</div>

这看起来很棒,乍一看工作正常。但是如果你添加数字 4,索引信息就变得无用了。这是因为现在每个数字都得到了一个新的索引:

<div v-for="(number, index) in numbers" :key="index">
  4 - 0
  1 - 1
  2 - 2
  3 - 3
</div>

如你所见,1、2 和 3 失去了它们的状态,必须重新渲染。这就是为什么对于这种情况,使用唯一的 key 是必需的。对于每个项目来说,保持其索引号并且不在每次更改时重新分配是很重要的:

<user-item
  v-for="(user, index) in users"
  v-bind:key="user.id"
  v-bind:name="user.name"
></user-item>

作为一个经验法则,每当你以一种导致索引变化的方式操纵列表时,使用 key,这样 Vue 可以在之后正确地更新 DOM。这些操纵包括以下内容:

  • 在数组中的任何位置添加一个项目

  • 从数组中删除一个项目,从除数组末尾以外的任何位置

  • 以任何方式重新排序数组

如果你的列表在组件的生命周期内从未改变过,或者你只是使用 push 函数而不是unshift函数添加项目,就像前面的例子一样,那么使用索引作为 key 是可以的。但是如果你试图追踪你何时需要使用索引和何时不需要使用索引,最终你会遇到“bug”,因为你可能会误解 Vue 的行为。

如果你不确定是否要使用索引作为 key,那么最好在v-for循环中使用带有不可变 ID 的key属性。使用具有唯一值的key属性不仅对v-for指令很重要,而且对 HTML 表单中的<input>元素也很重要。我们将在下一节中讨论这个问题。

使用 key 属性控制可重用的元素

为了提供更好的性能,我们发现 Vue 总是重用 DOM 节点而不是重新渲染,这可能会产生一些不良结果,正如前一节所示。这里有另一个没有使用v-for的示例,以演示为什么拥有 key 属性是相当重要的:

<div id="app">
  <template v-if="type === 'fruits'">
    <label>Fruits</label>
    <input />
  </template>
  <template v-else>
    <label>Vegetables</label>
    <input />
  </template>
  <button v-on:click="toggleType">Toggle Type</button>
</div>

<script type="text/javascript">
  new Vue({
    el: '#app',
    data: { type: 'fruits' },
    methods: {
      toggleType: function () {
        return this.type = this.type === 'fruits' ? 'vegetables' : 'fruits'
      }
    }
  })
</script>

在这个例子中,如果你在输入水果的名字并切换类型,你仍然会在vegetables输入框中看到你刚刚输入的名字。这是因为 Vue 试图尽可能地重用相同的<input>元素以获得最快的结果。但这并不总是理想的。你可以通过为每个<input>元素添加key属性以及一个唯一值来告诉 Vue 不要重用相同的<input>元素,如下所示:

<template v-if="type === 'fruits'">
  <label>Fruits</label>
  <input key="fruits-input"/>
</template>
<template v-else>
  <label>Vegetables</label>
  <input key="vegetables-input"/>
</template>

因此,如果您刷新页面并再次测试,输入字段现在应该按预期工作,而不会在切换它们时“重用”彼此。这不适用于<label>元素,因为它们没有key属性。但是,从视觉上来看,这不是问题。

您可以在本书的 GitHub 存储库的/chapter-5/vue/component/key/目录中的toggle-with-key.htmltoggle-without-key.html文件中找到此示例代码。

这就是您需要了解的关于 Vue 组件基本性质的全部内容。因此,到目前为止,您应该已经掌握了足够的基本知识,可以开始使用单文件组件创建 Vue 组件的下一个级别。让我们开始吧!

如果您想了解更多关于 Vue 组件以及 Vue 组件更深入的部分,例如插槽,请访问vuejs.org/v2/guide/components.html

创建单文件 Vue 组件

我们一直在使用单个 HTML 页面编写 Vue 应用程序,以便快速获得我们想要看到的结果。但是在 Vue 或 Nuxt 的实际开发项目中,我们不希望编写这样的东西:

const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }

在前面的代码中,我们在一个地方(例如在单个 HTML 页面中)使用 JavaScript 对象创建了两个 Vue 组件,但最好将它们分开,并在单独的.js文件中创建每个组件,如下所示:

// components/foo.js
Vue.component('page-foo', {
  data: function () {
    return { message: 'foo' }
  },
  template: '<div>{{ count }}</div>'
})

这对于简单的组件可以很好地工作,其中 HTML 布局很简单。但是,在涉及更复杂的 HTML 标记的更复杂布局中,我们希望避免在 JavaScript 文件中编写 HTML。这个问题可以通过具有.vue扩展名的单文件组件来解决,如下所示:

// index.vue
<template>
  <p>{{ message }}</p>
</template>

<script>
export default {
  data () {
    return { message: 'Hello World!' }
  }
}
</script>

<style scoped>
p {
  font-size: 2em;
  text-align: center;
}
</style>

然而,我们不能只是在浏览器上运行该文件,而不使用构建工具(如 webpack 或 rollup)进行编译。在本书中,我们使用 webpack。这意味着,从现在开始,我们将不再使用 CDN 或单个 HTML 页面来创建复杂的 Vue 应用程序。相反,我们将使用.vue.js文件,只有一个.html文件来创建我们的 Vue 应用程序。我们将在接下来的部分指导您如何使用 webpack 来帮助我们做到这一点。让我们开始吧。

使用 webpack 编译单文件组件

要编译.vue组件,我们需要将vue-loadervue-template-compiler安装到 webpack 构建过程中。但在此之前,我们必须在项目目录中创建一个package.json文件,列出我们项目依赖的 Node.js 包。您可以在docs.npmjs.com/creating-a-package-json-file上查看package.json字段的详细信息。最基本和必需的是nameversion字段。让我们开始吧:

  1. 在项目目录中创建一个package.json文件,其中包含以下必填字段和值:
// package.json
{
  "name": "vue-single-file-component",
  "version": "1.0.0"
}
  1. 打开一个终端,将目录更改为您的项目,并安装vue-loadervue-template-compiler
$ npm i vue-loader --save-dev 
$ npm i vue-template-compiler --save-dev

您应该在终端上看到一个警告,因为您在此处安装的 Node.js 包需要其他 Node.js 包,其中最显着的是 webpack 包。在本书中,我们在本书的 GitHub 存储库中的/chapter-5/vue/component-webpack/basic/中设置了一个基本的 webpack 构建过程。我们将在大多数即将推出的 Vue 应用程序中使用此设置。我们已将 webpack 配置文件分成了三个较小的配置文件:

  • webpack.common.js包含在开发和生产过程中共享的常见 webpack 插件和配置。

  • webpack.dev.js仅包含开发过程的插件和配置。

  • webpack.prod.js仅包含生产过程的插件和配置。

以下代码显示了我们如何在script命令中使用这些文件:

// package.json
"scripts": {
  "start": "webpack-dev-server --open --config webpack.dev.js",
  "watch": "webpack --watch",
  "build": "webpack --config webpack.prod.js"
}

请注意,在本书中,我们假设您已经知道如何使用 webpack 来编译 JavaScript 模块。如果您对 webpack 还不熟悉,请访问webpack.js.org/获取更多信息。

  1. 因此,在安装了vue-loadervue-template-compiler之后,我们需要在webpack.common.js(或webpack.config.js,如果您使用单个配置文件)中配置module.rules,如下所示:
// webpack.common.js
const VueLoaderPlugin = require('vue-loader/lib/plugin')

module.exports = {
  mode: 'development',
  module: {
    rules: [
      {
        test: /\.vue$/,
        loader: 'vue-loader'
      },
      {
        test: /\.js$/,
        loader: 'babel-loader'
      },
      {
        test: /\.css$/,
        use: [
          'vue-style-loader',
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin()
  ]
}
  1. 然后,我们可以使用在package.json中设置的以下命令来查看我们的应用程序运行情况:
  • $ npm run start用于在localhost:8080进行实时重新加载和开发

  • $ npm run watch用于在/path/to/your/project/dist/进行开发

  • $ npm run build用于在/path/to/your/project/dist/编译我们的代码

就是这样。现在你有了一个基本的构建流程来开发 Vue 应用程序与 webpack。所以,从现在开始,在更复杂的应用程序中,我们将编写单文件组件,并使用这种方法来编译它们。我们将在下一节中创建一个简单的 Vue 应用程序。

在单文件组件中传递数据和监听事件

到目前为止,我们一直在使用单个 HTML 页面进行我们的“todo”演示。这一次,我们将使用单文件组件来创建一个简单的“todo”购物清单。让我们开始:

  1. <div>元素中创建一个带有"todos"ID 的index.html文件,以便 Vue 运行 Vue 实例:
// index.html
<!doctype html>
<html>
  <head>
    <title>Todo Grocery Application (Single File 
     Components)</title>
  </head>
  <body>
    <div id="todos"></div>
  </body>
</html>
  1. 在项目根目录中创建一个/src/目录,并在其中创建一个entry.js文件作为文件入口点,以指示 webpack 应该使用哪些模块来开始构建我们的应用程序内部依赖图。webpack 还将使用此文件来找出入口点依赖的其他模块和库(直接和间接)。
// src/entry.js

'use strict'

import Vue from 'vue/dist/vue.js'
import App from './app.vue'

new Vue({
  el: 'todos',
  template: '<App/>',
  components: {
    App
  }
})
  1. <script>块中创建一个提供虚拟数据的父组件,其中包含项目列表:
// src/app.vue
<template>
  <div>
    <ol>
      <TodoItem
        v-for="thing in groceryList"
        v-bind:item="thing"
        v-bind:key="item.id"
        v-on:add-item="addItem"
        v-on:delete-item="deleteItem"
      ></TodoItem>
    </ol>
    <p><span v-html="&pound;"></span>{{ total }}</p>
  </div>
</template>

<script>
import TodoItem from './todo-item.vue'
export default {
  data () {
    return {
      cart: [],
      total: 0,
      groceryList: [
        { id: 0, text: 'Lentils', price: 2 },
        //...
      ]
    }
  },
  components: {
    TodoItem
  }
}
</script>

在上面的代码中,我们简单地将子组件作为TodoItem导入,并使用v-forgroceryList中生成它们的列表。

  1. methods对象中添加以下方法以添加和删除项目。然后,在computed对象中添加一个方法,计算购物车中项目的总成本:
// src/app.vue
methods: {
  addItem (item) {
    this.cart.push(item)
    this.total = this.shoppingCartTotal
  },
  deleteItem (item) {
    this.cart.splice(this.cart.findIndex(e => e === item), 1)
    this.total = this.shoppingCartTotal
  }
},
computed: {
  shoppingCartTotal () {
    let prices = this.cart.map(item => item.price)
    let sum = prices.reduce((accumulator, currentValue) =>
     accumulator + currentValue, 0)
    return sum
  }
}
  1. 创建一个子组件,通过props显示从父级传递下来的项目:
// src/todo-item.vue
<template>
  <li>
    <input type="checkbox" :name="item.id" v-model="checked"> {{
     item.text }}
    <span v-html="&pound;"></span>{{ item.price }}
  </li>
</template>

<script>
export default {
  props: ['item'],
  data () {
    return { checked: false }
  },
  methods: {
    addToCart (item) {
      this.$emit('add-item', item)
    }
  },
  watch: {
    checked (boolean) {
      if (boolean === false) {
        return this.$emit('delete-item', this.item)
      }
      this.$emit('add-item', this.item)
    }
  }
}
</script>

在这个组件中,我们还有一个checkbox按钮。这用于发出delete-itemadd-item事件,并将项目数据传递给父级。现在,如果你用$ npm run start运行应用程序,你应该看到它在localhost:8080加载。

干得好!你已经成功构建了一个使用 webpack 的 Vue 应用程序,这就是 Nuxt 在幕后使用的编译和构建你的 Nuxt 应用程序。了解已建立系统下方运行的内容总是有用的。当你知道如何使用 webpack 时,你可以使用刚学到的 webpack 构建设置来进行各种 JavaScript 和 CSS 相关的项目。

你可以在本书的 GitHub 存储库中的/chapter-5/vue/component-webpack/todo/中找到这个示例。

在下一节中,我们将把前面几节学到的内容应用到/chapter-5/nuxt-universal/local-components/sample-website/中的sample website示例网站中,这个示例可以在本书的 GitHub 存储库中找到。

在 Nuxt 中添加 Vue 组件

在示例网站中,我们只有两个.vue文件可以使用 Vue 组件进行改进:/layouts/default.vue/pages/work/index.vue。首先,我们应该改进/layouts/default.vue。在这个文件中,我们只需要改进三件事:导航、社交媒体链接和版权。

重构导航社交链接

我们将从重构导航和社交媒体链接开始:

  1. /components/目录中创建一个导航组件,如下所示:
// components/nav.vue
<template>
  <li>
    <nuxt-link :to="item.link" v-html="item.name">
    </nuxt-link>
  </li>
</template>

<script>
export default {
  props: ['item']
}
</script>
  1. /components/目录中也创建一个社交链接组件,如下所示:
// components/social.vue
<template>
  <li>
    <a :href="item.link" target="_blank">
      <i :class="item.classes"></i>
    </a>
  </li>
</template>

<script>
export default {
  props: ['item']
}
</script>
  1. 将它们导入到布局的<script>块中,如下所示:
// layouts/default.vue
import Nav from '~/components/nav.vue'
import Social from '~/components/social.vue'

components: {
  Nav,
  Social
}

请注意,如果您在 Nuxt 配置文件中将components选项设置为true,则可以跳过此步骤。

  1. <template>块中删除现有的导航和社交链接块:
// layouts/default.vue
<template v-for="item in nav">
  <li><nuxt-link :to="item.link" v-html="item.name">
  </nuxt-link></li>
</template>

<template v-for="item in social">
  <li>
    <a :href="item.link" target="_blank">
      <i :class="item.classes"></i>
    </a>
  </li>
</template>
  1. 用导入的NavSocial组件替换它们,如下所示:
// layouts/default.vue
<Nav
  v-for="item in nav"
  v-bind:item="item"
  v-bind:key="item.slug"
 ></Nav>

<Social
  v-for="item in social"
  v-bind:item="item"
  v-bind:key="item.name"
 ></Social>

有了这些,你就完成了!

重构版权组件

现在,我们将重构已经存在于/components/目录中的版权组件。让我们开始吧:

  1. /components/base-copyright.vue文件的<script>块中删除data函数:
// components/copyright.vue
export default {
  data () {
    return { copyright: '&copy; Lau Tiam Kok' }
  }
}
  1. props属性替换前面的data函数,如下所示:
// components/copyright.vue
export default {
  props: ['copyright']
}
  1. 将版权数据添加到<script>块中,而不是/layouts/default.vue中:
// layouts/default.vue
data () {
  return {
    copyright: '&copy; Lau Tiam Kok',
  }
}
  1. <template>块中删除现有的<Copyright />组件:
// layouts/default.vue
<Copyright />
  1. 添加一个新的<Copyright />组件,并将版权数据绑定到它:
// layouts/default.vue
<Copyright v-bind:copyright="copyright" />

到此为止,您应该已经成功将数据从默认页面(父级)传递给组件(子级),在默认页面中保留了您的数据。干得好!这就是/layouts/default.vue的全部内容。我们还可以改进工作页面,我们已经为您在/chapter-5/nuxt-universal/local-components/sample-website/中完成了这项工作,这可以在本书的 GitHub 存储库中找到。如果您在本地安装此示例网站并在本地机器上运行它,您将看到我们已经很好地应用了我们的组件。通过这个例子,您可以看到在理解了 Vue 组件系统的工作原理后,将布局中的元素抽象化为组件是多么容易。但是,如何将数据传递给父组件呢?为此,我们创建了一个示例应用程序,其中子组件向父组件发出事件,位于/chapter-5/nuxt-universal/local-components/emit-events/,这可以在本书的 GitHub 存储库中找到。我们还向应用程序添加了自定义输入和复选框组件,请查看一下。以下是一个示例片段:

// components/input-checkbox.vue
<template>
  <input
    type="checkbox"
    v-bind:checked="checked"
    v-on:change="changed"
    name="subscribe"
    value="newsletter"
  >
</template>

<script>
export default {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: { checked: Boolean },
  methods: {
    changed ($event) {
      this.$emit('change', $event.target.checked)
    }
  }
}
</script>

在这里,您可以看到我们在 Nuxt 应用程序中使用的组件代码与我们在 Vue 应用程序中编写的代码相同。这些类型的组件是嵌套组件。props属性和$emit方法用于在父组件和子组件之间传递数据。这些嵌套组件也是本地的,因为它们只在导入它们的组件(父级)的范围内可用。因此,从另一个角度来看,Vue 组件可以被归类为本地组件和全局组件。自从*什么是组件?*部分以来,您一直在学习全局组件。但是,您只学会了如何在 Vue 应用程序中使用它们。在接下来的部分中,我们将看看如何为 Nuxt 应用程序注册全局组件。但在跳入其中之前,让我们从整体的角度重新审视 Vue 组件:全局组件和本地组件。

注册全局和本地组件

我们已经创建了许多组件,无论是使用Vue.component()、纯 JavaScript 对象还是单文件组件引擎。我们创建的一些组件是全局组件,而另一些是本地组件。例如,在上一节中刚刚创建的/components/目录中的所有重构组件都是本地组件,而在*什么是组件?*部分中创建的组件都是全局组件。无论它们是本地组件还是全局组件,如果您想使用它们,都必须进行注册。其中一些在创建时注册,而另一些则需要手动注册。在接下来的部分中,您将学习如何全局和本地注册它们。您还将了解两种类型的注册将如何影响您的应用程序。我们将学习如何注册 Vue 组件,而不是传递它们。

在 Vue 中注册全局组件

全局组件,正如它们的名称所示,可以在整个应用程序中全局使用。当您使用Vue.component()创建它们时,它们会被全局注册:

Vue.component('my-component-name', { ... })

全局组件必须在根 Vue 实例实例化之前注册。注册后,它们可以在根 Vue 实例的模板中使用,如下所示:

Vue.component('component-x', { ... })
Vue.component('component-y', { ... })
Vue.component('component-z', { ... })

new Vue({ el: '#app' })

<div id="app">
  <component-x></component-x>
  <component-y></component-y>
  <component-z></component-z>
</div>

在这里,您可以看到注册全局组件非常容易 - 在创建它们时,您甚至可能意识不到注册过程。我们将很快在Nuxt 中注册全局组件部分中研究这种类型的注册。但现在,我们将学习如何注册本地组件。

在 Vue/Nuxt 中注册本地组件

在本章中,我们已经看到并使用了 Vue 和 Nuxt 应用中的本地组件。这些组件是通过使用纯 JavaScript 对象创建的,如下所示:

var ComponentX = { ... }
var ComponentY = { ... }
var ComponentZ = { ... }

然后,它们可以通过components选项进行注册,如下所示:

new Vue({
  el: '#app',
  components: {
    'component-x': ComponentX,
    'component-y': ComponentY,
    'component-z': ComponentZ
  }
})

还记得我们在本书的 GitHub 存储库中的/chapter-5/vue/component/basic.html文件中创建的 Vue 应用吗?该应用中的user-item组件是一个全局组件。现在,让我们对其进行重构并将其变成一个本地组件:

  1. 移除以下全局组件:
Vue.component('user-item', {
  props: ['user'],
  template: '<li>{{ user.name }}</li>'
})
  1. 使用以下方式替换为本地组件:
const UserItem = {
  props: ['user'],
  template: '<li>{{ user.name }}</li>'
}
  1. 使用components选项注册本地组件:
new Vue({
  el: '#app',
  data: {
    users: [
      { id: 0, name: 'John Doe' },
      //...
    ]
  },
  components: {
    'user-item': UserItem
  }
})

该应用程序将与以前的方式相同工作。唯一的区别是user-item不再全局可用。这意味着它在任何其他子组件中都不可用。例如,如果您想要在ComponentZ中使ComponentX可用,那么您必须手动"附加"它:

var ComponentX = { ... }

var ComponentZ = {
  components: {
    'component-x': ComponentX
  }
}

如果您正在使用 babel 和 webpack 编写 ES2015 模块,您可以将ComponentX作为单文件组件,然后导入它,如下所示:

// components/ComponentZ.vue
import Componentx from './Componentx.vue'

export default {
  components: {
    'component-x': ComponentX
  }
}

<component-x
  v-for="item in items"
  ...
></component-x>

您还可以从components选项中省略component-x,并直接在其中使用ComponentX变量,如下所示:

// components/ComponentZ.vue
export default {
  components: {
    ComponentX
  }
}

在 ES2015+中使用诸如ComponentX之类的变量作为 JavaScript 对象的简写形式为ComponentX: ComponentX。由于component-x从未注册过,所以您需要在模板中使用<ComponentX>而不是<component-x>

<ComponentX
  v-for="item in items"
  ...
></ComponentX>

在前面的单文件组件中编写 ES2015 与我们在 Nuxt 中编写.vue文件的方式相同。因此,到目前为止,您应该已经意识到我们一直在 Nuxt 应用程序中编写本地组件,例如/components/copyright.vue/components/nav.vue。但是在 Nuxt 应用程序中如何编写全局组件呢?这就是/plugins/目录发挥作用的地方。在下一节中,您将学习如何在 Nuxt 中进行此操作。

您可以在本书的 GitHub 存储库中的/chapter-5/vue/component/registering-local-components.html中找到前面的应用程序。

在 Nuxt 中注册全局组件

我们在第二章中学习了目录结构,开始使用 Nuxt/plugins/目录是我们可以创建 JavaScript 文件并在实例化根 Vue 应用程序之前运行的最佳位置。因此,这是注册我们的全局组件的最佳位置。

让我们创建我们的第一个全局组件:

  1. /plugins/目录中创建一个简单的 Vue 组件,如下所示:
// components/global/sample-1.vue
<template>
  <p>{{ message }}</p>
</template>

<script>
export default {
  data () {
    return {
      message: 'A message from sample global component 1.'
    }
  }
}
</script>
  1. /plugins/目录中创建一个.js文件,并导入前面的组件,如下所示:
// plugins/global-components.js
import Vue from 'vue'
import Sample from '~/components/global/sample-1.vue'

Vue.component('sample-1', Sample)
  1. 我们还可以直接在/plugins/global-components.js中创建第二个全局组件,如下所示:
Vue.component('sample-2', {
  render (createElement) {
    return createElement('p', 'A message from sample global
     component 2.')
  }
})
  1. 告诉 Nuxt 在 Nuxt 配置文件中在实例化根应用程序之前先运行它们,如下所示:
// nuxt.config.js
plugins: [
  '~/plugins/global-components.js',
]

请注意,此组件将在 Nuxt 应用程序的客户端和服务器端都可用。如果您只想在特定端上运行此组件,例如仅在客户端上运行,则可以注册它,如下所示:

// nuxt.config.js
plugins: [
  { src: '~/plugins/global-components.js',  mode: 'client' }
]

现在,这个组件只能在客户端使用。但是,如果你只想在服务器端运行它,只需在前面的mode选项中使用server

  1. 我们可以在任何地方使用这些全局组件,而无需手动再次导入它们,如下面的代码所示:
// pages/about.vue
<sample-1 />
<sample-2 />
  1. 在浏览器上运行应用程序。你应该得到以下输出:
<p>A message from sample global component 1.</p>
<p>A message from sample global component 2.</p>

就是这样!这就是你可以通过涉及各种文件在 Nuxt 中注册全局组件的方法。全局注册的底线是使用Vue.component,就像我们在 Vue 应用程序中所做的那样。然而,全局注册通常不是理想的,就像它的“表兄弟”全局混入一样,我们将在下一节中介绍。例如,全局注册组件但在大多数情况下不需要它们对于服务器和客户端来说都是不必要的。现在,让我们继续看看混入是什么,以及如何编写它们。

你可以在本书的 GitHub 存储库中的/chapter-5/nuxt-universal/global-components/中找到这个例子。

编写基本和全局混入

混入只是一个 JavaScript 对象,可以用来包含任何组件选项,比如createdmethodsmounted等等。它们可以用来使这些选项可重用。我们可以通过将它们导入到组件中,并将它们与该组件中的其他选项“混合”来实现这一点。

在某些情况下,使用混入可能是有用的,比如在第二章中,开始使用 Nuxt。我们知道,当 Vue Loader 编译单文件组件中的<template>块时,它会将遇到的任何资源 URL 转换为 webpack 模块请求,如下所示:

<img src="~/assets/sample-1.jpg">

前面的图像将被转换为以下 JavaScript 代码:

createElement('img', {
  attrs: {
    src: require('~/assets/sample-1.jpg') // this is now a module request
  }
})

如果您手动插入图像,这并不难。但在大多数情况下,我们希望动态插入图像,如下所示:

// pages/about.vue
<template>
  <img :src="'~/assets/images' + post.image.src" :alt="post.image.alt">
</template>

const post = {
  title: 'About',
  image: {
    src: '/about.jpg',
    alt: 'Sample alt 1'
  }
}

export default {
  data () {
    return { post }
  }
}

在这个例子中,当您在控制台上使用:src指令时,图像将会得到 404 错误,因为 Vue Loader 在构建过程中从未编译它。为了解决这个问题,我们需要手动将模块请求插入到:src指令中。

<img :src="require('~/assets/images/about.jpg')" :alt="post.image.alt">

然而,这也不好,因为更倾向于动态图像解决方案。因此,这里的解决方案如下:

<img :src="loadAssetImage(post.image.src)" :alt="post.image.alt">

在这个解决方案中,我们编写了一个可重用的loadAssetImage函数,以便在任何需要的 Vue 组件中调用它。因此,在这种情况下,我们需要混合。有几种使用混合的方法。我们将在接下来的几节中看一些常见的用法。

创建基本混合/非全局混合

在非单文件组件 Vue 应用程序中,我们可以这样定义一个混合对象:

var myMixin = {
  created () {
    this.hello()
  },
  methods: {
    hello () { console.log('hello from mixin!') }
  }
}

然后,我们可以使用Vue.extend()将其“附加”到一个组件中:

const Foo = Vue.extend({
  mixins: [myMixin],
  template: '<div>foo</div>'
})

在这个例子中,我们只将这个混合附加到Foo,所以当调用这个组件时,你只会看到console.log消息。

你可以在这本书的 GitHub 存储库的/chapter-5/vue/mixins/basic.html中找到这个例子。

对于 Nuxt 应用程序,我们在/plugins/目录中创建并保存混合对象,保存在.js文件中。让我们来演示一下:

  1. /plugins/目录中创建一个mixin-basic.js文件,其中包含一个在浏览器控制台上打印消息的函数,当 Vue 实例被创建时:
// plugins/mixin-basic.js
export default {
  created () {
    this.hello()
  },
  methods: {
    hello () {
      console.log('hello from mixin!')
    }
  }
}
  1. 在需要的地方随时导入它,如下所示:
// pages/about.vue
import Mixin from '~/plugins/mixin-basic.js'

export default {
  mixins: [Mixin]
}

在这个例子中,只有当你在/about路由上时,你才会得到console.log消息。这就是我们创建和使用非全局混合的方法。但在某些情况下,我们需要全局混合适用于应用程序中的所有组件。让我们看看我们如何做到这一点。

你可以在这本书的 GitHub 存储库的/chapter-5/nuxt-universal/mixins/basic/中找到这个例子。

创建全局混合

我们可以通过使用Vue.mixin()来创建和应用全局混合:

Vue.mixin({
  mounted () {
    console.log('hello from mixin!')
  }
})

全局混合必须在实例化 Vue 实例之前定义:

const app = new Vue({
  //...
}).$mount('#app')

现在,你创建的每个组件都将受到影响并显示该消息。你可以在这本书的 GitHub 存储库的/chapter-5/vue/mixins/global.html中找到这个例子。如果你在浏览器上运行它,你会看到console.log消息出现在每个路由上,因为它在所有路由组件中传播。通过这种方式,我们可以看到如果被滥用可能造成的潜在危害。在 Nuxt 中,我们以相同的方式创建全局混合;也就是使用Vue.mixin()。让我们来看一下:

  1. /plugins/目录中创建一个mixin-utils.js文件,以及用于从/assets/目录加载图像的函数:
// plugins/mixin-utils.js
import Vue from 'vue'

Vue.mixin({
  methods: {
    loadAssetImage (src) {
      return require('~/assets/images' + src)
    }
  }
})
  1. 在 Nuxt 配置文件中包含前面的全局混合路径:
// nuxt.config.js
module.exports = {
  plugins: [
    '~/plugins/mixin-utils.js'
  ]
}
  1. 现在,你可以在你的组件中随意使用loadAssetImage函数,如下所示:
// pages/about.vue
<img :src="loadAssetImage(post.image.src)" :alt="post.image.alt">

请注意,我们不需要像导入基本混入那样导入全局混入,因为我们已经通过nuxt.config.js全局注入了它们。但同样,要谨慎而谨慎地使用它们。

您可以在本书的 GitHub 存储库中的/chapter-5/nuxt-universal/mixins/global/中找到这个混入的示例。

混入非常有用。全局混入如全局 Vue 组件在数量过多时很难管理,因此会使您的应用难以预测和调试。因此,明智而谨慎地使用它们。我们希望您现在知道 Vue 组件是如何工作的以及如何编写它们。然而,仅仅知道它们是如何工作和如何编写它们是不够的 - 我们应该了解编写可读性和未来可管理性时需要遵守的标准规则。因此,在结束本章之前,我们将看一些这些规则。

定义组件名称和使用命名约定

在本章和前几章中,我们已经看到并创建了许多组件。我们创建的组件越多,我们就越需要遵循组件的命名约定。否则,我们将不可避免地会遇到混淆和错误,以及反模式。我们的组件将不可避免地会相互冲突 - 甚至与 HTML 元素相冲突。幸运的是,有一个官方的 Vue 风格指南,我们可以遵循以提高我们应用的可读性。在本节中,我们将介绍一些特定于本书的规则。

多词组件名称

我们现有和未来的 HTML 元素都是单词(例如articlemainbody等),因此为了防止冲突发生,我们在命名组件时应该使用多个单词(除了根应用组件)。例如,以下做法被认为是不好的:

// .js
Vue.component('post', { ... })

// .vue
export default {
  name: 'post'
}

组件的名称应该按照以下方式书写:

// .js
Vue.component('post-item', { ... })

// .vue
export default {
  name: 'PostItem'
}

组件数据

我们应该始终使用data函数而不是data属性来处理组件的数据,除了在根 Vue 实例中。例如,以下做法被认为是不好的:

// .js
Vue.component('foo-component', {
  data: { ... }
})

// .vue
export default {
  data: { ... }
}

上述组件中的数据应该按照以下方式书写:

// .js
Vue.component('foo-component', {
  data () {
    return { ... }
  }
})

// .vue
export default {
  data () {
    return { ... }
  }
}

// .js or .vue
new Vue({
  data: { ... }
})

但是为什么呢?这是因为当 Vue 初始化数据时,它会从vm.$options.data创建一个对data的引用。因此,如果数据是一个对象,并且一个组件有多个实例,它们都将使用相同的data。更改一个实例中的数据将影响其他实例。这不是我们想要的。因此,如果data是一个函数,Vue 将使用getData方法返回一个只属于当前初始化实例的新对象。因此,根实例中的数据在所有其他组件实例中共享,这些实例包含它们自己的数据。你可以通过this.$root.$data从任何组件实例中访问根数据。你可以在本书的 GitHub 存储库中的/chapter-5/vue/component-webpack/data//chapter-5/vue/data/basic.html中查看一些示例。

你可以在github.com/vuejs/vue/blob/dev/src/core/instance/state.js#L112上查看 Vue 源代码,了解数据是如何初始化的。

属性定义

我们应该在props属性中定义属性,以便尽可能详细地指定它们的类型(至少)。只有在原型设计时才可以不进行详细定义。例如,以下做法被认为是不好的:

props: ['message']

这应该这样写:

props: {
  message: String
}

或者,更好的做法是这样写:

props: {
  message: {
    type: String,
    required: false,
    validator (value) { ... }
  }
}

组件文件

我们应该始终遵守“一个文件一个组件”的政策;也就是说,一个文件中只写一个组件。这意味着你不应该在一个文件中有多个组件。例如,以下做法被认为是不好的:

// .js
Vue.component('PostList', { ... })

Vue.component('PostItem', { ... })

它们应该拆分成多个文件,如下所示:

components/
|- PostList.js
|- PostItem.js

如果你在.vue中编写组件,应该这样做:

components/
|- PostList.vue
|- PostItem.vue

单文件组件文件名大小写

我们应该只为单文件组件的文件名使用 PascalCase 或 kebab-case。例如,以下做法被认为是不好的:

components/
|- postitem.vue

components/
|- postItem.vue

它们应该这样写:

// PascalCase
components/
|- PostItem.vue

// kebab-case
components/
|- post-item.vue

自闭合组件

当我们的单文件组件中没有内容时,应该使用自闭合格式,除非它们在 DOM 模板中使用。例如,以下做法被认为是不好的:

// .vue
<PostItem></PostItem>

// .html
<post-item/>

它们应该这样写:

// .vue
<PostItem/>

// .html
<post-item></post-item>

这些只是一些基本的规则。还有更多规则,比如编写多属性元素的规则,指令简写,带引号的属性值等等。但是我们在这里突出显示的选定规则应该足够让你完成本书。你可以在vuejs.org/v2/style-guide/找到其他规则和完整的样式指南。

摘要

干得好!在本章中,你学会了全局和局部 Vue 组件之间的区别,如何在 Nuxt 应用程序中注册全局组件,以及如何创建局部和全局 mixin。你还学会了如何通过props属性将数据传递给子组件,如何使用$emit方法从子组件向父组件发出数据,以及如何创建自定义输入组件。然后,你学会了为组件使用key属性的重要性。之后,你学会了如何使用 webpack 编写单文件组件。最后但同样重要的是,你了解了在 Nuxt 和 Vue 应用程序开发中应该遵循的一些规则。

在下一章中,我们将进一步探讨/plugins/目录的使用,通过编写 Vue 中的自定义插件并导入它们来扩展 Nuxt 应用程序。我们还将研究如何从 Vue 社区导入外部 Vue 插件,通过将它们注入到 Nuxt 的$rootcontext组件中创建全局函数,编写基本/异步模块和模块片段,并使用 Nuxt 社区的外部 Nuxt 模块。我们将会对这些进行详细的指导,敬请关注!