Vue-快速启动指南-全-

Vue 快速启动指南(全)

原文:zh.annas-archive.org/md5/056a1fe7509ea158cc95e0fe373880b7

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

直到几年前,直接 DOM 操作是前端开发的标准,jQuery 一直引领潮流。所有这一切都随着现代 JavaScript 库和框架的普及而开始改变,主要是 Angular 和 React。然后,在 2014 年 2 月,Vue 发布了初始版本。

由于大型 IT 公司都支持 Angular 和 React,Vue 如何确立自己的地位并不清楚。最初由单个开发者 Evan You 开发,在短短四年内,没有公司支持的情况下,Vue 从一个单个开发者的有趣小项目发展成了大公司的不太可能的竞争对手,拥有 300 多名贡献者。这不再是一个人的表演。

如今,NASA、GitLab、阿里巴巴、Grammarly、WizzAir、EuroNews、小米、Adobe、Behance、任天堂、Chess.com 等都在使用 Vue。

结论?Vue 会一直存在。虽然人们可能会讨论到底是更好地学习 Elm,还是 React,还是 Angular,还是 Ember,或者完全不同的东西,但这种讨论基本上是无关紧要的。每个库和框架都有一些可提供的东西,最终,只是试用一下,看看它是否适合你。

我们开发人员需要接受必须跟上技术潮流的必要性,并接受学习新框架和范式只是我们职业生涯的一部分这一事实。因此,问题不是我们应该学习 Vue,还是学习其他经过考验和证明的技术。

Vue 已经取得了它的地位,并且正在与大公司同台竞技。唯一的问题是,“我该如何高效地学习它?”这本书就是试图回答这个问题。

这本书适合谁

这本书面向没有使用 Vue 或其他 VDOM JavaScript 库经验的初学者到中级前端开发人员。读者具有一些 JavaScript 和 CSS 知识将会有所帮助。它旨在迅速让读者了解 Vue 的设置方式以及其各个部分是如何协同工作的。它旨在简洁地为您概述几乎整个 Vue 领域,并提供大量示例。

这本书的目标很简单 - 快速高效地向您介绍 Vue,并让您轻松进入框架,而不需要大量的时间和精力投入。预期结果是,通过阅读本书,您将获得巨大的投资回报 - 获得足够的框架实际知识,以至于在您阅读本书的时间不长之后,您就有信心去应对一些更高级的 Vue 项目和概念。

本书涵盖的内容

第一章,“介绍 Vue”,讨论了 Vue 是什么,并开始使用 mustache 模板。我们探讨了 Vue 解决的问题以及使用 Vue 的原因。

第二章,“Vue 2 的基本概念”,讨论了响应性、计算属性和方法。我们还介绍了组件、模板、props、watchers 和生命周期钩子。

第三章,“使用 Vue-CLI、组件、Props 和插槽”,展示了如何安装 vue-cli 以及如何设置代码编辑器以更有效地使用 Vue。我们检查了基于 vue-cli 的项目结构,看看如何向子组件添加基本功能,并学习了如何将数据从子组件传递到父组件。

第四章,“过滤器和混入”,描述了如何使用过滤器。我们看看语法、用例和一些示例。我们还研究了如何使用混入。

第五章,“制作自己的指令和插件”,探讨了通过制作自定义指令来扩展 Vue 的方法。我们还从头开始构建自己的插件,并学习如何通过 npm 发布它。

第六章,“过渡和动画”,逐步引导读者比较 CSS 过渡和 CSS 动画,了解它们之间的区别以及如何开始将它们与 Vue 集成。然后,我们讨论了在 Vue 中组织过渡和动画的多种方式 - 使用过渡和过渡组件、使用过渡钩子作为 CSS 类、使用命名过渡钩子以及使用 JavaScript 过渡钩子。

第七章,使用 Vuex,从零开始向读者展示了状态的真正含义以及其重要性。它还解释了为什么需要存储中心化状态以及其内部工作原理。我们还会尝试一些代码示例,从这个中心化存储中控制我们的应用程序。

第八章,使用 Nuxt.js 和 Vue-Router,描述了单页应用程序的工作原理,它们存在的问题,以及如何通过服务器端渲染和代码拆分来克服这些问题。然后,我们将看到如何使用几个页面和一些添加的过渡效果构建一个非常简单的 Nuxt.js 应用程序。

为了从这本书中获得最大收益

如果您能做到以下几点,这本书将对您最有帮助:

  • 编写基本的 HTML、CSS 和 JavaScript 代码

  • 大致了解互联网和浏览器的工作原理

  • 有一些使用代码编辑器和控制台程序的经验

  • 愿意下载示例(或从 CodePen 中派生)

本书中的 JavaScript 代码大多以 ES5 编写,但随着书的进展,有时也会出现 ES6。之所以使用 ES5,是因为不假定读者了解 ES6 语法。同样,也不假定读者以前没有使用过它——因此,做出了一个妥协:不专注于 ES6 的特性,但也不完全忽视它们。作者认为,这种方法将把重点转移到重要的地方:理解 Vue。

下载示例代码文件

您可以从您在www.packt.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packt.com/support并注册,以便文件直接发送到您的邮箱。

您可以按照以下步骤下载代码文件:

  1. www.packt.com上登录或注册。

  2. 选择“支持”选项卡。

  3. 单击“代码下载和勘误”。

  4. 在搜索框中输入书名并按照屏幕上的说明操作。

下载文件后,请确保使用最新版本的解压缩软件解压缩文件夹。

  • WinRAR/7-Zip 适用于 Windows

  • Zipeg/iZip/UnRarX 适用于 Mac

  • 7-Zip/PeaZip 适用于 Linux

该书的代码包也托管在 GitHub 上:github.com/PacktPublishing/Vue.js-Quick-Start-Guide。如果代码有更新,将在现有的 GitHub 存储库上进行更新。

我们还有来自丰富图书和视频目录的其他代码包,可在github.com/PacktPublishing/上找到。去看看吧!

下载彩色图片

我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图片。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/9781789344103_ColorImages.pdf

使用的约定

本书中使用了许多文本约定。

CodeInText:表示文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。例如:"将下载的WebStorm-10*.dmg磁盘映像文件挂载为系统中的另一个磁盘。"

代码块设置如下:

...
data: {
  // the model goes here
}
...

当我们希望引起您对代码块的特定部分的注意时,相关行或项将以粗体显示:

div,.thetemplate {
  font-size: 30px;
  padding: 20px;
  color: limegreen;
  font-family: Arial;

任何命令行输入或输出都以以下方式编写:

cd quickstart-vue

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词会以这种方式出现在文本中。例如:"从管理面板中选择系统信息。"

警告或重要提示会以这种方式出现。技巧和窍门会以这种方式出现。

第一章:介绍 Vue

在本章中,我们将探讨如何开始学习 Vue 2。本章将向您展示快速入门的最简单方法,以及如何借助可用的 SaaS 平台轻松跟踪您的进度。

我们还将探讨 Vue 为什么变得如此受欢迎,以及为什么我们应该使用它。

此外,我们将讨论 Vue 的基本构建模块:mustache 模板、指令、修饰符、方法和计算属性。

在此过程中,我们将看到许多实际的例子。让我们首先看看 Vue 到底是什么。

在本章中,我们将看一下以下主题:

  • 什么是 Vue?

  • Vue 解决了哪些问题?

  • 为什么使用 Vue?

什么是 Vue?

Vue 是一个简单易用的 JS 框架,于 2013 年出现。它成功地将一些优秀的想法从 Angular 和 React 中提取出来,并结合在一个易于使用的包中。

与其他流行的前端框架相比,Vue 在简单性和易用性方面表现出色。

让我们看看如何开始使用它。

开始使用 Vue2 的最快方法

在过去的十年里,很多用于网页开发的工具已经转移到了网络上,所以让我们顺应潮流,在codepen.io/上开始一个新的项目。

您不必成为codepen.io/的成员才能在那里创建项目——您可以使用用户名Captain Anonymous保存它们。但最好还是注册一个账户,这样您的所有实验都在一个地方。

一旦您将浏览器导航到codepen.io,您将看到以下屏幕:

点击创建下拉菜单(在主导航栏中,位于屏幕右上角),然后点击新建项目。一旦您这样做了,您将看到默认的编辑器设置:

接下来,点击屏幕右上角的设置按钮,在弹出的窗口中选择 JavaScript:

接下来,在快速添加下拉字段中,选择 Vue 选项。一旦您这样做了,第一个输入框将填写当前的 Vue 的压缩版本,它是从 Cloudflare CDN 提供的,或者更具体地说,是从这个链接提供的:cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.min.js

就是这样!我们已经准备好在我们的 Codepen 项目中开始使用 Vue2 了。

关于 Vue 要理解的一件事是,它使我们的 HTML 动态化。这是通过添加胡须语法来实现的。这种语法非常容易理解。我们只需将其插入到 HTML 元素中。例如,我们可以像这样向h1标签添加胡须语法:

<h1>{{ heading1 }}</h1>

因此,让我们详细了解一下这是如何工作的。随意在您自己的 pen 上工作或在此处查看示例:codepen.io/AjdinImsirovic/pen/rKYyvE

胡须模板示例

让我们开始使用我们的第一个 pen:

<div id="entryPoint">
  <h1>Just an h1 heading here</h1>
  <h2>Just an h2 heading here</h2>
  <p>Vue JS is fun</p>
</div>

我们现在可以在 CodePen 预览窗格中看到我们的 HTML 正在呈现,屏幕上打印出以下文本:

Just an h1 heading here Just an h2 heading here Vue JS is fun

请注意,CodePen 应用程序通常会在不保存的情况下更新预览窗格,这比刷新浏览器要好得多——在本地项目上工作时必须这样做。尽管如此,经常保存您的 CodePen 项目是很好的,以免丢失任何更改(在浏览器冻结或发生其他异常情况时)。

接下来,让我们将以下 Vue 代码添加到我们 pen 内部的 JS 窗格中:

new Vue({
  el: '#entryPoint',
  data: {
     heading1: 'Just an h1 heading here',
     heading2: 'heading 2 here',
     paragraph1: 'Vue JS'
  }
})

最后,让我们更新 HTML,以便 Vue 代码可以发挥其魔力:

<div id="entryPoint">
  <h1>{{ heading1 }}</h1>
  <h2>Just an {{ heading2 }}</h2>
  <p>{{paragraph1}} is fun</p>
</div>

在前面的代码示例中,我们可以看到我们如何使用胡须模板将数据动态插入到我们的 HTML 中。

通过简单地将数据对象的键传递到我们的 HTML 标记中,并用开放的{{和关闭的}}标记将键括起来,可以实现胡须模板。

如前所述,CodePen 将自动更新预览窗格,但这不会影响预览,因为我们实际上产生的输出与我们仅使用纯 HTML 时所做的输出相同。

现在我们可以通过简单地更改数据输入中的键值对来玩耍:

new Vue({
  el: '#entryPoint',
  data: {
     heading1: 'This is an h1',
     heading2: 'h2 heading',
     paragraph1: 'Vue2'
  }
})

这次,输出将自动更新为这样:

这是一个 h1

只是一个 h2 标题

Vue2 很有趣

我们也可以更改我们的入口点。例如,我们可以让 Vue 只访问p标签:

new Vue({
  el: 'p',
  data: {
     heading1: 'This is an h1',
     //heading2: 'h2 heading',
     paragraph1: 'Vue2'
  }
})

更改后,我们的预览窗格将显示以下内容:

{{ heading1 }}

只是一个{{ heading2 }}

Vue2 很有趣

从这个输出中,我们可以得出结论,如果发生以下任何一种情况,我们的胡须模板将被呈现为常规文本:

  • 我们的入口点没有引用数据

  • 我们的数据中不存在的输入

我们还看到了我们的入口点可以是任何类型的选择器。您可以将其视为类似于在 jQuery 中定位不同的元素。

例如,我们可以将更复杂的选择器作为我们应用的入口点:

new Vue({
  el: 'div#entryPoint',
  data: {
     heading1: 'This is an h1',
     heading2: 'h2 heading',
     paragraph1: 'Vue2'
  }
})

使用 Vue 的数据选项作为函数

请注意,我们的 Vue 实例的data选项可以是对象,也可以是函数。数据作为对象的示例可以在先前的代码中看到。使用数据作为函数也很容易。

数据作为对象与可重用组件不兼容。因此,通常来说,将数据作为函数使用是使用 Vue 中数据选项的更有用的方式。

让我们看另一个笔记。这次,我们将使用数据选项作为函数,而不是作为对象。笔记在这里:codepen.io/AjdinImsirovic/pen/aKVJgd。我们唯一要做的改变是我们的 Vue 代码:

new Vue({
  el: '#entryPoint',
  data() {
    return {
     heading1: 'Just an h1 heading here',
     heading2: 'heading 2 here',
     paragraph1: 'Vue JS data as a function'
    }
  }
})

现在我们熟悉了 Vue 语法的基础知识,让我们看看它可以用来做什么。

Vue 解决了什么问题?

不打算列出一个详尽的清单,让我们快速地强调一些 Vue 最大的优点:

  • Vue——jQuery 的继任者?

  • Vue 对于初学者来说是一个很好的学习工具

  • Vue 是一个多才多艺和渐进的框架

  • Vue 是一个用于动画和交互的很棒的工具

  • Vue 的方法与其他现代前端框架和库类似

接下来,让我们简要地概述每一点。

Vue,jQuery 的继任者

著名的 jQuery 库于 2006 年出现。当它出现时,它做了一些非常好的事情:

  • 它使得编写跨浏览器的 JavaScript 变得更容易,这在当时是一个很大的优势,因为它大大减少了开发人员处理各种浏览器怪癖和不一致性的需求

  • 它有一个简单的语法,使得更容易定位和操作特定的 DOM 节点,这在他们的座右铭“写得更少,做得更多”中表达得很好

  • 它是学习 JavaScript 的绝佳入门点

  • 它有一个很棒的 API,使得使用 Ajax 变得简单和容易

然而,自那时以来发生了很多变化,变得更好了。

可以说,2006 年至今在 JavaScript 领域发生的最大改进是虚拟 DOM。

虚拟 DOM 是一种范式转变:我们不再需要编写过程化、混乱的 JS 来指示浏览器如何遍历和操作 DOM。我们现在不再告诉浏览器如何更新 DOM,而是告诉它更新什么。或者更具体地说,我们告诉一个框架要更新什么——像 View 或 React 这样的框架。虚拟 DOM 的实际实现是特定于框架的,目前不需要太在意。

我们现在可以通过使用处理底层框架的虚拟 DOM 实现的声明式代码来间接地使用 DOM。这种抽象是使 jQuery 或多或少变得多余的一件事。

当然,由于仍然有很多应用程序由 jQuery 提供动力,并且由于遗留代码有粘性的倾向,jQuery 在未来几年内仍将活跃。

然而,我们对 DOM 操作方式的范式转变使得 Vue 成为与 jQuery 竞争激烈的对手,成为当今最受欢迎的游戏。

Vue 还有其他优势:它是学习当今前端开发的绝佳起点。入门门槛非常低。

一个初学者的学习工具

如果一个 jQuery 开发人员面临学习现代前端框架/库(React、Angular、Vue、Ember...)的选择,哪一个可能是最容易入门的呢?

当然是 Vue!

正如我们已经看到的,使用 Vue 可以简单到只需导入 CDN。由于我们人类天生喜欢小而频繁的胜利,Vue 似乎是一条快乐的道路。这并不是说开发人员不应该尝试学习其他前端框架。只是 Vue 似乎是最简单的入门方式,也是最快速提高生产力的最佳方式。

一个多才多艺的渐进式框架

Vue JS 的官方网站称 Vue 为渐进式 JavaScript 框架。这意味着您可以逐步将 Vue 添加到现有的服务器端项目中。基本上,您可以将 Vue 添加到您网站的一个简单部分。难怪 Laravel 选择在其前端与 Vue 捆绑在一起。

但你不必只在这里和那里使用 Vue。您还可以使用 Vuex 和 Vue-Router 进行扩展。这使得 Vue 非常灵活,可以在许多不同的场景中使用。

动画和过渡工具

如果您需要进行高性能的动画和过渡效果,那就毫无疑问选择 Vue!Vue 的动画 API 非常易于理解,使用起来非常愉快。在 Vue 中做动画是如此容易,以至于您会惊讶于您在很短的时间内可以完成多少工作。

与其他现代前端框架和库类似的功能

与其他现代前端框架(如 React 和 Angular)一样,Vue 也具有以下特点:

  • 虚拟 DOM

  • 命令行界面(Vue-cli)

  • 状态管理(Vuex)

  • 路由(Vue-Router)

然而,似乎 Vue 的核心团队正在尽最大努力使 Vue 尽可能易于接近。这在几个例子中是显而易见的:

  • 他们为了避免设置 Vue-cli 的麻烦所付出的努力,这使得入门非常容易

  • 复杂的工具链的缺乏

  • Vue 的 API 的简单性

就像官方项目的网站所述,Vue 是易于接近,多才多艺和高性能的。

为什么使用 Vue?

我们已经在前一节讨论了 Vue 解决的问题。在本节中,我们将看一下为什么与之合作是一种乐趣的实际例子:

  • 声明性代码(我们告诉 Vue 要做什么,而不是如何做)

  • 易于理解的语法(尽可能简洁)

  • 感觉适合各种项目

声明性代码

让我们将原生 JavaScript 代码与 Vue JavaScript 代码进行比较。

对于这个例子,我们将打印出一个数组的成员。

在原生 JavaScript 中,这将是代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Document</title>
  <style>
    .list-item {
      background: white;
      color: gray;
      padding: 20px;
      margin: 20px;
    }
  </style>
</head>
<body>

  <script>
    var arr1 = ['a','b','c'];
    var unorderedList = document.createElement('ul');
    unorderedList.style.cssText = "background:tomato; width: 
    400px;height:400px";
    document.body.appendChild(unorderedList);
    for (var i=0; i<3; i++) {
      var listItem = document.createElement('li');
      listItem.className = "list-item";
      unorderedList.appendChild(listItem);
      listItem.innerHTML = arr1[i];
    }
  </script>
</body>
</html>

在这个文件中,重点应该放在script标签内的代码上。

您可以在此 URL 的表单中看到此示例:codepen.io/AjdinImsirovic/pen/xzPdxO

在这段代码中,我们正在做几件事:

  1. 我们正在设置array1,它将稍后填充我们将动态创建的列表项

  2. 我们正在创建一个ul——一个无序列表元素,它将包裹我们所有的列表项(所有我们的li元素)

  3. 我们正在为我们的ul设置样式

  4. 我们正在将unorderedList附加到我们文档的主体

  5. 接下来,我们使用for循环创建三个li元素

  6. 仍然在for循环中,我们为每个列表项添加一个类

  7. 然后我们将它们中的每一个附加到无序列表元素

  8. 最后,我们为每个列表项添加innerHTML

对于这段代码,可能会有很多反对意见。我们本可以使用forEach;我们本可以避免以我们的方式添加样式,而是从一个单独的文件中调用 CSS。但最大的反对意见是这段代码有多脆弱。让我们将这段代码与用 Vue 编写的相同内容进行对比。

在 Vue 中,我们的代码将如下所示:

<!-- HTML -->
<ul>
  <li v-for="entry in entries">
     {{ entry.content }}
  </li>
</ul>

// JS
var listExample = new Vue ({
  el: "ul",
  data: {
    entries: [
      { content: 'a'},
      { content: 'b'},
      { content: 'c'}
    ]
  }
})

此示例的代码可以在这里找到:codepen.io/AjdinImsirovic/pen/VdrbYW

正如我们在简单的一瞥中所看到的,与在原生 JavaScript 中实现的相同代码相比,Vue 的代码更容易理解和推理。

这里的 el 是我们 Vue 应用的入口点。data 选项是我们的 Vue 应用将使用的实际数据。

这种设置还有另一个主要好处:一旦你了解了 Vue 的工作原理,任何使用 Vue 的其他项目对你来说都会变得简单明了,这将提高生产力和效率。

Vue 的方式促进更快速地完成更多事情。

对各种项目来说都是合适的选择

Vue 的一个优点是可以逐步实现。如果你只是想在 Vue 中进行一个快速、简单的实验,没有问题。你可以在不到一分钟的时间内开始使用 Vue。

这使得它非常适合转换传统项目、从头开始构建项目,或者进行简单的实验。

Vue 也在迅速成熟。有一个充满活力的 Vue 社区,许多开发人员在不断地为其努力。例如,人们选择 React 而不是 Vue 的一个论点是 Vue 中缺乏用于构建原生移动应用的框架。这已经不再是问题了:Vue Native 从 2018 年 6 月起就可用了。你可以在 github.com/GeekyAnts/vue-native-core 查看它,或者在 vue-native.io/ 了解更多信息。

考虑到所有这些,学习 Vue 对任何人来说都是一个不错的投资回报,尤其是前端开发人员。

易于理解的语法

在这个非常简单的 Vue 应用示例中可以注意到一件事,就是使用了 v-for HTML 属性。

指令

Vue 中的所有 v-* 属性都被称为 指令,这是从 Angular 中借鉴过来的。

指令的概念非常有趣。它们使代码更易于理解、更易于思考,也更易于使用。

在 Vue 中还有其他指令,我们将在本书中广泛使用。现在,让我们列出其中一些:v-bindv-cloakv-forv-elsev-else-ifv-modelv-onv-oncev-textv-html

一个有用的指令示例是 v-modelv-model 指令用于使表单具有响应性;它帮助我们在用户输入事件中更新数据。虽然这个话题对于 Vue 的初学者来说可能有点高级,但这种复杂性被处理得如此优雅,以至于即使初学者也应该很容易看出代码中发生了什么:

<!-- HTML -->
<div id="app">
  <span>Enter the weight in kilograms:</span>
  <input v-model="someNum" type="number">
  <div>The weight in pounds is: {{ someNum * 2.20 }}</div>
</div>

// js
new Vue({
  el: '#app',
  data() {
    return {
      someNum: "1"
    }
  }
})

如您所见,{{ someNum }}的值绑定到用户在输入字段中键入的任何内容。换句话说,基于用户输入,底层数据模型someNum的值将发生变化。

要查看前面示例的 pen,请访问codepen.io/AjdinImsirovic/pen/pKdPgX

修饰符

Vue 中的指令还可以通过修饰符进行进一步扩展。

有关指令中修饰符的官方文档链接在此:vuejs.org/v2/guide/forms.html#Modifiers

要使用修饰符,我们只需将其附加到指令上。最简单的示例可能看起来像这样:

<!-- HTML -->
<div>
  <input v-model.trim="userInput" placeholder="type here">
  <p>You have typed in: {{ userInput }}</p>
</div>

// js
new Vue({
  el: 'div',
  data() {
    return {
      userInput: ""
    }
  }
})

我们刚刚将trim修饰符附加到v-model指令上。

您可以在此链接查看此代码的示例:codepen.io/AjdinImsirovic/pen/eKeRXK

此修饰符将修剪用户在输入字段中键入的任何空白(例如空格或制表符)。

在继续查看 Vue 语法的概述之前,让我们也提一下v-on指令,它用于事件处理。这里是一个快速示例:

<!-- HTML -->
<div id="example-1">
 <button v-on:click="counter += 1">Add 1</button>
 <p>The button above has been clicked {{ counter }} times.</p>
</div>

// JS var example1 = new Vue({
 el: '#example-1',
 data: {
 counter: 0
 }
})

Vue 甚至为v-on提供了快捷语法:@符号。因此,我们可以用@click替换v-on:click,我们的 Vue 计数器仍然可以工作。

要在codepen.io/中查看此示例,请访问以下网址:codepen.io/AjdinImsirovic/pen/PaOjvz

Vue 方法

Vue 实例中的methods选项只列出了该 Vue 实例(或 Vue 组件)上存在的所有函数。

methods选项与 Vue 实例的数据一起工作。接下来是这个概念的一个简单演示:

// HTML
<div id="definitions">
  <!-- 'whatIsVue' and 'whyUseVue' are functions defined in the 'methods' option in the Vue instance -->
  <button id="btn" v-on:click="whatIsVue">What is Vue?</button>
  <button id="btn" v-on:click="whyUseVue">Why use Vue?</button>
</div>

// JS
var definitions = new Vue({
 el: '#definitions',
 data: {
 name: 'Vue.js'
 },
 // define methods (functions) under the `methods` object
 methods: {
   whatIsVue: function () {
    console.info(this.name + ' is a Progressive Front-end Framework')
   },
   whyUseVue: function () {
    alert('Because ' + this.name + ' is nice.')
   }
 }
})

正如我们所看到的,data选项保存了Vue.js字符串,可以通过name键访问。

methods选项中,我们可以看到两个函数:whatIsVuewhyUseVuewhatIsVue函数接受点击事件并将name中的值记录到控制台。methods选项中的whyUseVue函数工作方式类似。

此代码可以在此地址的 pen 中查看:codepen.io/AjdinImsirovic/pen/yEPXdK

计算属性和观察者

计算属性用于避免复杂逻辑增加视图的臃肿。换句话说,计算属性对于隐藏 HTML 的复杂性是有用的,因此使我们的 HTML 易于理解、易于使用和声明性。换句话说,当我们需要从data选项计算一些值时,我们可以借助计算属性来实现。

以下示例的完整代码可以在codepen.io/AjdinImsirovic/pen/WyXEOz中查看:

<!-- HTML -->
<div id="example">
  <p>User name: "{{ message }}"</p>
  <p>Message prefixed with a title: "{{ prefixedMessage }}"</p>
</div>

// JS
var example = new Vue({
  el: '#example',
  data: {
    userName: 'John Doe',
    title: ''
  },
  computed: {
    // a computed getter
    prefixedMessage: function () {
      // `this` points to the Vue instance's data option
      return this.title + " " + this.userName
    }
  }
})

计算属性是被缓存的。只要计算属性的依赖项没有发生变化,Vue 将返回计算属性的缓存值。

观察者并不像计算属性那样经常使用。换句话说,观察选项要比计算属性选项少用。观察者通常用于具有变化数据的异步或成本高昂的操作。

观察者与响应式编程有关;它们允许我们通过时间观察事件序列并对特定数据属性的变化做出反应。

我们将在后面的章节中介绍计算属性和观察者的主题。目前,知道它们存在于 Vue 中并且被广泛使用就足够了。

总结

在本章中,我们看了如何通过codepen.io快速开始使用 Vue。我们还讨论了 Vue 中一些最重要的思想和概念,例如学习 Vue 2 的最快和最开发者友好的方式。我们了解了 Vue 解决了什么问题,它的优势是什么,以及为什么有时被称为新的 jQuery。我们了解了花括号模板、Vue 的声明性代码和易于理解的语法。最后,我们介绍了指令、修饰符、方法、计算属性和观察者。

在下一章中,我们将看到什么是响应式编程以及它如何应用在 Vue 中。我们还将进一步扩展本章涵盖的概念,并介绍 Vue 的一些其他特性。

第二章:Vue 2 的基本概念

在本章中,我们将讨论 Vue 中的数据驱动视图。我们还将研究如何使用指令来操作 DOM。接下来,我们将学习组件是什么以及如何创建它们,并涵盖与模板、方法、数据、计算属性和观察者相关的概念。

所有组件都有一个生命周期,并且我们有特殊的方法在其生命周期的某些时刻访问组件。这些方法被称为生命周期钩子,我们也将在本章中对它们进行讨论。

在本章中,我们将学习以下内容:

  • Vue 中的数据驱动视图

  • 计算属性和方法以及如何使用它们

  • 理解组件、模板和属性

  • 在 Vue 中构建组件模板的方式

  • 使用 Vue 组件和v-*指令快速原型网站

  • 在 Vue 中利用观察者

  • 生命周期钩子的重要性以及如何在 Vue 中插入这种功能

Vue 中的数据驱动视图

Vue 中的数据驱动视图是通过响应性来实现的。

什么是响应性?

为了更好地理解这个概念,让我们看一个例子代码,其中没有响应性。我们将使用一个非常类似于我们在上一章中比较 Vue 和原生 JS 时的例子。在原始的例子中,使用 JavaScript,我们创建了一个无序列表,里面有三个列表项。三个列表项的值是从我们声明的数组中添加的,并且使用 for 循环将无序列表填充了这些列表项。

这一次,我们将做一些稍微不同的事情。要将示例作为一个 pen 查看,请访问codepen.io/AjdinImsirovic/pen/JZOZdR

在这个非响应式的例子中,我们预先定义了数组的成员作为变量。然后我们用这些变量填充数组,并将它们作为无序列表的列表项打印到屏幕上,这个无序列表被附加到文档中:

var a = 1;
var b = a + 1;
var c = b + 2;
var arr1 = [a,b,c];
var unorderedList = document.createElement('ul');
document.body.appendChild(unorderedList);
for (var i=0; i<3; i++) {
  var listItem = document.createElement('li');
  listItem.className = "list-item";
  unorderedList.appendChild(listItem);
  listItem.innerHTML = arr1[i];
}
arr1[0] = 2;
for (var i=0; i<3; i++) {
  var listItem = document.createElement('li');
  listItem.className = "list-item";
  unorderedList.appendChild(listItem);
  listItem.innerHTML = arr1[i];
}

然而,当我们改变数组的一个成员并再次重复 for 循环时会发生什么?正如我们在代码中所看到的,第一个和第四个列表项是不同的。第一个值是1,第二个值是2。为了更明显,这些项目是以粗体红色文本和灰色背景显示的。第一个值是var a的初始值。第二个值是使用这行代码更新的var a的值:arr1[0] = 2

然而,变量bc的值在第二个 for 循环中没有更新,尽管我们已经分别定义了变量bc,它们分别是变量a增加 1 和 2。

因此,我们可以看到 JavaScript 本身并没有响应性。

就 Vue 而言,响应性是指 Vue 跟踪变化的方式。换句话说,响应性是指状态变化如何在 DOM 中反映出来。实际上,这意味着当对data进行更改时,该更改将传播到页面,以便用户可以看到它。因此,说Vue 是响应式就等同于说Vue 跟踪变化。作为一个概念,就是这么简单。

Vue 是如何实现这一点的呢?

Vue 将其数据存储在data选项中,这可以是一个函数或一个对象:

...
data: {
  // the model goes here
}
...

data模型的任何变化都会反映在视图(屏幕)上。Vue 通过 getter 和 setter 实现了这种响应性。当data对象被 Vue 实例接收时,data对象的所有属性都将被更新为 getter 和 setter。这是通过Object.definePropertyAPI 来实现的。

计算属性和方法

Vue 中响应性的实用性可以用计算属性和方法之间的区别来描述。

正如我们之前提到的,Vue 实例可以有计算属性、方法,或者计算属性和方法两者兼有。那么,这两者之间有什么区别呢?

方法只是在被调用时运行。另一方面,计算属性是被缓存的,这意味着它们只在基础数据模型发生变化时才会运行。这通常是以计算属性的依赖关系来描述的。此外,方法可以有参数,而计算属性则不行。

这些依赖关系到底是什么?

考虑这个简单的 Vue 应用程序,可以在这个链接中找到:codepen.io/AjdinImsirovic/pen/qKVyry

这是这个简单应用程序的代码:

<!--HTML-->
<div id="example">
  <p>Enter owner name and the thing that is owned:
    <input v-model="ownerName" placeholder="enter owner">
    <input v-model="thing" placeholder="enter thing">
  </p>
  <span>{{ ownerName }}</span>
  <span> has a </span>
  <span>{{ thing }}</span>
</div>

// JS
var example = new Vue({
  el: '#example',
  data: {
    ownerName: 'e.g Old McDonald',
    thing: 'e.g cow'
  },
  computed: {
    // a computed getter
    ownerHasThing: function () {
      // `this` points to the Vue instance's data option
      return this.ownerName + " " + this.thing
    }
  }
})

这段代码将在屏幕上产生这样的输出:

首先,我们可以看到视图中有这样一个奇怪的“has a”文本行。问题在于我们没有使用我们的ownerHasThing计算属性。换句话说,HTML 中的这三行是完全多余的:

<span>{{ ownerName }}</span>
<span> has a </span>
<span>{{ thing }}</span>

另外,如果我们只想在两个输入字段都填写完毕并且焦点已经移出输入框,或者按下Enter键后才运行计算属性,该怎么办呢?

这可能看起来是一个相对复杂的事情。幸运的是,在 Vue 中非常容易实现。

让我们来看一下更新后的代码(也可以在这里找到:codepen.io/AjdinImsirovic/pen/aKVjqj):

<!--HTML-->
<div id="example">
  <p>Enter owner name:
    <input v-model.lazy="ownerName" placeholder="enter owner">
  </p>
  <p>Enter thing owned:
    <input v-model.lazy="thing" placeholder="enter thing">
  </p>
  <h1 v-if="ownerName && thing">{{ ownerHasThing }}</h1>
</div>

JavaScript 代码只有轻微的不同:

var example = new Vue({
  el: '#example',
  data: {
    ownerName: '',
    thing: ''
  },
  computed: {
    // a computed getter
    ownerHasThing: function () {
      // `this` points to the Vue instance's data option
      return this.ownerName + " has a " + this.thing
    }
  }
})

我们可以得出结论,计算属性只是一些数据依赖关系,对它们进行了一些计算。换句话说,ownerHasThing是一个计算属性,它的依赖是ownerNamething

每当ownerNamething发生变化时,ownerHasThing计算属性也会更新。

然而,ownerHasThing不会总是更新,因为它被缓存了。相反,方法总是会更新;也就是说,它总是会运行,无论数据模型是否发生了变化。

这可能看起来不是非常重要的区别,但考虑一种情况,你的方法需要从第三方 API 获取数据,或者有很多代码需要运行。这可能会减慢速度,这就是为什么在这种情况下,使用计算属性是正确的方法。

在我们结束本节之前,让我们快速回顾一下之前示例中的代码。

在 HTML 中,我们使用了v-model.lazylazy修饰符会等待用户要么点击输入框外部,要么按下键盘上的Enter键,或者离开输入框(比如按下Tab键)。

在 HTML 中,我们还使用了v-if指令,并给它传递了ownerName && thing。然后,我们添加了双大括号模板:{{ ownerHasThing }}v-if指令会等待直到ownerNamething在数据对象中更新。因此,一旦两个输入框都填写完毕并且不再聚焦,计算属性才会更新底层数据模型,然后才会在屏幕上打印出{{ ownerHasThing }}消息。

在下一节中,我们将看看如何使用模板和组件。

理解组件、模板和 props

首先,让我们看看如何在 Vue 中创建一个组件。首先,我们像这样指定组件:

Vue.component('custom-article', {
  template: `
    <article>
      Our own custom article component!<span></span>
    </article>`
})
new Vue({
    el: '#app'
})

组件是一段我们给予自定义名称的代码块。这个自定义名称可以是我们想到的任何东西,它是整个代码块的一个单一标签,以自定义 HTML 标签的形式。在前面的例子中,我们将articlespan标签分组,并给该自定义标签命名为custom-article

组件使用 kebab-case 命名。

这个组件的代码可以在 Codepen 上找到:codepen.io/AjdinImsirovic/pen/xzpOaJ

现在,要创建我们组件的一个实例,我们只需在我们的 HTML 中使用<custom-article>开放和关闭标签,就像这样:

<main id="app">
    <custom-article></custom-article>
</main>

我们的 custom-article 组件被称为组件。

父级是实际的 Vue 实例。

请注意,即使没有组件,您也可以使用字符串模板。您只需将模板选项添加到您的 Vue 实例中,就像这样:

//HTML
<main id="app"></main>
//JS
new Vue({
  el: '#app',
  template: '<article>A string template without a component!<span></span></article>'
})

前面例子的示例代码可以在这里找到:codepen.io/AjdinImsirovic/pen/RJxMae

接下来,我们将看到如何通过propsdata选项来改进我们的组件。

添加 props 和 data 以改进组件

为了使我们的custom-article组件更有用,我们将向其添加一个props选项,就像这样:

Vue.component('custom-article', {
  props: ['content'],
  template: '<article>{{content}}</article>'
})
new Vue({
  el: '#app'
})

Props 是从父级向子级传递数据的一种方式。它们是父级和子级之间数据的单向流动。Props 总是被定义为一个数组。

前面例子的代码可以在这里找到:codepen.io/AjdinImsirovic/pen/KeZNPr

我们在组件中注册了一个 prop,现在我们可以在 HTML 中使用它作为一个名为与我们的 prop 相同的属性:

<main id="app">
  <custom-article content="This component was made with the help of a prop."> 
  </custom-article>
</main>

当我们需要对组件进行较小的更改而不必制作一个全新的组件时,我们使用 props。它们帮助我们重复使用我们已经拥有的东西。

在下一节中,我们将使用 Vue 实例的data对象向我们的custom-article组件添加内容。

使用数据对象向我们的组件添加内容

这个例子的代码笔可以在codepen.io/AjdinImsirovic/pen/QxadmE找到。

在我们的 HTML 中,我们将代码更改为以下内容:

<main id="app">
  <custom-article v-bind:content="datacontent"> 
  </custom-article>
</main>

在我们的 JS 中,我们将更新我们的 Vue 代码如下:

Vue.component('custom-article', {
  props: ['content'],
  template: '<article>{{content}}</article>'
})
new Vue({
    el: '#app',
    data: {
      datacontent: 'This component was made with the help of a data object in the Vue instance'
    }
})

在前面的例子中,我们使用v-bind指令将我们的custom-article组件中的contentprop 绑定到我们的data对象的datacontent属性。

如果你仔细思考这段代码,你会发现 props 几乎就像是命名变量(在示例中,prop 的变量namecontent)。Props 只是将从父组件接收到的数据传递给子组件。

我们还可以用另一种方式来做这件事。我们可以将数据传递给组件,而不是在 Vue 实例内部使用数据;只是这一次它必须是一个数据函数。以下是此实现的完整代码:

// HTML
<main id="app">
  <custom-article></custom-article>
</main>

// JS
Vue.component('custom-article', {
  template: '<article>{{datacontent}}</article>',
  data: function() {
    return {
      datacontent: 'This component was made with the help of a data function in the Vue component called custom-article'
    }
  }
})
new Vue({
    el: '#app'
})

要查看上一个示例的代码,请访问codepen.io/AjdinImsirovic/pen/VdyQzW

如果我们将数据作为对象而不是函数使用,那么响应性将适用于组件的所有实例。由于组件的主要目的是可重用的,因此重要的是要记住在这种情况下数据必须是一个函数。

Props 也可以被定义为对象,这样我们可以给它们更多的信息:验证传入的数据,设置默认值(如果没有数据传入的话),等等。

在以下示例中,我们声明我们的custom-article组件期望父组件传递一个名为message的 prop,或者是字符串类型,这是必需的:

<!--HTML-->
<div id="app">
  <custom-article :message-being-passed="datacontent"></custom-article>
</div>

//JS
Vue.component('custom-article', {
  props: {
    messageBeingPassed: {
      type: String,
      required: true,
      default: 'Hello Vue'
    }
  },
  template: `<div class="thetemplate">{{ message }}</div>`
});

new Vue({
  el: "#app",
  data: function() {
    return {
      datacontent: 'This component was made with the help of a data function in the Vue component called custom-article, and the data passed was validated with the help of the props object inside the Vue component'
    }
  }
})

//CSS
.thetemplate {
  font-size: 30px;
  padding: 20px;
  color: limegreen;
  font-family: Arial;
  border: 3px solid green;
  border-radius: 10px;
}

此示例可在codepen.io/AjdinImsirovic/pen/mKpxGZ找到。

假设我们注释掉了 Vue 实例的data函数中的datacontent属性。你能猜到会发生什么吗?

换句话说,如果datacontent没有提供正确的数据会发生什么?子组件将简单地回到props对象中的default属性。

要看到这个实例的效果,请访问此链接:codepen.io/AjdinImsirovic/pen/BVJxKL

Vue 中构建组件模板的其他方法

到目前为止,我们已经看过将模板定义为字符串(使用单引号或双引号)和模板字面量(使用反引号)。还有许多其他处理组件模板的方法:

  • 内联模板

  • X-templates

  • 渲染函数

  • 单文件组件

  • JSX

它们大多都有各自的优缺点。例如,在 Vue 中使用 JSX 是可能的,但通常不被赞同,因为这不是 Vue 的做事方式。内联模板是使用 HTML 中的inline-template属性制作的。

如果你在 HTML 的 script 标签中添加type=''text/x-template'',你将创建一个 Vue x-template。以下是一个例子:

// HTML
<div id="app">
  <script type="text/x-template" id="custom-article-template">
    <p>{{ name }}</p>
  </script>
</div>

// JS
Vue.component('custom-article', {
  template: '#custom-article-template',
  props: ['name']
})
new Vue({
    el: '#app'
})

此示例的代码笔可在此处找到:codepen.io/AjdinImsirovic/pen/NzXyem

单文件模板可能是在 Vue 中创建模板的最实用方式。您可以将所有的 HTML、JS 和样式都放在一个文件中(使用.vue文件扩展名),然后使用构建过程(如 Webpack)编译此文件。在后面的章节中,当我们使用 Vue-cli(借助 Vue 中 Webpack 的使用)时,我们将深入研究这一点。

通过组件构建简单的网页

正如我们在前一节中所看到的,Vue 中构建组件的方式有很多种,这可能会使事情看起来比必须复杂。虽然重要的是要意识到 Vue 为我们构建组件的各种方式带来的多样性,但在本节中,我们将看一种简单的使用组件构建网页的方式。

在开始构建我们的页面之前,有一件事情应该对我们清楚:Vue 中的每个组件也只是另一个 Vue 实例。这意味着每个组件都需要一个选项对象,其中包含与任何其他 Vue 实例相同的键值对。唯一的例外是根 Vue 实例具有一些额外的选项,只能在其中使用。

在这些介绍性的澄清之后,让我们看看如何将组件添加到 Vue 实例中。

将简单组件添加到 Vue 实例中

在开始这个示例之前,我们将从一个简单的 Vue 实例开始。

在我们的 JavaScript 文件中,让我们创建一个最简单的 Vue 实例,以#app元素作为其入口点:

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

接下来,让我们在我们的 HTML 中添加一个 div,这样我们的 Vue 实例就有了页面中的一个元素来访问其 DOM:

<div id="app"></div>

现在我们将在我们的 JavaScript 文件中添加另一个组件。让我们通过将以下代码添加到顶部来扩展我们现有的 JS 文件:

Vue.component('the-header', {
  template: '<h1 class="header css classes go here">Our example header</h1>'
})

现在,我们可以在我们的 HTML 中简单地添加自定义的the-header组件:

<div id="app">
  <the-header></the-header>
</div>

这样做将在屏幕上呈现我们的示例标题文本。

既然我们已经看到了向我们的 Vue 应用程序添加一个简单组件有多么容易,让我们再添加一个来加强这一点。

我们将从扩展我们的 JS 文件开始,添加另一个组件the-footer

Vue.component('the-header', {
  template: '<h1 class="header css classes go here">Our example header</h1>'
});

Vue.component('the-footer', {
  template: '<h1 class="footer css classes go here">Our example header</h1>'
});

//Root Instance
new Vue({
  el: '#app',
  data: {}
})

当然,我们需要更新我们的 HTML 以使其工作:

<div id="app">
  <the-header></the-header>
  <the-footer></the-footer>
</div>

在命名自定义组件时,我们需要使用连字符。这样做是为了确保与常规 HTML 元素没有命名冲突。

本节示例代码可在此处找到:codepen.io/AjdinImsirovic/pen/qypBbz

现在我们了解了如何向 Vue 实例添加简单组件,让我们通过添加一个更复杂的例子来练习。

在 Vue 中创建由组件组成的更复杂的页面

首先,让我们向我们的新 Vue 实例中添加一个组件。这次,我们将在自定义组件的选项对象中使用数据选项。

这是我们开始的代码:

Vue.component('the-header', {
  template: '<h1 class="h1 text-success">{{header}}</h1>',
  data: function() {
    return {
      header: 'Just another simple header'
    }
  }
});

//Root Instance
new Vue({
  el: '#app',
  data: {}
})

在这段代码中,我们已经在模板中添加了 mustache 语法。然后我们利用了数据选项来返回将在模板中插值的文本。mustache 语法告诉我们的组件在数据选项中查找header

此示例的代码在此处可用:codepen.io/AjdinImsirovic/pen/wxpvxy

接下来,在我们的页眉下,我们将添加一些 Bootstrap 卡片。

为简单起见,我们将使用官方 Bootstrap 文档中的现有示例,该示例位于以下 URL:getbootstrap.com/docs/4.0/components/card/#using-grid-markup

该示例提供了以下代码:

<div class="row">
  <div class="col-sm-6">
    <div class="card">
      <div class="card-body">
        <h5 class="card-title">Special title treatment</h5>
        <p class="card-text">
          With supporting text below as a natural lead-in to additional 
          content.    
        </p>
        <a href="#" class="btn btn-primary">Go somewhere</a>
      </div>
    </div>
  </div>
  <div class="col-sm-6">
    <div class="card">
      <div class="card-body">
        <h5 class="card-title">Special title treatment</h5>
        <p class="card-text">
          With supporting text below as a natural lead-in to additional 
          content.
        </p>
        <a href="#" class="btn btn-primary">Go somewhere</a>
      </div>
    </div>
  </div>
</div>

尽管 Bootstrap 框架不是本书的主题,但对我们来说,给出一个在实践中使用 Vue 组件的真实例子将是有用的。由于 Bootstrap 基本上已成为前端框架的行业标准,它是展示 Vue 组件不仅如何一般使用,而且如何与其他前端技术结合的完美候选者。

现在让我们看看如何向我们的 Vue 网页示例中添加一个单个卡片。这是要添加到我们的 JS 中的代码:

Vue.component('the-card', {
  template: '<div class="card"><div class="card-body"><h5 class="card-title">Special title treatment</h5><p class="card-text">With supporting text below as a natural lead-in to additional content.</p><a href="#" class="btn btn-primary">Go somewhere</a></div></div></div>',
});

我们代码开发的这个阶段的代码在这里可用:codepen.io/AjdinImsirovic/pen/VByYeW

接下来,让我们将我们的卡片组件添加到我们的 HTML 中。完整的更新代码将如下所示:

<div id="app">
 <div class="container">
    <the-header></the-header>
    <div class="row">
      <div class="col-sm-6">
        <the-card></the-card>
      </div>
      <div class="col-sm-6">
        <the-card></the-card>
      </div>
    </div>
</div>

将上述代码添加到我们的 HTML 中,并根据之前描述的进行 JS 更新,我们将得到以下结果:

我们在 JS 中添加了一个单独的卡片组件;然而,正如我们在之前的例子中看到的,我们现在可以根据需要在 HTML 中重复使用它。

这为我们提供了一个绝佳的机会,借助 Vue 快速原型设计完整的网页。

我们甚至可以进一步进行,正如我们将在下一节中看到的。

使用 v-for 改进我们基于 Vue 的布局

在这一部分,我们将通过 Vue 指令来改进我们现有的网页。

我们的具体目标是尝试在组件实例中使用数据选项,并结合 Vue 指令的功能来进一步改进我们的 Vue 应用程序。

此部分的代码可在codepen.io/AjdinImsirovic/pen/Epoamy中找到。

让我们使用 ES6 的反引号语法使我们的 JS 更容易阅读。这种语法允许我们编写跨多行的 JavaScript 字符串:

Vue.component('the-header', {
  template: '<h1 class="h1 text-success">{{header}}</h1>',
  data: function() {
    return {
      header: 'Just another simple header'
    }
  }
});

Vue.component('the-card', {
  template: `
    <div class="card">
      <div class="card-body">
        <h5 class="card-title">Special title treatment</h5>
        <p class="card-text">
          With supporting text below as a natural lead-in to addtional 
          content.
        </p>
        <a href="#" class="btn btn-primary">Go somewhere</a>
      </div>
    </div>`,
});

//Root Instance
new Vue({
  el: '#app',
  data: {}
})

现在,让我们将data选项添加到the-card Vue 组件中:

  data: function() {
    return {
      customCard: [{
        heading: 'John Doe',
        text: 'John.doe@acme.org'
      }, 
      {
        heading: 'John Doe',
        text: 'John.doe@acme.org'
      }
     ]}
  }

正如我们在前面的代码中所看到的,我们返回了一个包含特定headingtextcustomCard对象数组。

接下来,我们可以在我们的模板中使用v-for指令,就像这样:

Vue.component('the-card', {
  template: `
    <div class="card">
      <div class="card-body" v-for="customCard in customCards">
        <h5 class="card-title">{{customCard.heading}}</h5>
        <p class="card-text">
          {{customCard.text}}
        </p>
        <a href="#" class="btn btn-primary">Go somewhere</a>
      </div>
    </div>`,

我们在具有card-body类的div中引入v-for指令。我们循环遍历我们的customCards集合中的每个customCard,并为我们customCard数组中的每个对象的h5文本内容插入customCard.heading

最后,让我们在 HTML 中添加一个 Bootstrap 类,这样我们网页的h1标签就不会紧贴在视口的顶部。为此,我们将使用 Bootstrap 的间距实用程序。您可以在这里阅读有关它们的信息:getbootstrap.com/docs/4.0/utilities/spacing/

我们的 HTML 中的更改将是最小的,只是添加了另一个 CSS 类:mt-5

最后,以下是改进页面的完整 JS 代码。首先,我们注册主标题组件:

//Register main title component
Vue.component("main-title-component", {
  template: '<h1 class="text-center mt-5 mb-4">{{title}}</h1>',
  data: function() {
    return {
      title: "Just another title"
    };
  }
});

然后我们注册list group组件:

//Register list group component
Vue.component("list-group-component", {
  template: `
    <ul class="list-group">
      <li class="list-group-item" v-for="item in items">{{item.description}}</li>
    </ul>`,
  data: function() {
    return {
      items: [
        {
          description: "Description one"
        },
        {
          description: "Description two"
        },
        {
          description: "Description three"
        }
      ]
    };
  }
});

之后,我们注册了card组件:

// Register card component
Vue.component("card-component", {
  template: `
    <div class="card">
      <div class="card-body">
        <h5 class="card-title">{{title}}</h5>
        <p class="card-text">{{text}}</p>
        <a href="#" class="btn btn-primary">Go somewhere</a>
      </div>
    </div>`,
  data: function() {
    return {
      title: "This is the card title",
      text: "This is the card text"
    };
  }
});

我们还添加了root instance

//root Instance
new Vue({
  el: "#app",
    data: {}
});

这是 HTML:

<div id="app">
  <div class="container mt-5 mb-5">
    <main-title-component></main-title-component>
    <div class="row">
      <div class="col">
        <list-group-component></list-group-component>
      </div>
      <div class="col">
        <card-component></card-component>
      </div>
    </div>
  </div>
</div>

添加上述代码的结果可以在此截图中看到:

在这一部分,我们已经看过组件以及如何开始使用它们。接下来,我们将讨论 Vue 中的观察者。

Vue 中的观察者

Vue 中的每个组件都有一个观察者。

为了理解这是如何工作的,让我们从本章的一个早期例子开始。这个例子来自计算属性部分,链接在这里:codepen.io/AjdinImsirovic/pen/qKVyry。这是我们的起始代码。正如我们从前一节知道的,这里有两个输入字段,我们正在打印出这些输入字段中输入的值在表单下的一些 span 标签中。

让我们扩展我们的例子。初始代码是一样的;我们只会添加一个观察者。更新后的代码可以在这个 Codepen URL 找到:codepen.io/AjdinImsirovic/pen/jprwKe

可以观察到,我们对原始笔记本唯一的更新是添加了观察者选项,如下所示:

  watch: {
    ownerName(previousValue,currentValue) {
      console.log(`The value in the first input has changed from:   
        ${previousValue} to: ${currentValue}`);
    }
  },

之前的观察者是如何工作的?它允许我们使用一个方法,该方法的名称必须与我们在 HTML 中观察的计算属性相同。观察者有可选参数,我们可以将这些参数传递给它,在方法体中使用;在这种情况下,我们给我们的可选参数一些好的和描述性的名称:previousValuecurrentValue

watch方法的主体中,我们正在记录输入值的更改到 JavaScript 控制台。测试这样工作的一个优雅的方法是,例如,突出显示第一个输入字段的初始值的例如部分,然后简单地擦除它,只留下输入中的Old McDonald的值。

这样做会导致以下句子被记录到控制台中:

The value in the first input has changed from: e.g Old McDonald to: Old McDonald.

在下一节中,我们将看看如何在组件的生命周期的各个阶段挂钩,并在那个特定点用自定义代码改变其行为。

生命周期钩子

生命周期钩子是让我们在组件生命周期的各个阶段改变组件行为的方法。

什么是组件的生命周期?

这只是组件生命周期的自然进展。

因此,我们可以说生命周期钩子是这个旅程中每个组件都需要经历的。在组件生命周期的这些特定,我们可以使用这些方法来改变组件的行为。

Vue 团队为这些生命周期方法选择了非常描述性的名称。接下来是按组件生命周期的自然进展顺序组织的生命周期钩子列表:

  • beforeCreate

  • created

  • beforeMount

  • mounted

  • beforeUpdate

  • updated

  • activated

  • deactivated

  • beforeDestroy

  • destroyed

这个组件生命周期的可视化表示可以在这个地址找到:vuejs.org/images/lifecycle.png

请注意,打印出这张图片并随身携带直到完全理解它所传达的信息将是有益的。这将对更深入地理解 Vue 总体以及其组件生命周期特别有帮助。

正如我们所看到的,组件的生命周期有五个不同的阶段,每个阶段在特定阶段开始之前有一个生命周期钩子,以及在完成后有另一个生命周期钩子。

重要的是要注意,一个组件可以根据数据模型的变化被多次挂载。这在之前的提示框中引用的生命周期图中是可以验证的。然而,同样重要的是要理解,当底层数据发生变化时,DOM 重新渲染可能会导致组件被有效地卸载,尽管这在生命周期图中没有明确提到。

我们如何使用生命周期钩子?

让我们看一个简单的例子,可以在这个 Codepen 网址找到:codepen.io/AjdinImsirovic/pen/jprmoa

首先,让我们添加 HTML:

<div> Lorem ipsum dolor sit amet</div>
<div id="app">
  <custom-article :message="datacontent"></custom-article>
</div>

接下来,让我们添加 CSS:

div,.thetemplate {
 font-size: 30px;
 padding: 20px;
 color: limegreen;
 font-family: Arial;
  border: 3px solid green;
  border-radius: 10px;
}

最后,JS 部分:

Vue.component('custom-article', {
  props: {
    message: {
      type: String,
      required: true,
      default: 'Hello Vue'
    }
  },
  template: `<div class="thetemplate">{{ message }}</div>`
});

new Vue({
  el: "#app",
  beforeCreate() {
    alert("Lifecycle hook beforeCreate has been run");
  },
  created() {
    setTimeout(function(){
      alert('This message is showing 5 seconds after the \'created\' life cycle hook');
    },5000);
  },
  data: function() {
    return {
      datacontent: 'This component was made with the help of a data function in the Vue component called custom-article, and the data passed was validated with the help of the props object inside the Vue component'
    }
  }
});

如在提供的 Codepen 中所示,很容易在 Vue 中钩入生命周期方法。只需要在 Vue 实例中的生命周期钩子方法名称中提供所需的代码(功能)即可。

在前面的示例中,我们展示了beforeCreate()方法的警报,并且在created()方法运行后 5 秒钟显示另一个警报。

生命周期钩子还有许多有用的用途,这将在接下来的章节中介绍。

总结

在本章中,我们了解了 Vue 中的一些基本概念。我们描述了为什么这些概念很重要以及它们如何被使用。我们还看了一些在实践中使用这些概念的简单例子。

我们了解了 Vue 中的数据驱动视图以及响应性作为跟踪数据模型变化的一种方式。我们看了使用计算属性和方法、指令及其修饰符的方法。我们已经看到了一些关于组件、模板和 props 的实际例子,以及在 Vue 中构建组件模板的不同方法。

我们学习了如何使用 Vue 组件和指令原型化网站,并在本章中结束时,我们看了一下 watchers 和生命周期钩子,作为改变组件行为的强大方式,可以在它们的生命周期的任何时刻进行。

在下一章中,我们将进一步深入研究 Vue 中的响应式编程,重点关注组件、props 和插槽。

第三章:使用 Vue-CLI,组件,props 和插槽工作

上一章是对 Vue 基本概念的介绍。我们将以更现实的方式开始本章:我们将介绍 Vue-cli。我们将查看组件层次结构,全局和本地组件以及组件之间的通信。我们将介绍插槽,并且我们还将检查插槽和 props 之间的区别。

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

  • Vue 组件层次结构,全局和本地组件

  • 使用 Vue-cli

  • 设置代码编辑器以与 Vue 一起使用

  • 基于 Vue-cli 的项目结构

  • 向子组件添加基本功能

  • 向我们的HelloAgain.vue添加 props

  • 插槽介绍

Vue 组件层次结构,全局和本地组件

正如我们在第二章中学到的,Vue 2 的基本概念,要运行一个新的 Vue 实例,我们使用 new Vue:

new Vue(
  el: "#app",
  // the rest of the Vue instance code here
)

我们的app组件位于这个 Vue 实例中。

app 组件通常有一个子组件,就像我们在第二章中看到的例子一样,Vue 2 的基本概念codepen.io/AjdinImsirovic/pen/xzpOaJ

Vue.component('custom-article', {
  template: `
    <article>
      Our own custom article component!
    </article>`
})
new Vue({
    el: '#app'
})

在上一章中我们没有提到的是:

  • 子组件可以根据需要重复使用

  • 子组件也可以有自己的子组件

这个例子可以在以下 pen 中找到:codepen.io/AjdinImsirovic/pen/ZjdOdK

以下是演示这两个原则的代码:

// JS
Vue.component('custom-article', {
  template: `
    <article>
      Our own custom article component!
    </article>`
})
Vue.component('another-custom-article', {
  template: `
    <article>
      Another custom article component! 
      This one has it's own child component too!
      Here it is:
      <custom-article></custom-article>
    </article>`
})
new Vue({
    el: '#app'
})

/* CSS */
article {
  font-size: 40px;
  padding: 20px;
  color: limegreen;
  font-family: Arial;
  border: 3px solid green;
  border-radius: 10px;
}

<!-- HTML -->
<main id="app">
    <custom-article></custom-article>
    <custom-article></custom-article>
    <another-custom-article></another-custom-article>
</main>

如已经看到的,要向我们的 Vue 实例添加一个组件,我们使用以下语法:

Vue.component('another-custom-article', { // etc...

在 Vue 术语中,我们使用这段代码来注册一个组件。如前所述,它被称为全局注册。还有本地注册

本地注册与Vue.component语法类似。代码中唯一的区别是我们引入本地组件的方式与全局组件相比。在之前的代码中,我们有以下全局组件:

Vue.component('custom-article', {
  template: `
    <article>
      Our own custom article component!
    </article>`
})

将这个全局组件转换为本地组件就像删除这段代码一样简单:

Vue.component('custom-article'

与之前的代码不同,我们将简单地创建一个新变量,并给它与我们在全局组件中使用的完全相同的选项对象,就像这样:

var customArticle = {
  template: `
    <article>
      Our own custom article component!
    </article>`
}

为了在 Vue 实例中使用这个本地组件,我们将引入components选项,就像这样:

new Vue({
    el: '#app',
    components: { 
      'custom-article': customArticle
    }
})

这里有一个使用本地组件的示例:codepen.io/AjdinImsirovic/pen/ZMzrpr

然而,前面的示例是故意不完整的。正如我们所看到的,customArticle本地组件只能在主 Vue 实例中使用,而不能在anotherCustomArticle组件中使用。

为了使其正常工作并完成示例,我们需要调整这部分代码:

Vue.component('another-custom-article', {
  template: `
    <article>
      Another custom article component! 
      This one has it's own child component too!
      Here it is:
      <custom-article></custom-article>
    </article>`,
    //components: {
    // 'customArticle': customArticle
    //}
})

我们只需删除这三行注释:

    components: {
     'customArticle': customArticle
    }

通过这样做,我们已经在全局组件anotherCustomArticle中注册了本地组件customArticle。基本上,我们正在遵循在主 Vue 实例中注册本地组件的相同过程,并且我们正在应用相同的方法在anotherCustomArticle全局组件中注册本地组件。

要了解全局和本地注册的细微差别,您可以参考官方 Vue 文档的这一部分:

vuejs.org/v2/guide/components-registration.html

在下一节中,我们将开始使用 Vue-cli。

使用 Vue-CLI

为了开始使用 Vue-cli,我们需要在计算机上设置 Node.js,并且我们还需要在我们选择的操作系统上安装一个命令行应用程序。

例如,我的首选工具是 Windows 10 和 Git bash for Windows。

有许多不同的操作系统和命令行应用程序,您可能会使用其中之一。

如果在安装本节中提到的任何工具时遇到问题,值得查看这篇关于在操作系统上安装 Node.js 的深入指南:

www.packtpub.com/mapt/book/web_development/9781788626859/2

安装 Git bash

您首先需要访问git-scm.com/downloads,该网站可以让您在 macOS X、Windows 和 Linux/Unix 安装之间进行选择。点击 Windows 下载后,您可以按照 Git bash 的安装步骤进行安装。在安装过程中,只需按照默认预设选项进行操作即可。

安装 nvm

要下载 Windows 的 Node 版本管理器,请访问此链接:

github.com/coreybutler/nvm-windows/releases

一旦进入页面,点击nvm-setup.zip文件进行下载,然后运行下载的nvm-setup.exe并按照常规安装步骤进行操作。

接下来,以管理员权限启动 Git bash,并运行以下命令:

nvm install 8.11.4

以下消息将被记录到控制台:

Downloading node.js version 8.11.4 (64-bit)...

为什么使用 nvm?

有两个主要原因:

  • 安全关键升级

  • 在不同项目中更轻松地切换 Node 版本

这里列出的第一个原因与 Node.js 的未来更新有关。假设在本书出版几个月后有一个重大的安全补丁,更新系统上的 Node 将是明智的选择。使用 nvm 可以轻松实现这一点,这也带来了第二点。即使没有可供升级的 Node 的主要版本,您仍然可以根据不同项目的需求运行不同版本的 Node。无论如何,使用 nvm 都是值得的。

下载完成后,在我们的 Git bash 中,我们可以简单地运行此命令:

nvm use 8.11.4

现在,我们准备好使用 Vue-cli 了。

安装和更新 Vue-cli

值得注意的是,Vue-cli 是对 Webpack 的封装,经过调整和优化,以便在开发和发布 Vue 应用程序时提供最佳体验。这对开发人员来说是一个重大优势,因为这种设置让我们能够专注于编码,而不必长时间地与工具链搏斗。

让我们打开 Git bash 并运行以下命令:

npm install -g vue-cli

由于 Vue-cli 是一个npm包,您可以在这里阅读更多信息:www.npmjs.com/package/vue-cli

要检查系统上安装的 Vue-cli 当前版本,请运行此命令:

vue -V

请注意,Vue-cli 版本 2 和 3 之间有一个重大升级。为了确保您在系统上使用的是最新版本,您可以运行此命令:

npm install @vue/cli

此命令将更新您的 Vue-cli 版本为最新版本。更新是本地的,这意味着它将放在您运行上一个命令的文件夹的node_modules文件夹中。请注意,由于需要安装的所有依赖项,此操作可能需要一些时间。

在使用 Vue-cli 初始化项目之前,快速列出 Vue-cli 版本 3 带来的改进将是有益的。希望这将强化第一章中关于 Vue 易用性的一些关键点,介绍 Vue

Vue-cli 版本 3 的目标如下:

  • 简化和优化工具链,避免前端开发中的工具疲劳

  • 遵循最佳实践的工具,并使其成为 Vue 应用程序的默认选择

新版本的 Vue-cli 还有一系列功能和升级:

  • 为热模块替换、树摇和其他功能预设 Webpack 配置

  • ES2017 功能

  • Babel 7 支持

  • PostCSS 支持

  • 可选集成 Typescript、PWA、Jest、E2E 测试等

简而言之,Vue.js 跟上了时代的步伐,Vue-cli 只是更多的证据。

使用 Vue-cli 初始化一个新项目

安装完成后,我们可以使用以下命令初始化一个新项目:

vue create quickstart-vue

我们给我们的 Vue 应用程序命名为quickstart-vue。我们也可以给它取其他任何名字。

一旦运行了上述命令,我们可以选择使用预设,或者手动选择要使用的功能:

$ vue create quickstart-vue
 ? Please pick a preset: (Use arrow keys)
 > default (babel, eslint)
 Manually select features

我们可以选择默认预设,但作为一个小练习,让我们选择手动选择功能选项。然后我们将选择npm而不是yarn。这将导致屏幕上显示以下输出:

$ vue create quickstart-vue
 ? Please pick a preset: (Use arrow keys)
 ? Please pick a preset: default (babel, eslint)
 ? Pick the package manager to use when installing dependencies: (Use arrow keys)
 ? Pick the package manager to use when installing dependencies: NPM
Installing CLI plugins. This might take a while...

当您看到这条消息时,您将知道插件已安装:

...
Successfully created project quickstart-vue.
Get started with the following commands:
$ cd quickstart-vue
 $ npm run serve

现在我们可以简单地按照先前的说明,切换到quickstart-vue目录:

cd quickstart-vue

接下来,我们将运行服务器(实际上是在后台运行 Webpack 开发服务器):

npm run serve

我们的应用程序可在端口8080上使用,将在控制台中记录。因此,让我们在http://localhost:8080上打开浏览器,查看默认站点:

在下一节中,我们将设置两个编辑器以与我们的新 Vue 项目一起使用。这些编辑器是 Sublime Text 和 Visual Studio Code。

设置代码编辑器以与 Vue 一起使用

有许多代码编辑器和IDEs集成开发环境)可以用来处理 Vue。其中一些比较流行的包括:

在本节中,我们将看看如何在 Sublime Text 和 VS Code 中使用 Vue.js。

在 Sublime Text 3 中使用 Vue.js

Sublime Text 是一个成熟且有趣的文本编辑器,因此我们将下载并设置它以用于我们的 Vue.js 项目。

下载 Sublime Text 3

我们将从下载页面开始下载 Sublime Text 3:

www.sublimetext.com/3

接下来,访问网站packagecontrol.io/,这是 Sublime Text 的软件包管理器的主页。

安装软件包管理器

在软件包管理器网站上,单击页面右上角的立即安装按钮,然后按照这些安装步骤进行操作:

  1. 选择并复制 Sublime Text 3 标签内的所有文本。

  2. 打开新安装的 Sublime Text 3。

  3. 在 Sublime Text 3 中,按下Ctrl + `(按住并按下控制键,然后按下反引号键)的键盘快捷键。在大多数键盘上,反引号字符位于键盘的字母数字部分的 1 号键左侧。

  4. 将从packagecontrol.io复制的代码粘贴到上一步中打开的底部输入字段中。

完成这些步骤后,重新启动 Sublime Text,您将可以通过此键盘快捷键访问快速启动安装程序:Ctrl + Shift + P

这个键盘组合将在屏幕中间显示一个小输入框,您可以在其中输入单词install。这将显示不同的选项,您可以使用鼠标单击或使用箭头上箭头下键进行突出显示,然后使用Enter键运行:

接下来,选择读取“Package control: Install package”的选项。

这是我们将安装的软件包列表:

有趣的是,Chrome 浏览器最近也获得了类似的快速启动功能,可以通过相同的快捷键使用。要查看它的操作,您可以简单地使用F12键打开开发者工具实用程序,然后运行Ctrl* + Shift + P快捷键。

例如,在打开的启动器中,您可以输入单词node,然后单击下拉菜单中的第一个命令Capture node screenshot。此命令将捕获您当前在 DOM 树中的元素的屏幕截图。

  • 几个月前

在下一节中,我们将学习如何在 VS Code 中设置基于 Vue 的项目。

在 VS Code 中使用 Vue.js

尽管 Sublime Text 具有成熟和对系统资源消耗较少的优势,这使得它在较慢的机器上易于使用,但 VS Code 是一个可行的替代方案。

安装 VS Code 和扩展

让我们导航到code.visualstudio.com/download,并下载适合我们操作系统的 VS Code 版本。

如果您使用的是 Windows 10,您可以轻松地查看系统是 32 位还是 64 位。只需使用快捷键Winkey + X,然后在上下文菜单中单击 System。一个新窗口将打开,您将在Device Specifications | System type区域看到您的系统是 32 位还是 64 位。

一旦您下载并打开了 VS Code,就可以轻松地向其添加扩展。只需单击屏幕左侧最下方的图标(扩展图标):

单击该图标将打开扩展窗格,您可以在其中输入 Vue 并获得类似于此的结果:

接下来,只需选择 Vue VS Code Extension Packs 中的任一选项,然后单击绿色的 Install 按钮。此包含的扩展包括语法高亮、代码片段、代码检查和错误检查、格式化(如 js-beautify)、自动完成、悬停信息、自动重命名标签、VS Code 的 npm 支持、ES6 代码片段、ESLint 等。

或者,如果您想避免在 VS Code 扩展中出现臃肿,您可以通过安装 Pine Wu 的 Vetur 扩展来减少一些扩展,而不是之前提到的 Vue 扩展包。

安装完成后,我们只需单击标有 Reload 的按钮来重新启动 VS Code 并激活扩展。最后,要返回到项目的树结构,只需单击屏幕左侧 VS Code 主菜单下方的最顶部图标。

基于 Vue-cli 的项目结构

在这一部分,我们将看一下使用 Vue-cli 设置的 Vue 项目的文件结构。我们的quickstart-vue文件夹结构如下:

让我们首先检查main.js文件的内容:

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  render: h => h(App)
}).$mount('#app')

我们首先从vue文件夹中导入Vue。这个vue文件夹位于你的node_modules文件夹中。

接下来,我们从App.vue中导入App

正如我们已经学到的,new Vue创建了一个 Vue 实例,然后我们将选项对象传递给它。

在选项对象中,我们只设置了render属性。正如我们所看到的,render属性的值是一个箭头函数:

h => h(App)

箭头函数接受作为其参数我们在main.js文件的第二行导入的App组件。

正如你可能已经注意到的,前面的函数是用 ES6 编写的。转译为 ES5 后,它会变成这样:

function(h) {
  return h(App); }

前面的函数接收一个要呈现的 Vue 模板。它将在我们的index.html页面中呈现,替换我们传递给$mount()函数的 DOM 的任何静态部分。

它将在 DOM 中的位置取决于我们将什么作为参数传递给$mount()函数。在前面的代码中,我们传递了#app参数。

'#app'来自哪里?它来自App组件,更具体地来说,来自位于我们的src文件夹中的App.vue文件。

src文件夹包含我们 Vue 项目的所有实际应用程序代码。

请注意,main.js是我们项目中唯一的实际 JavaScript 文件——src文件夹中的所有文件都具有.vue扩展名。每个.vue文件都有三个部分:模板定义了组件的 HTML,脚本定义了 JS,样式标签定义了 CSS。此外,Vue-cli(在底层使用 Webpack)将所有这些放在一起,因为它知道如何处理.vue文件。

让我们修改我们src文件夹中的App.vue文件,使其看起来像这样:

<template>
  <div id="app">
    <HelloWorld msg="Welcome to Vue Quickstart!"/>
    <HelloAgain />
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue';
import HelloAgain from './components/HelloAgain.vue'

export default {
  name: 'app',
  components: {
    HelloWorld, HelloAgain
  }
}
</script>

<style>
#app {
  font-family: sans-serif;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

让我们也改变HelloWorld.vue的内容,使其看起来像这样:

<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
    <p>
      This is the beginning of something great.
    </p>
 </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  props: {
    msg: String
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
p {
  font-size: 20px;
  font-weight: 600;
  text-align: center;
}
</style>

最后,让我们在src/components/文件夹中添加另一个组件。我们将其命名为HelloAgain.vue,并给它以下代码:

<template>
 <p class="hello-again">
 This is another component.
 </p>
</template>

<script>
export default {
 name: 'HelloAgain'
}
</script>

<style scoped>
p {
 font-size: 16px;
 text-align: center;
 color: tomato;
}
</style>

在这三个文件中,我们主要只是删除了一些额外的代码,以更清楚地展示以下几点:

  • 每个vue文件都包含一个单文件组件

  • 每个单文件组件的结构都遵循相同的模式:顶部是模板,中间是脚本,底部是样式

  • 样式可以针对每个单独的文件进行作用域限定

  • App.vue文件从components文件夹导入组件并导出自身,以便main.js可以使用它

  • HelloWorldHelloAgain组件只是将自己导出到父组件App.vue文件中

  • 为了使用新引入的组件(HelloAgain组件),App.vue文件需要在其<template>标签内添加它

  • App.vue文件还需要导入和导出HelloAgain单文件模板,以便main.js可以使用它

App.vueHelloWorld.vueHelloAgain.vue是单文件组件的示例。单文件组件是我们 Vue 项目中使用组件的首选方式。

如果您按照前面描述的更改文件,您应该在浏览器中的http://localhost:8080看到以下屏幕:

现在我们已经看到了vue/components/文件夹的组织方式和基本工作原理,我们将列出我们 Vue 项目中的其他重要文件:

  1. 不应该被 Git 源代码版本控制跟踪的文件列表:.gitignore

  2. Babel 的配置文件:.babel.config.js

  3. 列出我们基于 npm 的项目的依赖项和其他信息的文件:package.json

  4. 我们应用的 markdown 格式手册:README.md

当然,还有一个公共文件夹,其中包含我们编译后的应用程序,从index.html文件中引用。这个文件最终将在浏览器中呈现和重新呈现,因为我们的 Vue 应用程序不断编译。index文件的内容非常简单:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title>quickstart-vue</title>
  </head>
  <body>
    <noscript>
      <strong>
        We're sorry but quickstart-vue doesn't work properly 
        without JavaScript enabled. Please enable it to continue.
      </strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

如前所述,id属性设置为appdiv是我们 Vue 应用程序的入口点。

现在我们对项目结构有了更好的理解,我们将继续构建子组件。

在下一节中,我们将向我们的HelloAgain组件添加一些基本功能。

向子组件添加基本功能

在这一部分,我们将向子组件添加一些非常基本的功能。在我们深入了解如何做到这一点之前,我们还需要安装官方的 Vue Chrome 扩展。

Chrome 的 Vue 开发者工具扩展可以在此 URL 找到:bit.ly/2Pkpk2I

安装官方的 Vue Chrome 扩展很简单;您只需像安装其他 Chrome 扩展一样安装它。

安装完成后,你将在 Chrome 右上角看到一个 Vue 标志,点击该标志将会给你以下消息:

Vue.js 在此页面上被检测到。打开 DevTools 并查找 Vue 面板。

打开 DevTools 很容易:只需按 F12 键。然后你可以在具有以下标签的区域中找到 Vue 面板:元素、控制台、源等。你应该会得到类似以下屏幕的东西:

回到 VS Code,让我们打开HelloAgain.vue组件,并更新代码的模板部分,使其看起来像这样:

<template>
 <p class="hello-again">
 This is another component.
 <button v-on:click="incrementUp">Add One</button>
 <br>
 <span>Current value of the counter: {{ counter }}</span>
 </p>
</template>

让我们也更新script标签,就像这样:

<script>
export default {
 name: 'HelloAgain',
 data() {
     return {
         counter: 0
     }
 },
 methods: {
     incrementUp: function() {
         this.counter++
     }
 }
}
</script>

最后,我们将更新样式,使我们的按钮看起来更漂亮:

<style scoped>
p {
 font-size: 16px;
 text-align: center;
 color: tomato;
}
button {
    display: block;
    margin: 10px auto;
    background: tomato;
    color: white;
    border: none;
    padding: 5px 10px;
    font-size: 16px;
    border-radius: 4px;
    cursor: pointer;
}
</style>

这次更新的最终结果将在我们的浏览器中呈现如下:

现在我们已经看过构建模板和使用一些基本功能,让我们把重点转移到另一个重要的主题:组件之间的通信。我们将从重新审视 props 开始,这是一种在父组件和子组件之间通信的方式。

向我们的 HelloAgain.vue 添加 props

在这一部分,我们将简要回顾 props,看一个在 Vue 中如何在父组件和子组件之间通信的实际例子。换句话说,我们想从父组件获取一些数据并将其传递给子组件。我们将要传递的数据只是要包含在我们的quickstart-vue应用程序的计数器中的额外数字。

在我们的App.vue文件中,我们将添加一个按钮:

<template>
  <div id="app">
    <HelloWorld msg="Welcome to Vue Quickstart!"/>
    <button v-on:click="addTen">Add 10</button>
    <HelloAgain v-bind:counterFromParent="countUp"/>
  </div>
</template>

按钮放置在我们已经拥有的两个组件之间。我们已经添加了v-on指令,跟踪按钮上的点击事件。点击事件将触发addTen方法,因此我们将在App.vue文件的<script>标签之间指定它:

  methods: {
    addTen() {
      this.countUp += 10
    }
  },

addTen方法正在使用countUp数据片段,所以让我们也将这个新数据添加到我们的<script>中:

  data: function() {
    return {
      countUp: 0
    };
  },

因此,最初,我们在App.vue中的data函数返回零的countUp。每当用户点击我们的App.vue组件中的按钮时,countUp的值增加 10。这个值是我们想要传递给子组件的数据,即HelloAgain.vue子组件中计数器中存储的值。

这就是props语法的用法。为了告诉我们的HelloAgain.vue组件它应该期望来自父组件的数据,我们将添加以下代码:

props: ['counterFromParent']

props键的值是一个数组,我们在其中添加了子组件应该从父组件那里期望的props字符串。

请注意,props选项也可以是一个对象。使用对象作为我们props选项的示例是,例如,如果我们想要验证从父组件传递给子组件的数据。我们将在本书的后面章节中验证props

HelloAgain.vue中,我们将修改其模板内的<span>标签,就像这样:

<span>Current value of the counter: {{ counter + counterFromParent }}</span>

现在我们已经在父组件和子组件中设置了代码,只需将数据从一个传递到另一个。我们将在App.vue模板中通过在<HelloAgain />标签上添加v-bind指令来实现这一点。以下是更新后的App.vue模板:

<template>
  <div id="app">
    <HelloWorld msg="Welcome to Vue Quickstart!"/>
    <button v-on:click="addTen">Add 10</button>
    <HelloAgain v-bind:counterFromParent="countUp"/>
  </div>
</template>

请注意,我们将counterFromParent绑定到countUp的值。countUp的值从零开始,每次点击父组件按钮时,将运行addTen方法,我们在父组件<script>标签的methods选项中指定了这一点。

addTen方法将 10 添加到countUp的当前值。

在子组件HelloAgain.vue中,我们只需将counterFromParent的当前值添加到我们的counter变量中。要获取counterFromParent的值,我们将其列在HelloAgain.vue组件的<script>标签的props数组中。

从子组件传递数据到父组件

要从子组件传递数据到父组件,我们使用以下语法:

this.$emit();

$符号用于表示内置的 Vue 函数。这个特定的$emit函数用于向父组件发送自定义事件。我们传递给$emit函数的第一个参数是自定义事件的名称。例如,我们可以将计数器重置为零,所以我们可以像这样命名自定义事件:

this.$emit('counterHasBeenReset')

第二个参数是要传递的数据,所以我们将传递当前的计数器值,就像这样:

this.$emit('counterHasBeenReset', this.countUp);

当然,这意味着我们需要更新countUp的值,使其返回到零。为了做到这一点,我们需要更新HelloAgain子组件的<script>标签中的methods选项,使其看起来像这样:

 methods: {
     incrementUp: function() {
         this.counter++
     },
     resetTheCounter() {
         this.countUp = 0;
         this.$emit('counterHasBeenReset', this.countUp);
     }
 }

基本上,我们在方法选项中说,每当运行resetTheCounter方法时,countUp的值应该被重置为0。接下来,我们通过在counterHasBeenReset自定义事件中发出这个更新后的值来跟进。

现在让我们在子组件template标签中添加一个重置按钮,也在HelloAgain.vue中。我们只需在template标签中添加另一行即可:

<button v-on:click="resetTheCounter">Reset parent-added values</button>

正如我们在这里看到的,按钮点击将运行resetTheCounter方法。

现在我们正在发出事件,我们将使用以下语法在父组件中捕获它:

<HelloAgain v-bind:counterFromParent="countUp" v-on:counterHasBeenReset="countUp = $event" />

正如我们在这里看到的,我们在父组件中的<HelloAgain>标签中添加了内容。具体来说,我们添加了一个v-on指令,如下所示:

v-on:counterHasBeenReset="countUp = $event" />

该组件正在监听counterHasBeenReset自定义事件,该事件将从子组件中发出。当在父组件中捕获到这样的事件时,countUp的值将被设置为事件本身中的值。由于我们将其设置为零,它就是零。

在 Vue 中,有其他的方式来在组件之间进行通信(包括父到子和子到子),我们将在后面的章节中讨论它们,当我们讨论 Vuex 时。

这个练习的最终结果是,我们将重置从父组件中添加的计数器中的值,但该事件不会影响从子组件中添加的值。

现在我们已经了解了自定义事件,我们可以通过查看插槽来继续讨论组件。

插槽介绍

插槽是重用组件的一种方式。通过 props,我们将数据传递给组件。但是,如果我们想要将整个组件传递给其他组件怎么办?这就是插槽的用武之地。

插槽只是一种将更复杂的代码传递给我们的组件的方法。它们可以是一些 HTML,甚至是整个组件。

要将 HTML 元素从父组件插入到子组件中,我们在子组件内部使用slot元素:

<slot></slot>

插槽的实际内容在父组件中指定。

这是插槽使用的一个例子:

<!-- HTML -->
<div id="app"></div>

// JS
Vue.component("basicComponent", {
  template: `
    <div>
      <slot name="firstSlot"></slot>
      <slot name="secondSlot"></slot>
      <slot></slot>
    </div>
  `
});

new Vue({
  el: "#app",
  template: `
    <basicComponent>
      <p slot="firstSlot">
        This content will populate the slot named 'firstSlot' 
        in the 'basicComponent' template
      </p>
      <p slot="secondSlot">
        This content will populate the slot named 'secondSlot' 
        in the 'basicComponent' template
      </p>
    </basicComponent>
  `
});

/* CSS */
div {
  font-size: 30px;
  padding: 20px;
  color: darkgoldenrod;
  font-family: Arial;
  border: 3px solid darkgoldenrod;
  border-radius: 0px;
}

这个例子可以在这里实时查看:codepen.io/AjdinImsirovic/pen/ERoLQM

在处理插槽时有几个关键点:

  • 插槽是基于 Web 组件规范草案实现的

  • 插槽的样式由子组件中的作用域样式标签确定

  • 插槽使可组合组件的使用成为可能

  • 您可以在插槽中使用任何模板代码

  • 如果您有多个插槽,可以为它们命名(使用name属性)

  • 如果您有多个插槽,可以在其中一个插槽中省略name属性,那个插槽将成为默认插槽

  • 从 Vue 2.1.0 开始,插槽可以被作用域化

  • 插槽作用域可以使用 ES2015 表达式解构进行解构

要向插槽添加默认信息,只需在插槽标记中添加内容。

只需将插槽标记的代码从这个:

<slot></slot>

更改为这个:

<slot>This is some default information</slot>

如果您通过在提供的示例笔中添加上面引用的默认未命名插槽代码来更新它,您会注意到即使我们没有在 Vue 实例中引用它,插槽也会被填充。

总结

在本章中,我们讨论了 Vue 的组件。我们讨论了 Vue 组件层次结构以及全局和本地组件之间的区别。我们启动了 Vue-cli v3 并学习了如何使用它。我们使用.vue文件并在几个代码编辑器中设置了开发环境。我们了解了如何向子组件添加功能以及 props 和 slots 的用例。最后,我们看了 Vue 中的组件通信。

在下一章中,我们将讨论过滤器作为改变屏幕上呈现内容的一种方式,而不影响其背后的数据。我们还将看到如何在编程中遵循 DRY 规则,借助混合来实现。

第四章:过滤器和混合器

在本章中,我们将展示如何使用过滤器来更改屏幕上呈现的内容,而不更改基础数据。我们还将介绍混合器,这是一种扩展组件并遵守编程的 DRY 规则的实用方法。

更具体地,在本章中,我们将讨论以下内容:

  • 使用过滤器:

  • 使用全局和本地过滤器

  • 用过滤器替换条件指令

  • 链接过滤器

  • 使用混合器:

  • 避免在混合器方法中重复代码

  • 使用数据选项为我们的混合器添加更多功能

  • 在混合器中使用生命周期钩子

使用过滤器

过滤器只是一个函数。它接受一些数据(作为参数传递给过滤器函数),并对该数据执行一些简单的操作。执行的操作的结果从过滤器函数返回,并显示在应用程序中的适当位置。重要的是要注意,过滤器不会影响基础数据;它们只会影响数据在屏幕上的显示方式。

就像组件一样,过滤器也可以注册为全局或本地。注册全局过滤器的语法如下:

Vue.filter('justAnotherFilter', function(someData1, someData2, someDataN) {
  // the filter function definition goes here (it takes N number of arguments)
});

除了全局注册,我们还可以像这样在本地注册过滤器:

filters: {
  justAnotherFilter(someData1, someData2, someDataN) {
    // the filter function is defined here...
  }
}

正如我们在这里所看到的,在本地注册的情况下,过滤器被添加为 Vue 组件的选项。

一个将学生成绩四舍五入的过滤器示例

假设我们有一个朋友是教授,他们需要一些帮助来处理他们学生的测试。学生参加的测试总是以小数形式产生分数。学生可以在该测试中获得的分数范围在 0 到 100 之间。

作为我们的好朋友,我们将制作一个简单的 Vue 应用程序,其中包含一个将小数分数四舍五入为整数的过滤器。我们还会偏向于学生,这意味着我们将始终将结果四舍五入。

此示例的代码可在codepen.io/AjdinImsirovic/pen/MqBNBR上找到。

我们的过滤器函数将非常简单:它将接受一个浮点数,并根据接收到的浮点数返回一个四舍五入的整数。过滤器函数将被称为pointsRoundedUp,并且看起来像这样:

  filters: {
    pointsRoundedUp(points){
      return Math.ceil(parseFloat(points));
    }
  }

因此,我们的pointsRoundedUp函数接受来自应用程序data()函数的points实例,并使用 JavaScript 的内置parseFloat()Math.ceil()函数调用points值返回这些points实例。

在我们的 HTML 中使用过滤器时,我们采用以下语法:

{{ points| pointsRoundedUp }}

points值是应用程序中存储的实际数据。pointsRoundedUp是我们用来格式化从 Vue 组件的数据选项接收到的数据的过滤器。

一般来说,我们可以说所有过滤器的基本逻辑如下:

{{ data | formattedData }}

这个一般原则可以这样阅读:为了格式化返回的数据,我们用管道符号(|)跟着调用特定的过滤器。

让我们检查一下我们应用程序的完整代码。HTML 将如下所示:

<div id="app">
 <h1>A simple grade-rounding Vue app</h1>
 <p>Points from test: {{ points }}</p>
 <p>Rounded points are: {{ points | pointsRoundedUp }}</p>
</div>

JS 也将很简单:

new Vue({
  el: "#app",
  data() {
    return {
      points: 74.44
    }
  },
  filters: {
    pointsRoundedUp(points){
      return Math.ceil(parseFloat(points));
    }
  }
});

应用程序将在屏幕上输出以下内容:

A simple grade-rounding Vue app
Points from test: 74.44
Rounded points are: 75

应用程序现在已经完成。

然而,过了一段时间,我们的朋友又向我们请求另一个帮助:根据分数计算学生的等级。最初,我们意识到这只是一个小小的计算,我们可以简单地将其放入条件指令中。

更新示例的代码可以在这里找到:codepen.io/AjdinImsirovic/pen/XPPrEN

基本上,我们在这个新示例中所做的是用几个条件指令扩展了我们的 HTML。虽然这解决了问题,但我们混乱了我们的 HTML,而我们的 JS 保持不变。更新后的 HTML 代码如下:

<div id="app">
  <h1>A simple grade-rounding Vue app</h1>
  <p>Points from test: {{ points }}</p>
  <p>Rounded points are: {{ points | pointsRoundedUp }}</p>
  <p v-if="points > 90">Final grade: A</p>
  <p v-else-if="points > 80 && points <= 90">Final grade: B</p>
  <p v-else-if="points > 70 && points <= 80">Final grade: C</p>
  <p v-else-if="points > 60 && points <= 70">Final grade: D</p>
  <p v-else-if="points > 50 && points <= 86">Final grade: E</p>
  <p v-else="points <= 50">Final grade: F</p>
</div>

我们的问题得到了解决。这次测试的分数是 94.44,应用程序成功地将以下信息打印到屏幕上:

A simple grade-rounding Vue app
Points from test: 94.44
Rounded points are: 95
Final grade: A

然而,我们意识到我们的 HTML 现在很混乱。幸运的是,我们可以利用过滤器使事情变得不那么混乱。

使用过滤器替换条件指令

在本节中,我们将使用过滤器返回学生的适当等级。

更新后的应用程序代码可以在这里找到:codepen.io/AjdinImsirovic/pen/LJJPKm

我们对该应用程序的 HTML 所做的更改如下:

<div id="app">
  <h1>A simple grade-rounding Vue app</h1>
  <p>Points from test: {{ points }}</p>
  <p>Rounded points are: {{ points | pointsRoundedUp }}</p>
  <p>Final grade: {{ points | pointsToGrade }}</p>
</div>

我们将条件功能移到了我们的 JavaScript 中,即一个名为pointsToGrade的新过滤器中:

new Vue({
  el: "#app",
  data() {
    return {
      points: 84.44
    }
  },
  filters: {
    pointsRoundedUp(points){
      return Math.ceil(parseFloat(points));
    },
    pointsToGrade(points){
      if(points>90) {
        return "A"
      } else if(points>80 && points<=90) {
        return "B"
      } else if(points>70 && points<=80) {
        return "C"
      } else if(points>60 && points<=70) {
        return "D"
      } else if(points>50 && points<=60) {
        return "E"
      } else {
        return "F"
      }
    }
  }
});

作为对我们更新的代码进行快速测试,我们还将点数更改为 84.44,成功地从pointsToGrade过滤器返回了 B 等级。

然而,不完全出乎意料的是,我们的朋友再次回来,并要求我们再次扩展应用程序。这一次,我们需要再次显示我们学生的姓名的正确格式,格式如下:

Last name, First name, year of study, grade.

这意味着我们将不得不扩展我们的应用程序以获得额外的功能。幸运的是,这不难,因为我们可以利用过滤器的另一个很好的特性:链接。

在 Vue 中链接过滤器

我们应用的要求已经更新,现在我们需要在屏幕上显示一些额外的、格式良好的数据。

由于要求已经改变,我们还需要更新数据。

本节的代码可在此处找到:codepen.io/AjdinImsirovic/pen/BOOazy

这是更新后的 JavaScript。首先,我们将添加eldata选项:

new Vue({
  el: "#app",
  data() {
    return {
      firstName: "JANE",
      lastName: "DOE",
      yearOfStudy: 1, 
      points: 84.44,
      additionalPoints: 8
    }
  },

在 JS 中,我们将添加过滤器:

  filters: {
    pointsRoundedUp(points){
      return Math.ceil(parseFloat(points));
    },
    pointsToGrade(points){
      if(points>90) {
        return "A"
      }
      else if(points>80 && points<=90) {
        return "B"
      }
      else if(points>70 && points<=80) {
        return "C"
      }
      else if(points>60 && points<=70) {
        return "D"
      }
      else if(points>50 && points<=60) {
        return "E"
      }
      else {
        return "F"
      }
    },
    yearNumberToWord(yearOfStudy){
      // freshman 1, sophomore 2, junior 3, senior 4 
      if(yearOfStudy==1) {
        return "freshman"
      } else if(yearOfStudy==2){
        return "sophomore"
      } else if(yearOfStudy==3){
        return "junior"
      } else if(yearOfStudy==4){
        return "senior"
      } else {
        return "unknown"
      }
    },
    firstAndLastName(firstName, lastName){
      return lastName + ", " + firstName
    },
    toLowerCase(value){
      return value.toLowerCase()
    },
    capitalizeFirstLetter(string) {
        return string.charAt(0).toUpperCase() + string.slice(1);
    }
  }
});

更新后的 HTML 如下所示:

<div id="app">
  <h1>A simple grade-rounding Vue app</h1>
  <p>Points from test: {{ points }}</p>
  <p>Rounded points are: {{ points | pointsRoundedUp }}</p>
  <p>Student info: 
  <!--
  <p>Name: {{ firstName, lastName | firstAndLastName | toLowerCase | capitalizeFirstLetter}}</p>
  -->
  <p>
    Name: 
    {{ lastName | toLowerCase | capitalizeFirstLetter }}, 
    {{ firstName | toLowerCase | capitalizeFirstLetter }}
  </p>
  <p>Year of study: {{ yearOfStudy | yearNumberToWord }}</p>
  <p>Final grade: <strong>{{ points | pointsToGrade }}</strong></p>
</div>

通过这些链接的过滤器,我们通过两个过滤器toLowerCasecapitalizeFirstLetter正确格式化了学生的姓名,通过这两个过滤器将数据(以全大写形式出现)传递过去。

我们还可以看到一个被注释掉的段落,显示了一个不成功的方法,它只将姓氏的第一个字母大写,而不是名字的第一个字母。原因是firstAndLastName过滤器,当应用时,将全名组合成一个字符串。

请注意,过滤器不会被缓存,这意味着它们将始终被运行,就像方法一样。

有关过滤器的更多信息,请参阅官方文档:vuejs.org/v2/guide/filters.html

使用混合

混合是我们在 Vue 代码中抽象出可重用功能的一种方式。在前端世界中,由 Sass 广泛使用,混合的概念现在出现在许多现代 JavaScript 框架中。

当我们有一些功能希望在多个组件中重用时,最好使用混合。在接下来的例子中,我们将创建一个非常简单的 Vue 应用程序,在页面上显示两个 Bootstrap 警报。当用户点击其中任何一个警报时,浏览器的视口尺寸将被记录到控制台中。

为了使这个例子工作,我们需要从 Bootstrap 框架中获取一些普通的 HTML 组件。具体来说,我们将使用警报组件。

关于这个 Bootstrap 组件的官方文档可以在这个链接找到:getbootstrap.com/docs/4.1/components/alerts/

重要的是要注意,Bootstrap 组件和 Vue 组件是不同的东西,不应混淆。

运行应用程序时,将产生以下结果:

这个例子的代码可以在这里找到:codepen.io/AjdinImsirovic/pen/jvvybq

在不同组件中构建具有重复功能的简单应用程序

首先,让我们构建我们简单的 HTML:

<div id="app">
  <div class="container mt-4">
    <h1>{{heading}}</h1>
    <primary-alert></primary-alert>
    <warning-alert></warning-alert>
  </div>
</div>

我们正在使用 Bootstrap 的containermt-4的 CSS 类。常规的 HTML h1标签也获得了一些特定于 Bootstrap 的样式。我们还在前面的代码中使用了两个 Vue 组件:primary-alertwarning-alert

在我们的 JavaScript 代码中,我们将这两个组件定义为primaryAlertwarningAlert,然后在它们的父组件的components选项中列出它们:

const primaryAlert = {
  template: `
    <div class="alert alert-primary" role="alert" v-on:click="viewportSizeOnClick">
      A simple primary alert—check it out!
    </div>`,
    methods: {
    viewportSizeOnClick(){
      const width = window.innerWidth;
      const height = window.innerHeight;
      console.log("Viewport width:", width, "px, viewport height:", height, "px");
    }
  }
}
const warningAlert = {
  template: `
    <div class="alert alert-warning" role="alert" v-on:click="viewportSizeOnClick">
      A simple warning alert—check it out!
    </div>`,
    methods: {
    viewportSizeOnClick(){
      const width = window.innerWidth;
      const height = window.innerHeight; 
      console.log("Viewport width:", width, "px, viewport height:", height, "px");
    }
  }
}

现在,仍然在 JS 中,我们可以指定构造函数:

new Vue({
  el: '#app',
  data() {
    return {
      heading: 'Extracting reusable functionality into mixins in Vue'
    }
  },
  components: {
    primaryAlert: primaryAlert,
    warningAlert: warningAlert
  }
})

要查看这个小应用程序的结果,请打开控制台并单击两个警报组件中的任何一个。控制台输出将类似于以下内容:

Viewport width: 930 px, viewport height: 969 px

正如我们在 JavaScript 代码中所看到的,我们还定义了一个viewportSizeOnClick

方法在primaryAlertwarningAlert组件的methods选项内。这种功能上的不必要重复是一个完美的抽象成 mixin 的候选,接下来我们将这样做。

使用 mixin 保持 DRY

改进后的应用程序的代码可以在这里找到:codepen.io/AjdinImsirovic/pen/NLLgWP

在这个例子中,虽然我们的 HTML 保持完全相同,但更新后的 JavaScript 代码将如下所示:

const viewportSize = {
    methods: {
      viewportSizeOnClick(){
        const width = window.innerWidth;
        const height = window.innerHeight;
        console.log("Viewport width:", width, "px, viewport height:", height, "px");
      }
  } 
}
const primaryAlert = {
  template: `
    <div class="alert alert-primary" role="alert" v-on:click="viewportSizeOnClick">
      A simple primary alert—check it out!
    </div>`,
  mixins: [viewportSize]
}
const warningAlert = {
  template: `
    <div class="alert alert-warning" role="alert" v-on:mouseenter="viewportSizeOnClick">
      A simple warning alert—check it out!
    </div>`,
  mixins: [viewportSize]
}
new Vue({
  el: '#app',
  data() {
    return {
      heading: 'Extracting reusable functionality into mixins in Vue'
    }
  },
  components: {
    primaryAlert: primaryAlert,
    warningAlert: warningAlert
  }
})

正如在这里所看到的,我们已经从两个组件中删除了methods选项,并添加了一个名为viewportSize的新对象。在这个对象内部,我们已经移动了共享的methods选项:

const viewportSize = {
    methods: {
      viewportSizeOnClick(){
        const width = window.innerWidth;
        const height = window.innerHeight;
        console.log("Viewport width:", width, "px, viewport height:", height, "px");
      }
  } 
}

methods选项只包含viewportSizeOnClick函数。

顺便说一句,vieportSizeOnClick方法的名称有点误导。如果你仔细看第二个组件(warningAlert组件)的代码,你会注意到我们更新了指令,所以它使用的是v-on:mouseenter,而不是v-on:click。这意味着方法的名称需要更改为更合适的名称。因此,我们将把方法重命名为logOutViewportSize

另外,让我们想象一下,我们希望以另一种方式显示视口信息。例如,我们可能会在警报框中显示它,而不是将其记录到控制台中。这就是为什么我们将引入另一种方法alertViewportSize

随着所有这些小改变的积累,现在是时候看到我们小应用的另一个更新版本了。新的代码可以在以下网址找到:codepen.io/AjdinImsirovic/pen/aaawJY

与以前的更新类似,更新后的示例只有对 JS 进行了更改,如下所示。我们从viewportSize开始:

const viewportSize = {
    methods: {
      logOutViewportSize(){
        const width = window.innerWidth;
        const height = window.innerHeight; 
        console.log("Viewport width:", width, "px, viewport height:", height, "px");
      },
      alertViewPortSize() {
        const width = window.innerWidth;
        const height = window.innerHeight;
        alert("Viewport width: " + width + " px, viewport height: " + height + " px");
      }
  }
}

接下来,我们将设置警报:

const primaryAlert = {
  template: `
    <div class="alert alert-primary" role="alert" v-on:click="alertViewPortSize">
      A simple primary alert—check it out!
    </div>`,
  mixins: [viewportSize]
}
const warningAlert = {
  template: `
    <div class="alert alert-warning" role="alert" v-on:mouseenter="logOutViewportSize">
      A simple warning alert—check it out!
    </div>`,
  mixins: [viewportSize]
}

最后,让我们用指定 Vue 构造函数来结束:

new Vue({
  el: '#app',
  data() {
    return {
      heading: 'Extracting reusable functionality into mixins in Vue'
    }
  },
  components: {
    primaryAlert: primaryAlert,
    warningAlert: warningAlert
  }
})

在下一节中,我们将看看如何通过重构来进一步改进我们的混合方法。

重构我们的 viewportSize 混合方法

在本节中,我们将探讨进一步改进我们的混合方法。虽然我们的代码既可读又易于理解,但在const声明中存在一些代码重复。此外,我们将利用这个机会来探讨混合重构的方法。更新后的代码将包括一些基本的事件处理。

有关可用事件的列表,请参阅此链接:developer.mozilla.org/en-US/docs/Web/Events

由于我们还将使用 JavaScript 的内置addEventListener()方法,因此也很有必要在 MDN 上获取有关它的更多信息,网址如下:developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener

在我们开始重构之前,我们将利用混合的能力来插入 Vue 的生命周期功能(就像组件一样)。此外,在我们的混合的这个迭代中,我们除了methods之外还引入了另一个选项。我们使用的选项是data。为了避免在混合的methods选项中重复const声明,我们将要处理的值存储在data选项中。

虽然 HTML 仍然保持不变,但我们的 JavaScript 文件将会有很大的不同。让我们从设置数据开始:

const viewportSize = {
    data(){
      return {
        viewport: {
          width: 0,
          height: 0
        }
      }
    },

接下来,我们将添加方法,即getViewportSizelogOutViewportSizealertViewportSize

    methods: {
      measureViewportSize(){
        this.viewport.width = window.innerWidth;
        this.viewport.height = window.innerHeight;
      },
      logOutViewportSize(){
        console.log("Viewport width:", this.viewport.width, "px, viewport height:", this.viewport.height, "px");
      },
      alertViewPortSize() {
        alert("Viewport width: " + this.viewport.width + " px, viewport height: " + this.viewport.height + " px");
      }
  },

接下来,让我们添加created

created() {
    this.listener =
      window.addEventListener('mousemove',this.measureViewportSize); 
    this.measureViewportSize();
 }
}

现在,我们可以设置primaryAlert

const primaryAlert = {
  template: `
    <div class="alert alert-primary" role="alert" v-on:click="alertViewPortSize">
      A simple primary alert—check it out!
    </div>`,
  mixins: [viewportSize]
}

我们将继续添加warningAlert

const warningAlert = {
  template: `
    <div class="alert alert-warning" role="alert" v-on:mouseenter="logOutViewportSize">
      A simple warning alert—check it out!
    </div>`,
  mixins: [viewportSize]
}

最后,让我们添加 Vue 构造函数:

new Vue({
  el: '#app',
  data() {
    return {
      heading: 'Extracting reusable functionality into mixins in Vue'
    }
  },
  components: {
    primaryAlert: primaryAlert,
    warningAlert: warningAlert
  }
})

本节的代码可以在以下代码 pen 中找到:codepen.io/AjdinImsirovic/pen/oPPGLW

在我们重构的混合中,我们有datamethodscreated这些选项。created函数是一个生命周期钩子,我们使用这个钩子来监听mousemove事件。当发生这样的事件时,我们运行我们的混合的this.getViewportSize方法,该方法更新视口尺寸,然后将其记录下来或显示在警报框中。

永远不要使用全局混合!全局混合会影响应用程序中的所有组件。这种情况并不多见,所以通常最好避免使用全局混合。

通过这个,我们结束了对 Vue 中混合的简要讨论。有关此主题的更多信息,请访问此官方链接:

vuejs.org/v2/guide/mixins.html

总结

在本章中,我们讨论了 Vue 中的过滤器和混合。我们讨论了在何种情况下使用过滤器是有意义的,并且我们看了一下如何使用全局和本地过滤器。我们还讨论了过滤器如何用来替换条件指令,并且我们研究了如何将过滤器串联在一起。

我们还探讨了如何通过将功能从组件移动到混合中来抽象可重用功能,并且我们看了一下如何避免在混合本身内部重复代码。我们用一个在混合中使用生命周期钩子的例子来结束了这一部分。

在下一章中,我们将看看如何构建我们自己的自定义指令。

第五章:制作您自己的指令和插件

在这一章中,我们将看看扩展 Vue 的方法。首先,我们将编写自己的指令并看看如何使用它们。接下来,我们将制作一个自定义的 Vue 插件。

更具体地,在这一章中,我们将研究以下内容:

  • 自定义指令的结构以及如何制作它们

  • 使用全局和本地自定义指令

  • 向自定义指令传递值

  • 编写 Vue 插件

  • 发布 Vue 插件

制作我们自己的指令

在 Vue 2 中,组件是使用的主要策略,无论是保持 DRY 还是抽象化一些功能。然而,你可以采取的另一种方法是利用自定义指令。

理解自定义指令

正如我们在本书中之前讨论过的,指令帮助我们解释给 Vue 什么样的行为我们想要附加到一段标记上。正如我们之前看到的,Vue 内置了许多指令。一些例子是v-onv-ifv-model等等。简单回顾一下,指令是以v-开头的 HTML 属性。

当我们需要构建一个自定义指令时,我们只需在连字符后面提供一个自定义单词。例如,我们可以创建一个自定义指令,我们将其称为v-custom-directive,然后我们可以在我们的标记中使用这个名称,例如,像这样:

<div id="app">
  <div v-custom-directive>{{ something }}</div>
<!-- etc -->

请注意,拥有一个没有值的指令是完全正常的,就像为它提供一个值一样,就像这样:

<div id="app">
  <div v-custom-directive="aValue">{{ something }}</div>
<!-- etc -->

接下来,在我们的 JS 代码中,我们需要注册这个指令,如下所示:

Vue.directive('customDirective', {
  // details for the directive go here
}

因此,正如我们所看到的,提供给Vue.directive的第一个参数是我们自定义指令的名称。请注意,Vue 在 HTML 中使用 kebab-case,而在 JS 中使用 lowerCamelCase 的约定也适用于自定义指令。

提供给我们自定义指令的第二个参数是一个包含所有指令功能的对象。

正如你现在可能推断的那样,前面的代码给出了全局注册指令的一个例子。如果你想要本地注册一个指令,你需要为特定组件指定一个directives选项。

例如,我们可以注册一个本地组件如下:

directives: {
  directiveName: {
    // some code to describe functionality
  }
}

就像组件一样,指令也使用钩子,这允许我们控制它们的功能何时被调用。有五个指令钩子:bindinsertedupdatecomponentUpdatedunbind

有关某些这些钩子可以接受的参数的完整列表,您可以参考vuejs.org/v2/guide/custom-directive.html#Directive-Hook-Arguments

构建一个简单的自定义指令

此示例的完整代码在此处可用:codepen.io/AjdinImsirovic/pen/yxWObV

在我们的 HTML 中,我们将添加以下简单代码:

<div id="app" class="container mt-5">
  <h1 class="h2">{{ heading }}</h1>
  <div v-custom-directive>
    Just some text here
  </div>
</div>

在我们的 JS 中,我们将全局添加我们的customDirective

Vue.directive('customDirective', {
  inserted: function(el) {
    el.style.cssText = `
      color: blue; 
      border: 1px solid black; 
      background: gray; 
      padding: 20px; 
      width: 50%;
    `
  }
});

new Vue({
  el: '#app',
  data() {
    return {
      heading: 'A custom global directive'
    }
  }
});

在先前的代码中,我们使用了inserted指令钩子。使用此钩子,当将指令绑定到的元素插入到其父节点中时,将运行指令的代码。

当这种情况发生时,该元素将根据我们分配给el.style.cssText的值进行样式设置。

当然,没有什么能阻止我们在一个元素上使用多个自定义指令。例如,我们可以指定几个自定义指令,然后根据需要混合和匹配它们。

在下一节中,我们将重写全局自定义指令为本地自定义指令。

使用本地指令

现在让我们看看如何重写先前的代码,使我们的指令使用本地指令而不是全局指令。

在本节中,我们将构建一个非常简单的自定义指令。我们将使用第四章中的一个示例,过滤器和混合,并在此基础上构建,以便我们可以轻松地将其与先前的示例进行比较,只是这次使用一个简单的本地自定义指令。

此示例的代码在此处可用:codepen.io/AjdinImsirovic/pen/yxWJNp

在我们的 HTML 中,我们将指定以下代码:

<main id="app">
    <custom-article v-custom-directive></custom-article>
    <custom-article></custom-article>
    <another-custom-article v-another-custom></another-custom-article>
</main>

在我们的 JS 中,我们将指定以下代码:

const anotherCustom = {
  inserted: function(el) {
    el.style.cssText = `
      color: green; 
      border: 1px solid black; 
      background: yellow; 
      padding: 20px; 
      width: 50%;
    ` 
  }
}

const customArticle = {
  template: `
    <article>
      Our own custom article component!
    </article>`
}

Vue.component('another-custom-article', {
  template: `
    <article>
      Another custom article component! 
      This one has it's own child component too!
      Here it is:
      <custom-article v-custom-directive></custom-article>
    </article>`,
    components: {
      'customArticle': customArticle
    },
    directives: {
      customDirective: {
        inserted: function(el) {
          el.style.cssText = `
            color: blue; 
            border: 1px solid black; 
            background: gray; 
            padding: 20px; 
            width: 50%;
          ` 
      }
    } 
  } 
})

new Vue({
    el: '#app',
    components: { 
      'customArticle': customArticle,
    },
    directives: {
      customDirective: {
        inserted: function(el) {
          el.style.cssText = `
            color: blue; 
            border: 1px solid black; 
            background: gray; 
            padding: 20px; 
            width: 50%;
          ` 
       }
     },
     'anotherCustom': anotherCustom
  }
})

在下一节中,我们将看到如何将值传递给自定义指令。

将值传递给自定义指令

我们将通过允许我们的自定义指令接收参数来改进本章的初始示例。此示例的代码在此笔中可用:codepen.io/AjdinImsirovic/pen/xaNgPN

这是我们示例中将值传递给自定义指令的 HTML:

<div id="app" class="container mt-5">
  <h1 class="h2">{{ heading }}</h1>
  <button v-buttonize="tomato">
    Just some text here
  </button>
  <button v-buttonize="lightgoldenrod">
    Just some text here
  </button>
  <button v-buttonize="potato">
    Just some text here
  </button> 
</div>

以下是 JavaScript 代码:

Vue.directive('buttonize', {
  bind(el, binding) {
    var exp = binding.expression;
    el.style.cssText += `
      padding: 10px 20px; 
      border: none; 
      border-radius: 3px; 
      cursor: pointer
    `;
    switch(exp) {
      case 'tomato':
          el.style.cssText += `
            background: tomato;
            color: white;
          `;
          break;
       case 'lightgoldenrod':
          el.style.cssText += `
            background: darkgoldenrod;
            color: lightgoldenrod;
          `;
          break;
        default:
            el.style.cssText += `
              background: gray;
              color: white;
            `
    }
  }
});

最后,在 JS 中,我们添加带有options对象的 Vue 构造函数:

new Vue({
  el: '#app',
  data() {
    return {
      heading: 'A custom global directive'
    }
  }
});

请注意,指令钩子参数的具体设置可以在vuejs.org/v2/guide/custom-directive.html#Directive-Hook-Arguments找到。对我们最感兴趣的一个参数是binding,它是一个带有这些属性的对象:namevalueoldValueexpressionargmodifiers

在上面的代码中,我们看到了传递两个不同值的示例,这些值根据传递的值给出了不同的结果。我们还看到了当我们传递一个无意义的值(利用switch语句的default分支)时会发生什么。

在下一节中,我们将讨论通过构建 Vue 插件来进一步扩展 Vue 功能的方法。

使用 Vue 插件。

一些流行的 Vue 插件是 Vuex 和 Vue-router。当我们需要为 Vue 全局提供额外的功能时,就会使用 Vue 插件。有一些非常常见的情况下,Vue 插件可能会有用:添加全局方法,添加全局资产,添加Vue.prototype上的实例方法,或者添加全局混合。

Vue 插件的亮点在于能够与社区共享。要了解 Vue 插件系统的广泛性,可以导航到以下网址:github.com/vuejs/awesome-vue#components--librariesvuejsexamples.com/

接下来,我们将创建一个简单的 Vue 插件。

创建最简单的 Vue 插件

我们将从创建最简单的 Vue 插件开始。为了做到这一点,我们将再次使用 Vue CLI,版本 3。设置 Vue CLI 的说明在第三章中可用,使用 Vue-CLI、组件、Props 和 Slots

首先,我们需要初始化一个新项目。将控制台导航到要创建新 Vue 项目的父文件夹,并运行以下命令:

vue create simple-plugin
cd simple-plugin
npm run-serve

当我们运行这三个命令中的第一个时,会有一些问题,之后会运行大量的软件包。这可能需要一些时间——一个休息的好机会。一旦完成,并且我们已经运行了前面列出的另外两个命令,我们的样板 Vue 应用将在localhost:8080上可用。

首先,让我们在src文件夹内创建一个新的文件夹,并将其命名为plugins。接下来,在plugins文件夹内,让我们创建另一个文件夹,我们将其称为SimplePlugin。在SimplePlugin文件夹内,让我们创建一个新文件,并将其命名为index.js

Vue 插件是一个对象。为了让我们的 Vue 应用程序可以访问插件对象,我们需要通过导出来使其可用。因此,让我们将这个导出代码添加到我们的index.js文件中:

export default {

}

Vue 的插件对象有一个install方法。install方法接受两个参数。第一个参数是Vue对象,第二个参数是options对象。因此,我们将在plugin对象内添加install方法:

export default {
    install(Vue, options) {
        alert('This is a simple plugin and currently the options argument is ' + options);
    }
}

目前,在我们的install方法内,我们只是向浏览器发出警报消息。这是我们的插件可以具有的功能的绝对最小值。有了这个功能,现在是时候在我们的应用程序中使用我们的插件了。

请注意,我们还将options参数连接到我们的警报消息中。如果我们不这样做,我们的 Vue-cli 将抛出一个错误,指出options 已定义但从未使用。显然,它更青睐于(no-unused-vars)的情况。

要使用插件,我们需要打开我们的main.js文件,并通过在main.js文件的第三行添加这两行代码来导入插件:

import SimplePlugin from './plugins/SimplePlugin'
Vue.use(SimplePlugin)

首先,我们导入插件并指定导入路径。接下来,我们将我们的插件作为参数添加到Vue.use方法中。

有了这个,我们已经成功地创建了最简单的插件。打开本地项目localhost:8080,你将看到一个警报消息,其中说明:

This is the simplest possible Vue plugin and currently the options argument is undefined

接下来,我们将看到如何将选项对象添加到我们的插件中。

创建带有定义选项的插件

由于我们设置项目的方式,我们将保持SimplePlugin不变,在 Vue 插件探索的这一部分,我们将在项目的plugins文件夹内添加另一个文件夹。我们将称这个文件夹为OptionsPlugin,并在其中再次创建一个index.js文件。

接下来,让我们更新main.js文件,使其看起来像这样:

import Vue from 'vue'
import App from './App.vue'

//import SimplePlugin from './plugins/SimplePlugin'
import OptionsPlugin from './plugins/OptionsPlugin'

//Vue.use(SimplestPlugin)
Vue.use(OptionsPlugin)

Vue.config.productionTip = false

new Vue({
  render: h => h(App)
}).$mount('#app')

现在,回到OptionsPlugin/index.js,我们将添加以下代码:

export default {
  install(Vue) {
    Vue.directive('text-length', {
        bind(el, binding, vnode) {
            const textLength = el.innerText.length;
            console.log("This element, " + el.nodeName + ", has text with " + textLength + " characters");

            el.style.cssText = "border: 2px solid tomato";
        }
    })
  }
}

请注意,我们在install方法中完全省略了options对象。原因很简单:options对象是可选的,不提供它不会破坏我们的代码。

在先前的插件定义中,我们获取了el.innerText字符串的长度,然后将其记录到控制台中。此外,应用了我们插件自定义的v-text-length指令的el也将通过红色边框更加显眼。

接下来,让我们在组件的模板中使用来自我们的插件的功能。具体来说,我们将在src/components文件夹中的HelloWorld.vue文件的开头使用它:

<template>
  <div class="hello">
    <h1 v-text-length>{{ msg }}</h1>

此时在浏览器中运行我们的应用程序将在控制台中产生以下消息:

This element, H1, has text with 26 characters

现在,我们可以引入我们的options对象。options对象的目的是允许我们自定义受v-text-length指令影响的 HTML 元素的显示方式。换句话说,我们可以决定让我们的插件的用户根据我们传递的选项来选择不同类型的样式。

因此,让我们使用以下代码更新我们的插件:

const OptionsPlugin = { 
  install(Vue, options) {
    Vue.directive('text-length', {
        bind(el, binding, vnode) {
            const textLength = el.innerText.length;
            console.log("This element, " + el.nodeName + ", has text with " + textLength + " characters");

      if (textLength < 40) {
        el.style.cssText += "border:" + options.selectedOption.plum;
      } else if (textLength >= 40) {
        el.style.cssText += "border:" + options.selectedOption.orange;
      }
        }
    })
  }
};

export default OptionsPlugin;

在上述代码中发生了一些事情。首先,我们正在即时创建一个对象,并将其分配给const OptionsPlugin。在文件底部,我们正在导出我们刚刚定义的OptionsPlugin

optionsPlugin对象内部,我们使用了一些 if 语句,根据el元素的文本节点中找到的文本长度来提供不同的样式。如果文本长度小于 40 个字符,那么我们将为options.selectedOption赋值

.plum赋给border CSS 属性。

否则,如果文本长度等于或大于 40 个字符,我们将在问题元素的内联style属性中的border CSS 属性赋值为options.selectedOption.orange的值。

接下来,让我们设置这些选项值。我们将在我们的main.js文件中进行。我们将更新使用插件的部分为以下代码:

Vue.use(OptionsPlugin, {
  selectedOption: {
    plum: "5px dashed purple",
    orange: "10px double orange"
  }
})

最后,在HelloWorld.vue文件中,我们只进行了轻微的更新。我们将插件定义的指令添加到紧随h1标签之后的p标签中:

<template>
  <div class="hello">
    <h1 v-text-length>{{ msg }}</h1>
    <p v-text-length>

现在,当我们运行我们的应用程序时,我们将在控制台中看到以下文本:

This element, H1, has text with 26 characters
This element, P, has text with 121 characters

在我们的视口中,这个插件将在h1标签周围添加一个虚线紫色边框,并在p标签周围添加一个双层橙色边框。

现在我们了解了插件可以被创建和使用的基本方式,我们可以想出创造性的方法让我们的插件做一些更有用的事情。例如,我们可以通过添加一个工具提示来改进现有的插件,该工具提示将显示页面上不同元素中存在的单词数量。我们还可以添加颜色强度:单词越多,我们可以给这个“字符计数”徽章更多的颜色。

或者,我们可以列出样式属性或类属性中存在的值,或者两者都有。这个插件对于快速检查样式而不用打开开发工具可能会很有用,这在较小的屏幕或只有一个屏幕的工作站上可能会很有用。

接下来,我们将讨论如何发布一个 Vue 插件。具体来说,我们将发布我们刚刚制作的 OptionsPlugin。

发布一个 Vue 插件

撰写一个 npm 插件的先决条件是在网站上注册并验证你的电子邮件地址。因此,在 npm 上撰写你的 Vue 插件的第一步是访问 www.npmjs.com 并注册一个账户。

我们将在 npm 上发布我们的 Vue 插件。首先,让我们检查是否已经有一个用户。在控制台中运行以下命令:

npm whoami

如果出现错误,您需要通过运行此命令创建一个新用户:

npm adduser

然后,只需按照说明将自己添加为用户。

添加一个简单的插件

添加一个简单的、单文件插件,只需在您选择的文件夹中运行 npm init。这个命令将帮助您创建一个 package.json 文件。

这是提供的问题和答案列表:

package name: "vue-options-plugin"
version: (1.0.0)
description: A simple Vue plugin that shows how to use the options object
entry point: "OptionsPlugin.vue"
test command:
git repository:
keywords:
license: (ISC)
About to write to ...
Is this ok? (yes)

npm init 实用程序提供的默认答案列在圆括号中。要接受默认值,只需按 Enter 键。否则,只需输入所需的答案。

npm 作者还有一个作用域的概念。作用域就是你的用户名。不用担心作用域的最佳方法是在你的 .npmrc 文件中设置它,通过命令行运行以下命令:

npm init --scope=username

当然,您需要用您的实际用户名替换 username 这个词。

完成后,运行 dir 命令列出文件夹的内容。它应该只列出一个文件:package.json。现在,我们可以创建另一个文件,命名为 OptionsPlugin.vue

touch OptionsPlugin.vue

让我们快速验证我们的 package.json 文件是否像这样:

{
  "name": "vue-options-plugin",
  "version": "1.0.0",
  "description": "A simple Vue plugin that shows how to use options object",
  "main": "OptionsPlugin.vue",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "<your-username-here>",
  "license": "ISC"
}

接下来,让我们用这段代码更新 OptionsPlugin.vue 文件:

const OptionsPlugin = { 
  install(Vue, options) {
    Vue.directive('text-length', {
        bind(el, binding, vnode) {
            const textLength = el.innerText.length;
            console.log("This element, " + el.nodeName + ", has text with " + textLength + " characters");

      if (textLength < 40) {
        el.style.cssText += "border:" + options.selectedOption.plum;
      } else if (textLength >= 40) {
        el.style.cssText += "border:" + options.selectedOption.orange;
      }
        }
    })
  }
};

export default OptionsPlugin;

最后,让我们添加一个README.md文件。md文件扩展名代表 Markdown,这是一种使在线内容编写变得非常容易的格式。我们将向README添加以下内容:

# optionsplugin
<p> A demo of making a simple Vue 2 plugin and using it with values stored in the options object. This plugin logs out to the console the number of characters in an element. It also adds different CSS styles based on the length of characters in the element.</p>

## Installation
```bash

npm install --save optionsplugin

```js
## Configuration
```javascript

import Vue from 'vue';

import OptionsPlugin from 'optionsplugin'

Vue.use(OptionsPlugin, {

selectedOption: {

plum: "5px dashed purple",

orange: "10px double orange"

}

})

```js
## Usage
<p>To use it, simply add the plugin's custom directive of v-text-length to an element in your template's code.</p>

这应该是我们插件描述的一个很好的起点。我们随时可以稍后改进README。现在我们已经准备好package.jsonREADME.mdOptionsPlugin.vue,我们可以通过运行以下命令简单地发布我们的插件:

npm publish --access=public

我们需要为我们的npm publish命令提供--access=public标志,因为作用域包默认为私有访问,我们需要明确覆盖此设置。

一旦发布,我们的控制台将输出以下信息:

+ vue-options-plugin@1.0.0

这是我们成功发布插件的标志。我们的新插件现在有了自己的主页,位于以下 URL:

www.npmjs.com/package/vue-options-plugin

最后,让我们看看如何将我们新添加的插件安装到另一个项目中。

在 Vue CLI 3 中安装我们的 NPM 插件

npm安装我们的 Vue 插件,我们需要首先创建一个新项目。让我们运行这些命令:

vue create just-another-project
cd just-another-project
npm run-serve

现在,我们可以通过运行以下命令添加我们的npm插件:

npm install --save vue-options-plugin

就是这样;现在,我们的插件已经在我们的项目中可用,我们可以像之前描述的那样导入它来使用:

import VueOptionsPlugin from 'vue-options-plugin'

现在,我们可以根据需要使用我们插件的功能。

要学习的其他插件

查看其他人的代码的良好编码示例总是很有益的,这样我们就可以从中学习。我们可以学习并可能贡献的一些有用的插件包括:

摘要

在本章中,我们讨论了在 Vue 中创建自定义指令和自定义插件。我们介绍了如何构建自定义指令,以及如何创建全局和局部自定义指令。我们还讨论了如何向自定义指令传递数值以及如何使用 Vue 插件。我们还介绍了如何创建一些自定义 Vue 插件。最后,我们看到了如何将我们的插件发布到npm,以及如何从 NPM 在我们的项目中安装它。

在接下来的章节中,我们将探讨如何通过过渡和动画使我们的应用程序更具交互性。

第六章:过渡和动画

在这一章中,我们将看看如何在 Vue 中使用过渡和动画。这是一个很大的主题,需要更多的章节来覆盖。因此,我们将处理一些基本概念,以便在将来构建。

我们将重点关注以下主题:

  • 理解 CSS 过渡和动画

  • 使用transition组件实现过渡

  • 在 Vue 中使用 CSS 过渡和动画

  • 与第三方 CSS 和 JS 库集成

  • 绑定 CSS 样式

  • 与过渡组一起工作

  • JavaScript 动画钩子

阅读完本章后,您应该对 Vue 中如何使用过渡和动画有扎实的理解。

CSS 中的过渡和动画

要了解 Vue.js 如何处理过渡和动画,我们首先需要快速复习一下它们在 CSS 中的工作原理。我们将专注于基础知识,目标是重新审视管理过渡和动画的原则。我们还将看看它们的区别。目标是更好地理解 Vue 如何帮助,而不是深入了解过渡和动画的细微差别。

CSS 过渡的工作原理

当我们悬停在一个元素上时,我们将该元素置于悬停状态。当用户通过与我们的网页进行交互触发悬停状态时,我们可能希望强调这种状态的变化已经发生。

为了强调状态的变化,我们可以例如在用户悬停在元素上时改变该元素的 CSS background-color属性。

这就是 CSS 过渡发挥作用的地方。当我们为 CSS 过渡编写代码时,我们指示浏览器如何显示对该特定 CSS 属性所做的更改-在我们的例子中是background-color属性。

假设我们有一个 HTML button元素。该元素的 CSS 属性background-color设置为red

button {
  background-color: red;
}

当用户悬停在按钮上时,我们希望将background-color属性的值从red更改为blue。我们可以这样做:

button:hover {
  background-color: blue;
}

示例代码在这里可用:codepen.io/AjdinImsirovic/pen/LJKJYY

然而,这种颜色的变化是突然的。为了平滑过渡HTML 元素的 CSS 属性从一个值到另一个值,我们使用 CSS 的transition属性。transition属性是一个简写的 CSS 属性。它只是另一个我们在目标元素上指定的 CSS 属性,我们希望对其应用这种平滑过渡。

在我们的例子中,我们希望平滑地将按钮从红色背景过渡到蓝色背景。我们只需在按钮元素上添加简写的transition属性,并在这个transition属性上设置两个值:

button {
 background-color: red;
 transition: background-color 4s;
}
button:hover {
 background-color: blue;
}

这是公式:

transition: property-to-transition transition-duration, property-to-transition transition-duration

在我们的例子中,我们只为一个属性指定了持续时间,但我们可以根据需要添加更多。

CSS 动画的工作原理

在上一个例子中,我们看到了一个简单的过渡。在这个例子中,我们将把过渡转换为动画。更新后的 CSS 代码将如下所示:

button {
  background-color: red;
}
button:hover {
  animation: change-color 4s;
}
@keyframes change-color {
  0% {
    background: red;
  }
  100% {
    background: blue;
  }
}

在上一个代码中,我们已经将我们简单的 CSS 过渡转换为了 CSS 动画。

此示例可在此链接找到:codepen.io/AjdinImsirovic/pen/WaNePm

然而,它并不完全相同。当我们悬停在按钮上时,我们并没有得到与过渡示例中相同的行为。原因是我们已经指定了动画的初始状态(为0%)和最终状态(为100%)。因此,我们实际上是将我们在过渡示例中的行为映射到动画示例中的行为。

然而,当我们将鼠标指针从按钮上移开时,动画并不会倒回到初始状态,而是突然切换回原始的红色背景颜色。在 CSS 中,没有mouseout属性。

然而,我们可以在中间添加额外的步骤。例如,我们可以在变化动画的 50%处将背景颜色设置为绿色。结果可以在此 URL 中看到:codepen.io/AjdinImsirovic/pen/QZWWje

在我们深入了解 Vue 如何实现过渡和动画之前,让我们先看看它们在 CSS 中的区别。

CSS 中过渡和动画的区别

以下是 CSS 中过渡和动画之间的两个快速、不完整的区别列表。

CSS 过渡的规则

以下是 CSS 过渡的一些重要规则:

  • 过渡只有暗示的开始和结束状态

  • 浏览器决定了过渡的执行方式;换句话说,浏览器决定了过渡的中间步骤如何执行

  • 我们只能指定要过渡的确切 CSS 属性,以及持续时间、缓动等

  • 过渡是被触发的;触发可以是悬停或页面上元素的出现(通过 JavaScript)

  • 过渡不能循环

  • 当触发状态(悬停状态)被恢复时,过渡会以相反的方式播放,也就是当鼠标取消悬停

  • 过渡语法比动画的语法更简单

接下来,让我们列出 CSS 动画的重要概念。

CSS 动画规则

以下是 CSS 动画的不完整规则列表:

  • 动画允许我们指定 CSS 属性的初始状态、中间状态和结束状态

  • 我们的 CSS 动画中可以有尽可能多的步骤

  • 我们可以延迟动画,播放x次(无限次),或者以相反的方向播放它们

  • 动画不一定要被触发,但它们可以被触发

在弄清楚这些基本区别之后,让我们接下来看看如何在 Vue 中处理过渡和动画。

Vue 中的过渡元素

让我们看看之前的纯 CSS 过渡的示例,转换成 Vue。在下面的示例中,第一个按钮包裹在一个自定义组件中,而第二个按钮只是常规的 HTML 按钮元素。它们仍然共享相同的样式,如应用程序的 CSS 中指定的那样:

<!-- HTML -->
<div id="app">
  <button>Hover me!</button>
  <custom-component></custom-component>
</div>

// JS
Vue.component('customComponent', {
  template: `
    <button>Hover me too!</button>
  `
});
new Vue({
  el: '#app'
});

/* CSS */
button {
  background-color: red;
  transition: background-color 4s;
}
button:hover {
  background-color: blue;
}
/* some additional styling */
* {
  border: none;
  color: white;
  padding: 10px;
  font-size: 18px;
  font-weight: 600;
}

之前的代码可以在这里找到:codepen.io/AjdinImsirovic/pen/vVYERO. 如例所示,在这种情况下,Vue 并没有偏离纯 HTML 和 CSS 中过渡和动画的工作方式。

Vue 并不是为了覆盖 CSS 过渡和动画的正常用例而设计的,而是为了与它们一起工作,有一个特定的目标:在屏幕上过渡其组件的出现和移除。这些组件的添加和移除是通过 Vue 的transition元素来完成的。

例如,当您希望一个组件中的事件影响另一个组件的添加和移除时,您只需将另一个组件包裹在transition元素中。从之前的纯 CSS 示例构建,这是 Vue 中的一个简单实现:

<!-- HTML -->
<div id="app">
  <button v-on:click="show = !show">
    Show? {{ show }}
  </button>
  <transition>
    <span v-if="show">
      <custom-component></custom-component>
    </span>
  </transition>
</div>

// JS
Vue.component('customComponent', {
  template: `
    <button>Hover me!</button>
  `
});
new Vue({
  el: '#app',
  data: {
    show: true
  }
});

/* CSS is the same as in the previous example */

示例代码可以在这里找到:codepen.io/AjdinImsirovic/pen/ZqExJO

如果您需要元素在初始页面加载时平滑出现,而不受条件限制,那么您可以在转换包装器上使用appear属性,就像这样:<transition appear>

在上面的代码中发生的是,我们有条件地切换custom-component元素的挂载,这取决于用户是否点击了第一个按钮。请注意,原始的 CSS 转换在两个按钮中仍然以完全相同的方式工作。当我们悬停在它们中的任何一个上时,我们仍然会得到从红色到蓝色的四秒过渡背景颜色。当我们从任何一个按钮悬停离开时,浏览器仍然会处理按钮背景的反向过渡。

然而,第二个按钮在屏幕上的挂载是没有任何转换的。第二个按钮简单地在点击第一个按钮时出现和消失,没有任何缓入或缓出。

为了实现这种逐渐出现和消失的效果,transition元素带有内置的 CSS 类名。这些内置的转换类名也被称为动画钩子。这些动画钩子描述了包裹在transition元素内的组件的起始状态、结束状态和中间状态;也就是说,它们描述了受影响的组件将如何在屏幕上切换开和关闭。

我们可以将动画钩子添加到进入转换或离开转换。进入转换类是v-enterv-enter-activev-enter-to。离开转换类是v-leavev-leave-activev-leave-to

设置进入转换

为了在上一个例子的基础上进行扩展,我们将利用这些动画钩子使第二个按钮的出现和消失更加平滑。上一个例子和这个例子之间唯一的区别是在我们的 CSS 中添加了动画钩子:

.v-enter {
  opacity: 0;
}
.v-enter-active {
 transition: opacity 3s;
}

这个例子的代码可以在以下链接找到:codepen.io/AjdinImsirovic/pen/MPWVNm

如果我们将第二个按钮的外观想象成一个常规的 CSS 转换,那么.v-enter动画钩子将是初始转换状态,.v-enter-active将是中间步骤,.v-enter-to将是最终转换状态,也就是元素将要转换的状态。

因为我们在示例中没有使用.v-enter-to动画钩子,所以我们得到的行为如下:当点击第一个按钮时,第二个按钮需要三秒钟才能从初始值零过渡到暗示的值一(不透明度)。这就完成了我们的进入过渡。

设置离开过渡

我们之前的示例有一个小问题:当我们再次点击第一个按钮时,第二个按钮会立即消失,因为它的不透明度值会在没有任何过渡的情况下重置为零。原因很简单:我们没有指定任何离开过渡钩子,所以按钮就会消失。我们将在下一个示例中修复这个问题,只需简单地指定离开过渡,就像这样:

.v-leave {
  opacity: 1;
}
.v-leave-active {
  transition: opacity 3s;
}
.v-leave-to {
  opacity: 0;
}

完整的代码可以在这里找到:codepen.io/AjdinImsirovic/pen/XxWqOy。在这段代码中,我们做的是:当组件需要被动画移出时,我们过渡的初始状态是.v-leave。在.v-leave动画钩子中的 CSS 声明是opacity: 1。接下来,我们指定中间步骤:要过渡的 CSS 属性,即opacity,以及过渡的持续时间:3s。最后,我们指定过渡的完成状态,其中opacity被设置为零的值。

从这些示例中我们可以得出结论,离开过渡的动画钩子(v-leavev-leave-activev-leave-to)应该是镜像的,就像进入过渡的动画钩子(v-enterv-enter-activev-enter-to)一样。

我们还可以得出结论,过渡组件和随之而来的动画钩子用于在屏幕上挂载和卸载组件。当在屏幕上过渡组件时,动画钩子的自然进展是这样的:

.v-enter --> .v-enter-active --> .v-enter-to --> .v-leave --> v-leave-active --> .v-leave-to

我们还可以将共享相同值的某些 CSS 选择器分组,如下所示:

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

这个示例可以在以下网址找到:codepen.io/AjdinImsirovic/pen/dgyKMG

正如在这里所看到的,.v-enter(初始进入动画钩子)与.v-leave-to(最后离开动画钩子)被合并在一起,这是因为过渡必须以相反的方式播放,以获得最符合预期的行为。同样,我们将中间步骤,即-active钩子分组,以具有相同的transition CSS 属性。最后,进入动画的最终钩子需要与初始离开动画钩子共享 CSS 声明。此外,由于.v-enter-to.v-leave的值默认情况下是隐含的,我们甚至可以省略它们,仍然可以拥有一个正常工作的组件过渡,类似于官方文档中描述的过渡。

为了简化推理,我们在最近的示例中还将data选项的show键更改为false的值。这样,最初组件不会挂载到 DOM 上。只有当用户点击第一个按钮时,第二个按钮的进入动画钩子才会启动,并平滑地过渡组件。再次点击时,第二个按钮的离开动画钩子将启动,并以相反的方式过渡组件。这很重要,因为最初我们让进入动画过渡组件的卸载,而离开动画过渡组件重新挂载到页面上,这可能会使事情变得更加复杂。

命名过渡组件

我们可以给过渡元素添加name属性。这样做会改变动画钩子的命名约定。例如,如果我们给过渡命名为named,那么动画钩子将需要按照以下方式重新命名。对于每个过渡类,我们将用name属性的值替换v-的开头。因此,v-enter将变成named-enterv-leave将变成named-leave,依此类推。

让我们用一个命名过渡来重写前面的例子:

<!-- HTML -->
<div id="app">
  <button v-on:click="show = !show">
    Show? {{ show }}
  </button>
  <transition name="named">
    <span v-if="show">
      <custom-component></custom-component>
    </span>
  </transition>
</div>

/* CSS */
/* 'named' transition */
.named-enter, .named-leave-to {
  opacity: 0;
}
.named-enter-active, .named-leave-active {
  transition: opacity 3s;
}
.named-enter-to, .named-leave {
  opacity: 1;
}

// JS is unchanged

此示例的代码可在此 CodePen 中找到:codepen.io/AjdinImsirovic/pen/MPWqgm

使用过渡组件的 CSS 动画

CSS 动画也可以通过过渡组件来实现。以下是将以 CSS 过渡为例的先前示例转换为使用 CSS 动画的示例。我们将从 HTML 开始:

<div id="app">
  <button v-on:click="show = !show">
    Show? {{ show }}
  </button>
  <transition name="converted">
    <span v-if="show">
      <custom-component></custom-component>
    </span>
  </transition>
</div>

接下来,我们将添加以下 JavaScript 代码:

Vue.component('customComponent', {
  template: `
    <button>Lorem ipsum</button>
  `
});
new Vue({
  el: '#app',
  data: {
    show: false
  }
});

我们也会添加一些简单的样式:

/* 'named' transition is replaced with 'converted' animation */
.converted-enter-active {
  animation: converted .5s;
}
.converted-leave-active {
  animation: converted .5s reverse;
}
@keyframes converted {
  0% { opacity: 0; }
  35% { background-color: purple; }
  65% { background-color: green; }
  100% { opacity: 1; }
}
/* other styles */
button {
  background-color: red;
  transition: background-color 4s;
}
button:hover {
  background-color: blue;
}
/* some additional styling */
* {
  border: none;
  color: white;
  padding: 10px;
  font-size: 18px;
  font-weight: 600;
}
span {
  display: inline-block;
}

此示例的代码在此处可用:codepen.io/AjdinImsirovic/pen/vVEXEv。转换后的动画与以前使用 CSS 过渡的示例完全相同,只是在动画完成的 35%和 65%处改变了动画行为。我们得到的效果有点像边框颜色效果,即使我们正在改变此元素的background-color属性。这证实了我们已经讨论过的一些结论,即以下内容:

  • Vue 中的transition元素影响整个<transition>组件的出现和消失,而不是其内容

  • 实际动画可以有许多步骤;换句话说,要获得与 CSS 过渡示例中相同的效果,只需简单地删除我们在动画完成的 35%和 65%处指定的步骤即可。

在下一节中,我们将讨论自定义过渡类。

自定义过渡类

自定义过渡类在我们想要从第三方 CSS 动画库中添加功能时非常有用。在这个例子中,我们将使用Animate.CSS动画库,可以在这里找到:daneden.github.io/animate.css/

官方文档在此 URL 上充分涵盖了自定义过渡类的使用:vuejs.org/v2/guide/transitions.html#Custom-Transition-Classes

唯一需要添加的是我们一直在构建的示例,可以在这里找到:codepen.io/AjdinImsirovic/pen/rqazXZ

示例的代码如下。首先,我们将从 HTML 开始:

<div id="app">
  <button v-on:click="show = !show">
    Show? {{ show }}
  </button>
  <transition :duration="4000"
     name="converted"
     enter-active-class="rubberBand animated"
     leave-active-class="bounceOut animated">
        <div v-if="show">
          <custom-component>
          </custom-component>
        </div>
  </transition>
</div>

接下来,让我们看看 JavaScript:

Vue.component('customComponent', {
  template: `
    <button>Lorem ipsum</button>
  `
});
new Vue({
  el: '#app',
  data: {
    show: false
  }
});

最后,在我们的样式中,我们将设置一些基本的 CSS 声明:

button {
  background-color: red;
  transition: background-color 4s;
}
button:hover {
  background-color: blue;
}

* {
  border: none;
  color: white;
  padding: 10px;
  font-size: 18px;
  font-weight: 600;
}
* { overflow: hidden }

基本上,我们指定了与动画钩子相同名称的属性,以及属性名称末尾的额外-class。因此,默认的v-enter-active CSS 类变成了自定义的enter-active-class HTML 属性。然后我们给这个自定义的 HTML 属性赋予我们选择的值。我们给它的值是我们之前选择的 CSS 动画库中要使用的效果的类名——在这种情况下是Animate.CSS库。在之前的代码中,我们还设置了:duration属性,指定过渡的持续时间为 4000 毫秒。在我们的示例中,如果我们设置的:duration属性比我们从第三方库提供的动画的持续时间短,这实际上只会产生效果。例如,尝试将:duration属性设置为 100 毫秒,看看动画被截断。这可能会产生一些有趣的效果。

结合过渡模式、持续时间、键和 v-if。

过渡模式用于在屏幕上平滑地移除一个元素,并无缝地替换为另一个元素。<transition>组件默认的过渡模式是同时过渡:一个元素被移除的同时另一个元素被添加。

但是,在某些过渡中,最好让新元素出现,只有当这个过渡完成时,旧元素才被移除。这种过渡模式被称为in-out过渡模式。要添加它,我们只需使用自定义模式 HTML 属性,并给它赋值in-out,就像这样:

<transition mode="in-out">

或者,我们可能想要使用out-in过渡模式,首先让旧元素过渡出去,然后,当过渡完成时,新元素才过渡进来。

让我们看看实际操作。示例可在此处找到:codepen.io/AjdinImsirovic/pen/yRyPed

以下是 HTML 代码:

<div id="app">
  <transition name="smooth" mode="out-in" :duration="500">
      <button v-if="show" 
              key="first" 
              v-on:click="show = !show">
                Show? {{ show }}
      </button>
      <button v-else
              key="second" 
              v-on:click="show = !show">
                Show? {{ show }}
      </button> 
  </transition>
  <transition :duration="1000"
     enter-active-class="slideInDown animated"
     leave-active-class="slideOutDown animated">
        <div v-if="show">
          <custom-component>
          </custom-component>
        </div>
  </transition>
</div>

我们仍然使用相同的 JS:

Vue.component('customComponent', {
  template: `
    <button>Lorem ipsum</button>
  `
});
new Vue({
  el: '#app',
  data: {
    show: false
  }
});

我们的 CSS 发生了一些变化:

/* CSS classes used are imported from the Animate CSS library
and can be found in Settings of this pen */
/* other styles */
.smooth-enter, .smooth-leave-to {
  opacity: 0;
}
.smooth-enter-active, .smooth-leave-active {
  transition: opacity .5s;
}
.smooth-enter-to, .smooth-leave {
  opacity: 1;
}

button {
  background-color: red;
  transition: background-color 4s;
}
button:hover {
  background-color: blue;
}

* {
  border: none;
  color: white;
  padding: 10px;
  font-size: 18px;
  font-weight: 600;
}
* { overflow: hidden }

我们在转换过渡中在两个button元素之间切换开关。由于这两个元素具有相同的标签名称,我们需要给它们不同的key属性,以便 Vue 可以区分它们。

此外,我们有条件地渲染我们的按钮。虽然我们在第一个按钮中保留了v-if="show"的检查,但在第二个按钮中,我们只是使用了v-else指令,而没有给它一个要检查的值。

在 Vue 中绑定 CSS 样式

在这一部分,我们将讨论如何在组件挂载或移除时,对页面的其他部分进行动画处理。为此,我们将使用v-bind指令,正如我们在前几章中所看到的,我们可以使用这个指令来绑定 HTML 属性。一旦绑定,这些属性就可以从我们的 Vue 实例中进行操作。

我们将演示 CSS 样式绑定的示例是一个简单的入职演示。在网页可用性方面,入职是向 Web 应用的新用户展示网页的整体功能的做法,这是通过突出显示页面的某个部分并显示一个包含一些信息的气泡窗口来实现的,进一步描述了入职过程中特定步骤的功能。

首先,我们需要了解,我们可以通过将v-bind:class指令的值作为对象来静态地绑定 CSS 类,就像下面的例子一样:

<p v-bind:class="{}">Some text...</p>

在对象内部,我们可以简单地将 CSS 类添加为键,将布尔值truefalse作为值。设置为true的 CSS 值将被使用,否则将不会被使用,就像下面的例子一样:

<button v-bind:class="{'btn': true, 'btn-lg': true, 'btn-primary': true, 'btn-secondary': false}">A button</button>

在这个例子中,我们使用了 Bootstrap 框架的 CSS 类。我们将按钮设置为btn-primary类,因为它被设置为true,而不是btn-secondary,它被设置为 false。

因为v-bind指令允许我们以编程方式控制 HTML 属性,所以我们可能会在点击时使我们的应用切换 CSS 类。例如,在一个基本的 Vue 应用中,我们可能会在我们的 HTML 中这样做:

<button v-bind:class="'btn':true','btn-lg':true, 'btn-primary':true, 'btn-secondary':btnClicked">
A button
</button>

在前面的代码中,我们将btnbtn-lgbtn-primary的类设置为true,并将btn-secondary的值设置为btnClicked。接下来,我们将在 JavaScript 中将btnClicked的值设置为false

data: {
  btnClicked: false,
}

最后,我们将为我们的按钮添加点击事件,因此当点击时,btnClicked的值将从true切换到false,反之亦然。以下是代码:

<button 
  v-on:click="btnClicked = !btnClicked" 
  v-bind:class="'btn':true','btn-lg':true, 'btn-primary':true, 'btn-secondary':btnClicked">
    A button
</button>

此示例可在以下网址找到:codepen.io/AjdinImsirovic/pen/KGVvML

我们可以进一步扩展这个示例,使用data属性来存储一组 CSS 类,并使用 JavaScript 三元表达式来检查btnClicked值当前是否设置为truefalse

<!-- HTML -->
<div id="app" class="p-4">
  <h1>Improving dynamic CSS classes example</h1>
  <p class="lead">Click the button below a few times</p>
  <button 
    v-on:click="btnClicked = !btnClicked" 
    v-bind:class="btnClicked ? btnPrimary : btnSecondary">
      btnClicked {{ btnClicked }} 
  </button>
</div>

// JS
new Vue({
  el: '#app',
  data() {
    return {
      btnClicked: false,
      btnPrimary: 'btn btn-lg btn-primary',
      btnSecondary: 'btn btn-lg btn-secondary'
    }
  }
})

前一个示例的代码可以在codepen.io/AjdinImsirovic/pen/wYMEJQ找到。

使用动态 CSS 类在单击时为按钮添加动画

现在,我们可以通过简单地添加来自上述 Animate.CSS 动画库的额外 CSS 类来添加动画。对于前一个示例代码的更新很少。我们只在这里添加了两个 CSS 类:

      btnPrimary: 'btn btn-lg btn-primary bounce animated',

当然,我们还必须包括 Animate.CSS 库,如下所示:codepen.io/AjdinImsirovic/pen/RerEyy。要在两次点击时添加动画,我们只需将btnSecondary的条目更改为:

btnSecondary: 'btn btn-lg btn-secondary tada animated'

现在,按钮将在每次点击时都会有动画。

使用过渡组

虽然单个过渡组件用于包装单个元素,但过渡组用于为多个元素添加动画。它们带有额外的动画钩子:v-move

在接下来的示例中,我们将构建一个简单的功能,用户可以在线奖励一段内容,类似于medium.com/的鼓掌功能,工作原理如下:如果网站的访问者喜欢一段内容,他们可以通过点击鼓掌按钮最多 50 次来奖励它。因此,鼓掌功能就像是网站访问者对一段内容的欣赏程度的计数器。

在我们的实现中,我们将结合我们已经介绍的功能。不同之处在于,我们将使用transition-group组件而不是过渡。这是 HTML 代码:

<!-- HTML -->
<div id="app">
    <div class="tale">
        <transition-group>
          <button 
                  class="bare" 
                  key="howManyClaps" 
                  v-if="clapCount">
                    {{ clapCount }}
          </button>
          <button 
                  class="fa fa-thumbs-o-up animated orange" 
                  key="theClapButton" 
                  v-on:click="aClap()">
          </button>
        </transition-group>
    </div>
</div>

以下是 JS 代码:

new Vue({
  el: "#app",
  data: { 
    clapCount: false
  },
  methods: {
    aClap() {
      var target = document.querySelector('.fa-thumbs-o-up');
      if (!target.classList.contains('wobble')) {
        target.classList.add('wobble');
      }
      setTimeout(function() {
        target.classList.remove('wobble')}, 300
      )
      if (this.clapCount < 10) {
        this.clapCount++
      } else {
        target.classList.remove('orange','wobble')
      }
    }
  }
});

以下是 CSS 代码:

button.bare {
  font-size: 30px;
  background: white;
  border: none;
  margin: 0 20px;
}
button:focus.bare, button:focus.fa {
  outline: 0;
}
button.fa {
  cursor: pointer;
  color: white;
  padding: 20px;
  border-radius: 50%;
  font-size: 30px;
  border: none;
}
.orange {
  background: orange;
}

/* animation hooks */
.v-enter,
.v-leave-to{
  opacity: 0;
  transform: translate(1000px, 500px);
}
.v-enter-active,
.v-leave-active {
  transition: opacity 5s, transform 1s
}

前面的代码可以在此 URL 作为笔记本使用:codepen.io/AjdinImsirovic/pen/JmXJgd

这段代码中发生了几件事情。在 HTML 中,我们使用transition-group组件来处理两个按钮。在 JS 中,我们设置了我们掌声行为的逻辑。我们开始将clapCount设置为false,这将强制转换为零。在 CSS 中,我们为按钮设置样式,并使用动画钩子。transformtransition的值已经设置为极端值,以便通过调整这些值来更好地理解它们的工作方式(例如,在X轴上的平移为1000 px,在Y轴上的平移为500 px)。

JavaScript 动画钩子

我们可以将 Vue 的transition类用作 JavaScript 方法。就像生命周期钩子一样,我们不必访问它们中的任何一个。或者我们可以挑选出我们想要使用的那些。首先,在我们的 Vue 构造函数的methods选项中,我们可以指定要对所有这些方法做什么:

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

正如我们所看到的,我们有四种进入过渡的方法,另外还有四种离开过渡的方法。所有的方法都接受el参数,而enterleave方法还接受done参数,表示动画完成。如果没有使用done参数,钩子将在不等待done回调完成的情况下被调用,过渡将立即完成。

让我们使用这些 JavaScript 动画钩子来重写前面的例子。为了保持易于理解,我们将把官方文档的例子整合到我们的例子中,这样我们就可以看到当动画钩子仅通过 JavaScript 调用时,这个例子是如何工作的。

这是我们将在 HTML 中使用的代码:

<transition 
  v-on:before-enter="beforeEnter"
  v-on:enter="enter"
  v-on:leave="leave"
  v-bind:css="false">
<p v-if="show" style="font-size:25px">Animation example with velocity</p>
</transition>

这是我们将在 JS 中使用的代码:

new Vue({
  el: "#app",
  data: { 
    clapCount: false
  },
  methods: {
    beforeEnter: function(el) {
      el.style.opacity = 0
    },
        enter: function (el, done) {
      Velocity(el, { opacity: 1, fontSize: '1.4em' }, { duration: 300 })
      Velocity(el, { fontSize: '1em' }, { complete: done })
    },
    leave: function (el, done) {
      Velocity(el, { translateX: '15px', rotateZ: '50deg' }, { 
      duration: 600 })
      Velocity(el, { rotateZ: '100deg' }, { loop: 2 })
      Velocity(el, {
        rotateZ: '45deg',
        translateY: '30px',
        translateX: '30px',
        opacity: 0
      }, { complete: done })},
    aClap() {
      var target = document.querySelector('.fa-thumbs-o-up');
      if (!target.classList.contains('wobble')) {
        target.classList.add('wobble');
      }
      setTimeout(function() {
        target.classList.remove('wobble')}, 300
      )
      if (this.clapCount < 10) {
        this.clapCount++
      } else {
        target.classList.remove('orange','wobble')
      }
    }
  }
});

这是 CSS:

button.bare {
  font-size: 30px;
  background: white;
  border: none;
  margin: 0 20px;
}
button:focus.bare, button:focus.fa {
  outline: 0;
}
button.fa {
  cursor: pointer;
  color: white;
  padding: 20px;
  border-radius: 50%;
  font-size: 30px;
  border: none;
}
.orange {
  background: orange;
}

示例在这里:codepen.io/AjdinImsirovic/pen/PyzqxM

通过这种理解,很容易在我们的 Vue 构造函数中更改特定方法中的参数,以实现我们 JavaScript 动画的期望效果。

总结

在这一章中,我们讨论了在 Vue.js 中使用过渡和动画。具体来说,我们研究了 CSS 中过渡和动画的工作原理。我们分析了 CSS 中过渡和动画的区别,并建立了它们的规则。我们使用了 Vue 中的过渡和过渡组件,并讨论了动画钩子及其分组进入和离开过渡。我们看到了过渡组件如何命名,并给定了键值,以及如何分配自定义过渡类,以便更轻松地与第三方动画库集成。

我们解释了何时使用过渡模式,以及如何使用:durationconditional指令进一步调整我们的动画。我们提到了在 Vue 中绑定 CSS 样式的重要性,以及这种方法如何用于为我们的 Web 应用程序添加动画。最后,我们看到了如何将基于 CSS 类的过渡转换为基于 JavaScript 的动画钩子。

在下一章中,我们将讨论如何使用 Vuex。

第七章:使用 Vuex

在本章中,我们将学习如何通过使用 Vuex 在 Vue 中管理复杂状态。Vuex 有助于处理 Vue 应用程序中状态管理和深度嵌套组件的问题。

在本章结束时,您将了解 Vuex 解决了什么问题以及它是如何解决这些问题的,您应该了解所有移动部分的适用位置。您还将了解如何构建一个简单的 Vuex 应用程序以及在考虑扩展它时应采取的方法。

具体来说,我们将讨论以下主题:

  • 理解状态

  • 状态管理、数据存储和单向数据流

  • 热重载

  • 构建一个非常简单的 Vuex 应用程序

  • 如何从 Vue DevTools 的 Vuex 选项卡更新状态

  • 构建一个更复杂的 Vuex 应用程序

让我们首先了解状态到底是什么。

什么是状态?

应用程序的状态是其在某一时间点的所有数据。由于我们通常关注当前应用程序的状态,我们可以将其重新表述为以下内容:状态是应用程序当前的数据,是由应用程序采取的先前步骤和应用程序内部的函数对用户交互做出响应而产生的。

那么,在我们的应用程序中是什么改变了它的状态呢?当然是函数。用户与我们的应用程序交互,触发函数来将当前状态更改为其他状态。

然而,随着我们的应用程序增长,组件嵌套几层是很常见的。如果我们说状态是应用程序在任何给定时间应该显示在屏幕上的“真相之源”,那么让我们尽可能地使这个真相之源易于理解和简单易用对我们来说是有益的。

很不幸,在复杂的应用程序中,这并不容易。我们应用的任何部分,应用内的任何组件都可能影响应用的任何其他部分。在我们的应用程序中管理状态有点像玩打地鼠游戏:应用程序中的一个部分的交互会导致应用程序的其他部分出现问题。

关于如何在前端应用程序中管理复杂状态的最佳实践的思考导致了诸如“数据存储”和“单向数据流”等概念。

状态管理、数据存储和单向数据流

管理复杂状态问题的常见解决方案是存储的概念:一个保持应用程序状态所有数据的唯一真相源。一旦我们有了这个中心位置—存储—我们就可以更容易地理解状态,因为现在只需要将状态数据发送到那些在应用程序生命周期中任何时候都需要它的组件。

为了使状态更新更简单,我们需要限制这些更新的方式。这就是单向数据流的作用。通过单向数据流,我们规定了数据在应用程序内部流动的规则,这意味着数据(状态)只能以有限的方式流动,使得在需要时更容易理解状态和调试状态。这种方法也是一个很好的时间节省者,因为现在作为开发者,我们知道可以期待什么;也就是说,要寻找我们知道状态是可变的地方。

Vuex 状态管理模式

Vuex 是 Vue 的一个插件,由 Vue 的核心团队开发。设置非常简单。如果您需要一个快速的原型,您可以简单地从 CodePen 在线编辑器的设置中添加 Vuex 库,如第一章中所述,介绍 Vue

您还可以通过 npm 安装它,使用以下命令:

npm install --save vuex

当试图理解 Vuex 的工作原理时,通常会在网上找到描述 Vuex 作为受 Flux 强烈影响的状态管理模式的参考资料。这在一定程度上是正确的,但有趣的是 Flux 本身受到了 Elm 架构的启发。不管怎样,在 Vuex 中,数据流如下:

  • Vue 组件到动作

  • 动作到变化

  • 变化到状态

  • 状态到 Vue 组件

数据总是以一种方式流动,最终回到起点,更新组件,然后分发动作,然后提交变化,然后改变状态,然后渲染组件,循环重复。因此,从略微不同的角度看单向数据流,我们可以重新表述它,专注于动词来描述存储中数据发生的情况:

  • 动作被分发

  • 变化被提交

  • 状态被改变

  • 组件被渲染

再次看一下这种单向数据流,我们现在可以用这些名词来描述数据流:组件动作突变状态。用动词描述数据流,我们可以将这个过程视为:分发提交突变渲染

在 Vuex 中查看数据流的这两种方式是同一个硬币的两面,是状态更新的同一个循环,因此将这两个简短的列表都记住并不会有害,因为它将有助于加快对基本 Vuex 概念的理解。

为了在视觉上加强这些解释,可以在官方的 Vuex 文档中找到这种单向数据流的图表,网址是:vuex.vuejs.org/vuex.png

你可能会问,为什么要采取这种间接的方式?为什么组件不能直接改变状态?这样做有两个主要原因:首先,在 JavaScript 世界中,异步代码只是一个事实,因此选择在 Vuex 中分离异步和同步操作。因此,动作被设置为异步的,所以它们可以,例如,从服务器获取一些数据,只有在这个异步数据获取完成后才能提交 突变;由于突变是严格同步的,在服务器调用完成之前调用它们是没有意义的。其次,这种分离关注点的方式使得更容易跟踪状态变化,甚至包括时间旅行调试:按时间顺序重新运行突变以跟踪状态的变化并追踪错误。

在 Vuex 状态管理模式中,组件永远不能直接改变全局状态。突变会这样做。

在接下来的部分,我们将看看这些构建块中的每一个。

store

store需要添加到 Vue 实例的根部,以便所有组件都可以共享这个集中的全局状态。通常,我们将 store 声明为const,然后在代码中稍后将其添加到作为参数传递给 Vue 构造函数的对象文字中,就像这样:

const store = new Vuex.Store({ 
  // store details go here
})
new Vue({
 el: '#app',
 store: store,
 // etc
})

接下来,我们将学习有关获取器的知识。

Vuex store 中的获取器

我们的 store 也可以有获取器。获取器允许我们在模板中从状态中返回值。它们有点像计算值。它们是只读的,意味着它们不能改变状态。它们的责任只是读取它并对其进行一些非破坏性的操作。然而,底层数据没有被突变。

因此,我们在存储中使用getters对全局状态执行一些非破坏性工作。那么我们接下来该怎么做呢?我们如何使用它们?我们在应用程序的另一侧使用它们——在组件内部——我们使用computed并从存储中返回getters的值。

Vuex 存储变化

变化,顾名思义,改变状态,并且是同步的。改变状态的函数接收参数:现有状态和有效负载。有效负载参数是可选的。它们负责直接更新 Vuex 中的状态。您可以使用以下语法从操作中执行变化:state.commit

Vuex 存储中的操作

操作以异步和间接的方式更新状态,通过调用我们在存储中定义的一个或多个变化。因此,操作调用所需的变化。另一方面,在组件内部,要执行一个操作,我们使用存储的分派值,使用以下语法:store.dispatch

现在让我们扩展我们的样板代码,包括我们刚刚讨论的内容:

const store = new Vuex.Store({ 
  // store details go here; they usually have:
  state: {
    // state specified here
  },
  getters: {
    // getters are like computed values - they don't mutate state
 },
 mutations: {
   // they mutate the state and are synchronous, 
   // functions that mutate state can have arguments; these arguments are called 'payload'
 },
 actions: {
   // asynchronous functions that commit mutations
 }
})
new Vue({
 el: '#app',
 store,
 // etc
})

在 Vue 构造函数中,我们可以看到,使用 ES6 语法,可以简化构造函数对象字面参数中的store: store键值对,只需使用store

热重载

另一个由 Webpack 的兴起带来的流行概念是热重载。当您的应用程序正在运行时,更新文件时,例如,在组件的一个文件中添加一些变化的作用域样式,Webpack 将热重载更新的文件,而不使用您应用程序中的状态。换句话说,它不会重新加载整个页面,而只会重新加载受更改影响的应用程序部分。这是有用的原因是,使用热模块替换,状态将被保留,如果刷新页面是不可能的。这带来了更快的开发时间和浏览器中更新的无缝体验的额外好处。

使用 Vuex 构建水果计数器应用程序

我们将构建的应用程序只是一个简单的水果计数器应用程序。目标是帮助用户确保每天吃五份水果,我们将设置一个简单的应用程序,开始时有五份水果可供食用,每次点击按钮时,它将减少1的数量。这样,我们可以跟踪我们的健康饮食目标。

我们将通过设置初始状态来开始我们的应用程序,其中只有一个键值对:

const store = new Vuex.Store({
  state: {
    count: 5
  },

接下来,我们将设置getters。正如我们已经学过的,getters只返回状态:

  getters: {
    counter(state) {
      return state.count;
    }
  },

接下来,我们将添加两个 mutations:第一个 mutation,decrementCounter,将通过减去 payload 参数中存储的值来操作计数器。我们将递减 state.count 的值,直到达到0。为了确保state.count的值不能小于0,我们将使用三元语句进行检查,并相应地设置其值。

第二个 mutation,resetCounter,将重置计数器的值为初始状态:

  mutations: {
    decrementCounter(state, payload) {
      state.count = state.count - payload;
      state.count<0 ? state.count=0 : state.count
    },
    resetCounter(state) {
      state.count = 5;
    }
  },

接下来,我们将设置两个操作,decrementreset

  actions: {
    decrement(state, payload) {
      state.commit("decrementCounter", payload);
    },
    reset(state) {
      state.commit("resetCounter");
    }
  }

最后,我们正在设置我们的应用,并在其 Vue 构造函数中指定elstorecomputedmethods选项。

const app = new Vue({
  el: "#app",
  store: store,
  computed: {
    count() {
      return store.getters.counter;
    }
  },
  methods: {
    eatFruit(amount) {
      store.dispatch("decrement", amount);
    },
    counterReset() {
      store.dispatch("reset");
    }
  }
});

接下来,在我们的 HTML 中,我们设置了我们简单应用的结构:

<div id="app">
 <h1>Fruit to eat: {{count}}</h1>
 <button v-on:click="eatFruit(1)">Eat fruit!</button>
 <button v-on:click="counterReset()">Reset the counter</button>
</div>

可以在以下网址找到工作示例:codepen.io/AjdinImsirovic/pen/aRmENx

使用 Vue DevTools 插件跟踪我们的 Vuex 状态

如果你在 Chrome 扩展程序网上商店的搜索栏中输入vuejs devtools,你会得到一些结果。第一个结果是官方插件的稳定版本。第二个结果是 Vue DevTools 扩展的 beta 版本。要查看正在开发的所有选项,并了解这个插件的发展方向,最好安装 beta 版本。有趣的是,一旦在 Chrome DevTools 中打开,两个版本显示相同的信息。目前,消息显示为Ready. Detected Vue 2.5.17-beta.0

与常规版本相比,实验版本多了一些标签,即routingperformance。然而,即使是现有的标签也有一些非常有用的改进。例如,Vuex 标签具有直接从 DevTools 内部更新状态的功能。要访问该功能,只需按下F12键打开 Chrome DevTools。将 DevTools 定位到使用 Vue 扩展的最佳方法是将其设置为Dock to bottom选项。这个选项可以通过按下位于 DevTools 窗格右上角的 DevTools 关闭图标旁边的三个垂直点图标(自定义和控制 DevTools图标)来访问。

启用底部停靠后,打开 Vue 标签,然后在其中激活 Vuex 标签,您将看到两个窗格。最初,左窗格列出了基本状态。这个窗格列出了所有的变化,并允许我们进行时间旅行调试。右窗格列出了实际的负载、状态和变化,因此它给了我们更细粒度的视图,让我们了解在任何给定的变化中发生了什么。要进行任何特定变化的时间旅行,只需将鼠标悬停在其上,然后单击“时间旅行”图标。您还可以选择在列出的任何变化上运行“提交”或“还原”。正如您可能猜到的那样,当执行“提交”命令时,将对当前悬停的变化执行提交,而“还原”命令将撤消特定变化的提交。

另一个有用且有趣的功能是能够直接从 Vuex 标签更新状态。例如,假设我们点击了“吃水果!”按钮几次。现在,我们可以点击变化窗格中的任何给定的“减少计数”变化,然后在右窗格中得到以下信息:

▼ mutation
    payload: 1
    type: ''decrementCounter''
▼ state
    count: 1
▼ getters
    counter: 1

使用这个窗格非常简单。如果我们需要更新我们的状态,在“状态”条目内悬停在“计数:1”上将触发四个图标的出现:编辑值图标、减号图标、加号图标和“复制值”图标,显示为三个垂直点。在这里,我们还可以看到“getter”是只读的证据。悬停在“getter”条目上不会显示任何编辑图标。与此相反,“状态”和“变化”条目都可以从这个窗格中进行编辑。

改进我们的水果计数应用程序

在本节中,我们将对我们的水果计数应用程序进行一些改进。目标是看看我们如何使用 Vuex 来扩展我们的应用程序。

我们将通过添加额外的功能来更新我们的应用程序。即,我们将为不同的水果添加按钮:苹果和梨。要吃的水果数量以及吃的水果的数量和种类也将出现在我们的应用程序中。

这是更新后的 JS 代码。我们首先定义存储中的状态:

const store = new Vuex.Store({
  state: {
    count: 5,
    apples: 0,
    pears: 0
  },

接下来,我们设置 getter:

  getters: {
    counter(state) {
      return state.count;
    },
    appleCount(state) {
      return state.apples;
    },
    pearCount(state) {
      return state.pears;
    }
  },

在定义变化时,我们需要decrementWithApplesCounterdecrementWithPearsCounter,以及resetCounter功能:

  mutations: {
    decrementWithApplesCounter(state, payload) {
      state.count = state.count - 1;
      state.count < 0 ? (state.count = 0) : (state.count, state.apples 
       += 1);
    },
    decrementWithPearsCounter(state, payload) {
      state.count = state.count - 1;
      state.count < 0 ? (state.count = 0) : (state.count, state.pears 
      += 1);
    },
    resetCounter(state) {
      state.count = 5;
      state.apples = 0;
      state.pears = 0;
    }
  },

接下来,我们将列出我们的动作,decrementWithApplesdecrementWithPears和“重置”:

  actions: {
     decrementWithApples(state, payload) {
       setTimeout(() => {
         state.commit("decrementWithApplesCounter", payload);
       }, 1000)
     }, 
    decrementWithPears(state, payload) {
      state.commit("decrementWithPearsCounter", payload);
    },
    reset(state) {
      state.commit("resetCounter");
    }
  }
});

我们将通过添加 Vue 构造函数来结束它:

const app = new Vue({
  el: "#app",
  store: store,
  computed: {
    count() {
      return store.getters.counter;
    },
    apples() {
      return store.getters.appleCount;
    },
    pears() {
      return store.getters.pearCount;
    }
  },
  methods: {
    eatApples(payload) {
      store.dispatch("decrementWithApples", payload);
    },
    eatPears(payload) {
      store.dispatch("decrementWithPears", payload);
    },
    counterReset() {
      store.dispatch("reset");
    }
  }
});

正如我们在这段代码中看到的,我们可以在 JS 三元运算符中更新多个变量的值。我们还使用setTimeout()函数调用来“模拟”对服务器的调用;这是不必要的,但用作更复杂的异步操作的示例。

更新后的 HTML 代码将如下所示:

<div id="app" class="p-3">
  <h1>Fruit to eat: {{ count }}</h1>
  <p>Eaten: {{ apples }} apples, {{ pears }} pears</p>
  <button v-on:click="eatApples(1)" class="btn btn-success">
    An apple!
  </button>
  <button v-on:click="eatPears(1)" class="btn btn-warning">
    A pear!
  </button>
  <button v-on:click="counterReset()" class="btn btn-danger">
    Reset the counter
  </button>
</div>

更新后的示例应用程序可以在这里找到:codepen.io/AjdinImsirovic/pen/EdNaaO

总结

在本章中,我们了解了 Vuex,这是一个强大的 Vue 插件,可以帮助我们从集中式全局存储管理状态。我们了解了什么是状态,以及为什么需要在更复杂的应用程序中集中数据存储。我们讨论了单向数据流及其在 Vuex 中的实现,通过使用 getter、store mutation 和 store action。我们从理论转向实践,首先构建了一个简单的应用程序,然后学习如何借助 Vue Devtools 扩展来简化我们的开发过程。

在下一节中,我们将使用 Vue-router 进行路由处理,并学习使用 Nuxt 进行服务器端渲染。

第八章:使用 Nuxt.js 和 Vue-Router

随着单页应用SPA)的兴起,出现了一些特定的问题。针对这些问题已经有了各种尝试,并且一些常见的解决方案也从这些尝试中出现。在本节中,我们将看看围绕 SPA 的问题以及在 Vue 中解决这些问题的方法。

在这一章中,我们将使用 Nuxt.js 和 Vue-Router 来理解一些概念:

  • 单页应用

  • 初始页面加载

  • 服务器端渲染和通用 Web 应用

  • 安装 Nuxt.js

  • Nuxt 页面作为路由

  • 使用nuxt-link标签链接页面

我们将首先了解 SPA 是什么以及它们是如何工作的。

单页应用和服务器端渲染

传统上,Web 服务器只提供静态内容。当用户在应用中请求一个链接时,通常服务器会处理该请求并将处理结果作为整个页面发送给客户端,包括 HTML、CSS 和由浏览器提供的 JS。这发生在请求 Web 应用中的每个路由时。如果开发人员想要查看浏览器发送的内容,只需在所选浏览器中运行view source命令即可。

查看源代码的快捷键在一些浏览器中传统上是Ctrl + U,比如 Chrome 和 Firefox。

随着网络体验向桌面端靠拢的推动,近年来我们看到了单页应用(SPA)的兴起。流行的 SPA 示例包括 Gmail、Twitter 和 Google Maps。

单页应用的工作方式是这样的:当用户在网站上浏览不同的页面(路由)时,浏览器不会下载一个全新的页面,也不会向服务器发送一个全新的请求。与其每次用户访问一个路由时从服务器下载完整页面不同,SPA 在客户端渲染所有内容。向服务器的请求仅用于获取新数据。

判断一个 Web 应用是否可以被视为 SPA 的一个好方法是:在应用中访问不同的路由是否会导致整个应用刷新?如果不会,那么它就是一个 SPA。

SPA 从服务器请求新数据,而传统 Web 应用从服务器下载整个页面。

这通常意味着所有的 SPA 代码将在一个页面加载中下载——初始页面加载。这包括 HTML、CSS 和 JS——所有的代码,没有这些代码,SPA 将无法运行。这种方法的缺点是,在较慢的网络上运行或者由于应用程序的体积过大时,下载时间可能会相当长,特别是考虑到许多这些 SPA 都充斥着 JavaScript 代码。

然而,如前所述,SPA 的目标是提供出色的用户体验,表现得像桌面应用程序一样,具有即时执行和无延迟。

解决这个问题的一个方法是引入服务器端渲染。服务器端渲染简单地说就是前端框架在服务器上准备 HTML、CSS 和 JS 的能力,因此当用户访问我们的 SPA 时,他们的浏览器不需要一次性下载整个应用程序,而只需下载部分代码——完整 SPA 的一部分——尽管如此,用户仍然可以与页面进行交互。通过代码分割和重新注水等概念,SPA 无缝地只下载应用程序的那部分内容,以便开始使用它,然后再下载 SPA 的其余部分,而用户已经在与之交互。这种方法减少了初始加载的延迟。

过去 SPA 的另一个主要问题是搜索引擎爬虫无法读取的问题。由于这些爬虫在爬取 SPA 网站时无法运行 JavaScript,访问的搜索引擎爬虫将无法看到 SPA 的内容。因此,服务器端渲染是加快 web 应用程序速度并使其更易于被搜索引擎爬虫索引的一种优雅方法。

当一个 web 应用程序可以在服务器和客户端上渲染网页时,它被称为通用 web 应用程序。通用 web 应用程序基本上是具有 SSR 能力的 SPA。

许多现代前端框架都有自己的 SSR 实现。在 Vue 中,这就是我们所说的 Nuxt.js。

安装 Nuxt.js 并预览默认项目

为了提供不同的路由,Nuxt.js 在后台使用 Vue-router。为了保持简单,我们将专注于使用 Nuxt.js。

有几种方法可以开始使用 Nuxt.js。一种选择是通过vue init命令。另一种是使用一种常见的实践,即create-nuxt-app命令,类似于create-elm-appcreate-react-app

使用 vue init 命令安装 Nuxt.js

让我们首先找到一个位置来保存我们的新 Nuxt 应用程序,然后使用vue init命令来创建它:

vue init nuxt-community/stater-template chapter8

在未安装vue init的情况下运行此命令可能会在控制台中返回以下消息:

Command vue init requires a global addon to be installed. 
Please run yarn global add @vue/cli-init and try again.

因此,要纠正问题,只需运行这个命令:

yarn global add @vue/cli-init

这将需要一些时间,但最终我们将能够运行**vue init**命令:

vue init nuxt-community/starter-template chapter8

这次运行前面的命令将导致我们需要回答一些问题,以便根据我们的喜好配置项目。与我们在 Vue-cli 中看到的类似,要接受默认值,我们可以简单地按下Enter键。

这是控制台的输出,包括所有问题和答案:

? Project name (chapter8)
? Project name chapter8
? Project description (Nuxt.js project)
? Project description Nuxt.js project
? Author (AuthorName <author@email.com>)
? Author AuthorName <author@email.com>)
   vue-cli Generated "chapter 8"

   To get started:
     cd chapter8
     npm install # Or yarn
     npm run dev

让我们按照描述运行这些命令。我们将cd进入chapter8文件夹,然后运行npm install。这将产生一个输出,其中包括 Nuxt 标志的一些漂亮的 ASCII 艺术,贡献者和支持者的列表,以及其他项目信息。现在,我们可以运行npm run dev命令,这将产生以下输出:

[11:12:14] Building project
[11:12:14] Builder initialized
...
[11:12:33] Listening on http://localhost:3000

如果我们访问localhost:3000页面,我们将看到标准的欢迎屏幕,其中有 Nuxt.js 标志,在下面将是我们项目的名称(chapter8),以及两个按钮:链接到文档和项目的 GitHub 存储库。

调试 eslint 错误

在撰写本书时,尽管所有软件都是最新的,eslint仍然会抛出错误。如果在运行npm run dev之后,您打开localhost:3000,您可能会在页面的左上角看到以下错误,指出 eslint 模块未定义。

如果发生这种情况,您可以通过打开代码编辑器中的nuxt.config.js文件,并将第 23 行后的所有代码替换为以下内容来修复它:

  build: {
    /*
    ** Run ESLint on save
    */
    /*
    extend (config, { isDev, isClient }) {
      if (isDev && isClient) {
        config.module.rules.push({
          enforce: 'pre',
          test: /\.(js|vue)$/,
          loader: 'eslint-loader',
          exclude: /(node_modules)/
        })
      }
    }
    */
    extend(config) {
      if (process.server && process.browser) {
        config.module.rules.push({
          enforce: 'pre',
          test: /\.(js|vue)$/,
          loader: 'eslint-loader',
          exclude: /(node_modules)/
        })
      }
    } 
  }
}

在上面的代码中,我们已经注释掉了有问题的代码,并用正确的代码替换,以便比较差异并理解需要修复的内容。

现在我们可以重新运行npm run dev命令,我们应该在localhost:3000上看到应用程序没有任何错误。

使用 create-nuxt-app 安装

或者,我们可以使用create-nuxt-app命令。首先,我们需要全局安装它,以便我们可以在计算机的任何地方使用它:

npm install -g create-nuxt-app

这个命令是全局安装的,可能需要一些时间。成功安装将在控制台中记录几行,即create-nuxt-app已安装在本地驱动器的位置,以及其他一些信息,类似于这样:

+ create-nuxt-app@2.1.1
added 401 packages in 20.234s

接下来,让我们将控制台指向所需的文件夹,然后运行这个命令:

create-nuxt-app chapter8b

与第一种安装方法类似,这种方法也会产生一些带有预设答案的问题,我们可以通过简单地按下Enter键来接受。这是带有默认答案的问题列表:

$ create-nuxt-app chapter8b
> Generating Nuxt.js project in C:\Users\PC\Desktop\chapter8b
? Project name (chapter8b)
? Project name chapter8b
? Project description (My smashing Nuxt.js project)
? Project description My smashing Nuxt.js project
? Use a custom server framework (Use arrow keys)
? Use a custom server framework none
? Use a custom UI framework (Use arrow keys)
? Use a custom UI framework none
? Choose rendering mode (Use arrow keys)
? Choose rendering mode Universal
? Use axios module (Use arrow keys)
? Use axios module no
? Use eslint (Use arrow keys)
? Use eslint no
? Use prettier (Use arrow keys)
? Use prettier no
? Author name (AuthorName)
? Author name AuthorName
? Choose a package manager (Use arrow keys)
? Choose a package manager npm
Initialized empty Git repository in C:/Users/PC/Desktop/chapter8b/.git/

与先前的安装类似,我们可以看到运行样板项目的说明如下:

  To get started:

    cd chapter8b
    npm run dev

  To build & start for production:

    cd chapter8b
    npm run build
    npm start

所以,让我们运行cd chapter8b,然后跟着运行npm run dev。输出将几乎与先前的安装方法相同。

编辑 index.vue 文件

让我们也编辑我们的index.vue文件,在pages文件夹中。这是我们应用程序的根路由。我们将进行的更改很小:我们将删除<div class="links">标签内的所有代码。更新后,该代码片段应如下所示:

      <div class="links">
        <p>Vue Quickstart is a simple introduction to Vue</p>
      </div>

由于后台的 webpack 正在刷新我们的页面,所以在保存更改后,我们应该在浏览器中看到这个更改的结果:

到目前为止,我们已经看到了两种不同方式初始化新的 Vue Nuxt 项目。在下一节中,我们将看看 Nuxt 风格的实现约定优于配置方法:页面作为路由。

Nuxt 页面作为路由

约定优于配置方法是由 Ruby on Rails 推广的。这是一种对 Web 开发的看法,以一种设置和忘记的方式在框架中设置一些东西。当我们说它是有看法的,它只是意味着在处理问题时,框架的开发人员选择了一种特定的方法,而这种方法是做某事的唯一方式。

我们可以说 Nuxt.js 是有看法的,因为它遵循页面作为路由的约定。因此,我们不必在应用程序中手动设置路由,而是遵循简单的约定。在页面文件夹中,index.vue文件充当根路由:/。这意味着如果我们运行我们的应用程序,访问localhost:3000的根路由等同于访问localhost:3000/index.vue

同样地,如果我们创建一个名为about.vue的文件并将其放在页面文件夹中,要查看此文件,我们需要访问localhost:3000/about路由。

所以,让我们就这样做。在我们的页面文件夹中,我们将创建一个新文件并将其命名为contact.vue。在该文件中,我们将添加以下代码:

<template>
  <h1>Contact</h1>
</template>

这就是使/contact路由可用所需的所有内容,您可以通过导航到localhost:3000/contact来查看。我们甚至可以将此文件设置为contact文件夹的默认根路由。在这种情况下,我们必须在pages文件夹内创建一个子文件夹,并将其命名为contact。现在,我们可以在新创建的contact文件夹内创建一个index.vue文件,路由将保持不变。只是我们在pages文件夹内的文件和文件夹结构略有改变,但最终结果是相同的。

然而,将文件分离成子文件夹将使您在添加更多文件时更容易保持组织。

通过components文件夹向 Nuxt 应用程序添加导航

在我们应用程序开发的这一点上,将导航放在适当的位置将是很好的。导航本身不是一个页面;它是一个应该存在于我们应用程序的每个页面中的组件。因此,让我们通过打开components文件夹并添加一个新文件来创建它,我们将其称为Navigation.vue。让我们向其中添加这段代码:

<template>
  <div class="navigation">
    <ul>
        <li><nuxt-link to="/">Home</nuxt-link></li>
        <li><nuxt-link to="/contact">Contact</nuxt-link></li>
        <li><nuxt-link to="/news">News</nuxt-link></li>
    </ul>
  </div>
</template>

<style scoped>
.navigation {
    width: 100%;
    margin: 0;
    padding: 20px;
    background: orange;
    color: #444;
    font-family: Arial, sans-serif;
    font-size: 20px;
}
ul {
    list-style: none;
}
ul li {
    display: inline-block;
}
</style>

请注意<nuxt-link>标签。它只是 Vue-router 实现的一个包装器,to="..."属性的值是我们指定的实际 URL,它只是pages文件夹内特定文件的名称。

接下来,让我们找到layouts文件夹,在其中的default.vue文件中,让我们在模板中添加Navigation组件,使其看起来像这样:

<template>
 <div>
 <Navigation></Navigation>
 <nuxt />
 </div>
</template>

请注意,我们可以自我关闭组件,所以我们可以写简写版本,而不是<Navigation></Navigation>,简写版本就是<Navigation />

我们需要确保通过在template标签下方添加script标签来导入Navigation组件:

<script>
import Navigation from '@/components/Navigation'
export default {
 components: {
 Navigation
 }
}
</script>

在这一点上,我们的主页,通过导航更新,将如下所示:

现在我们的导航已经就位,我们将添加另一个页面,我们将其称为News.vue,代码如下:

<template>
  <h1>News</h1>
</template>

在这一点上,我们的导航中有三个链接,现在我们可以专注于向每个页面添加更多内容。

向我们的 Nuxt 应用程序页面添加内容

让我们更新News.vue组件:

<template>
  <section class="news">
    <h1>News</h1>
    <hr>
    <article>
        <h2>We are taking orders for our new product</h2>
        <div>
            Lorem ipsum dolor sit amet, consectetur adipisicing elit. Laudantium perspiciatis dolorem blanditiis maxime doloremque quibusdam obcaecati autem enim ipsum deserunt. Aliquid dolor consequatur repellendus odit, dolores possimus ab cum et voluptatem placeat sunt perferendis porro, eligendi perspiciatis harum pariatur veniam quo sed, reprehenderit voluptates maiores hic! Sint, facilis voluptatibus animi!
        </div>
    </article>
    <article>
        <h2>Our website is live</h2>
        <div>
            Lorem ipsum dolor sit amet, consectetur adipisicing elit. Delectus unde fugit quod, tempore enim obcaecati quam eius explicabo voluptates quo consequatur! Ad iste consequuntur dolorem minima at cupiditate veniam saepe voluptatum, qui hic corporis modi repellendus illum natus optio aut! Omnis praesentium placeat pariatur neque dolorum eaque, labore at et dignissimos impedit nobis, commodi rerum. Debitis est exercitationem ipsa, commodi nihil! Inventore minus ex, quam, facilis ut fuga unde harum possimus dolore ea voluptatum non debitis nihil ipsum repellendus aut dolorum nam nostrum assumenda eveniet corrupti consequatur obcaecati provident alias! Ad est minus repudiandae aliquid maxime provident labore. Asperiores, qui!
        </div>
    </article>
  </section>
</template>

<script>

</script>

<style scoped>
    .news {
        max-width: 500px;
        margin: 0 auto;
        padding-top: 30px;
        font-size: 20px;
    }
    .news article div {
        line-height: 30px;
    }
    h1, h2 {
        padding-top: 20px;
        padding-bottom: 20px;
    }
</style>

现在新闻链接将如下所示:

接下来,让我们更新Contact.vue组件:

<template>
  <section class="contact">
    <h1>Contact</h1>
    <hr>
    <article>
        <h2>Feel free to get in touch!</h2>
        <div>
            <p>Our managers:</p>
            <ul>
                <li>John Doe, +01 123 4567</li>
                <li>Jane Doe, +01 124 4567</li>
                <li>Another Person, +01 125 4567</li>
            </ul>
        </div>
    </article>
  </section>
</template>

<script>

</script>

<style scoped>
    .contact {
        max-width: 500px;
        margin: 0 auto;
        padding-top: 30px;
        font-size: 20px;
    }
    .contact article div {
        line-height: 30px;
    }
    h1, h2 {
        padding-top: 20px;
        padding-bottom: 20px;
    }
</style>

我们不会改变 Nuxt.js 项目的原始主页。有限的更改原因是我们只需要有一些带有虚拟内容的页面,这样我们就可以继续到下一节,在那里我们将看到如何将页面过渡效果添加到我们的 Nuxt.js 应用程序中。

向我们的 Nuxt.js 应用程序添加页面过渡效果

正如我们在第六章中学到的,Vue 提供了许多方法来为我们的应用程序添加交互性、过渡效果和动画。为了加快这个过程,我们将使用Animate.css中的动画,稍作修改。

在 Nuxt 中,我们可以像我们已经学过的那样使用页面过渡钩子。我们只需将.v-*过渡钩子中的v字母替换为.page-*。所有的功能以及一切的工作方式都将保持不变。

让我们从打开pages/index.vue文件并在其style标签的顶部添加以下代码开始:

.page-enter-active, .page-leave-active {
  transition: opacity 1s;
}
.page-enter, .page-leave-active {
  opacity: 0;
}

接下来,我们将打开contact.vue文件,并在其style标签的顶部添加以下代码:

.page-enter-active {
    animation: zoomIn .5s;
} 
@keyframes zoomIn {
from {
    opacity: 0;
    transform: scale3d(0.4, 0.4, 0.4);
}

50% {
    opacity: 1;
}
}

.zoomIn {
animation-name: zoomIn;
}

最后,我们将使用以下代码更新news.vuestyle标签顶部:

.page-enter-active {
    animation: bounce .5s;
} 
.page-leave-active {
    animation: bounce .5s;
} 
@keyframes bounce {
    from,
    20%,
    55%,
    85%,
    to {
        animation-timing-function: cubic-bezier(0.320, 0.70, 0.355, 1);
        transform: translate3d(0, 0, 0);
    }

    40%,
    43% {
        animation-timing-function: cubic-bezier(0.700, 0.05, 0.855, 
         0.06);
        transform: translate3d(0, -30px, 0);
    }

    70% {
        animation-timing-function: cubic-bezier(0.700, 0.05, 0.855, 
        0.06);
        transform: translate3d(0, -15px, 0);
    }

    90% {
        transform: translate3d(0, -4px, 0);
    }
}

在这一点上,随时测试您的应用程序,并看看您如何通过对路由文件中的style标签进行少量更改来实现显著的视觉改进。

在本章中,我们了解了构建基本 Nuxt.js 应用程序的基础知识。有许多方法可以改进和扩展这一点。要继续构建更好的应用程序并了解更多关于在 Node 上运行 Vue 应用程序的知识,请随时参考 Packt 图书馆中的其他标题,比如使用 Vue.js 和 Node 进行全栈 Web 开发

总结

在本章中,我们学习了关于单页面应用程序的知识,以及导致它们出现的想法,以及它们的实施带来的挑战,比如初始页面加载的问题。我们还学习了与 SPA 相关问题的解决方案,比如服务器端渲染,以及 Nuxt.js 如何帮助我们构建通用的 Web 应用程序。我们学习了如何安装 Nuxt.js 并设置 Nuxt.js 页面作为路由。我们使用nuxt-link标签链接了我们的 Vue 应用程序的路由,并为每个页面添加了一些内容。最后,为了建立在前几章学到的知识基础上,我们为更流畅的用户体验添加了一些页面过渡效果。

这就是Vue JS 快速入门的结尾。我们已经介绍了一整套基本的 Vue JS 概念。简要概述一下,我们可以重申一些我们所涵盖的内容:mustache 模板、指令、修饰符、方法、计算属性、观察者、组件(全局和本地)、props、生命周期钩子、vue-cli、插槽、父子组件通信、过滤器、混合、自定义指令和插件、过渡、动画、过渡组件、集成第三方动画、绑定样式、处理过渡组和 JavaScript 动画钩子、SPA、状态和存储的概念、单向数据流、使用 Vuex、处理初始页面加载、Nuxt、SSR 和通用 Web 应用程序。

在这本简短的书中,我们涵盖了很多内容。为了看到 Vue JS 的所有组成部分的全貌,我们必须保持基本。接下来该去哪里呢?

有几种方法可以进一步提高您的 Vue 相关技能。您可以专注于了解如何使用服务器端技术,比如 Node、Laravel 或.NET Core。您也可以使用 VuePress——一种使用 Vue 构建静态 JS 站点的方式。或者您可能想查看Vuex 快速入门指南

为了更容易地继续提高您的 Vue.js 技能,Packt 图书馆中有超过两打本书可供您选择,其中包括涉及本摘要中列出的主题的书籍。

posted @ 2024-05-16 12:10  绝不原创的飞龙  阅读(4)  评论(0编辑  收藏  举报