Vue2-秘籍-全-

Vue2 秘籍(全)

原文:zh.annas-archive.org/md5/dd7447834c754d87cebc9999e0cff7f3

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

Vue.js 2 是一个简单而强大的框架。它将使你能够快速原型化开发小型应用程序,并且在构建大型前端系统时不会阻碍。本书是一本食谱书,每一段都是一个菜谱;就像普通的食谱书一样,你可以快速跳到你感兴趣的菜谱,或者从头到尾阅读,成为一位优秀的厨师。

所有的菜谱(除了一小部分)都是基于可工作的 Vue 应用程序,所以在学习结束时你永远不会一无所获。当我编写它们时,我尽量给出有意义的例子,并尽可能增添一些乐趣。所有的菜谱在执行相同的任务时都稍有不同,所以即使实施非常相似的菜谱,你也会学到一些新的东西。

这本书花了大约 6 个月的时间来写,即使在这短暂的时间内,我不得不回过头来更新图片和 API 的变化,以及添加新的概念。然而,许多菜谱都融入了可重用性和良好工程的永恒概念,所以我希望这本书的一部分将作为有用的技术留在你心中,并在你的应用程序中进行重用。

最后,虽然我确保了每一章都有足够的图片来说明预期的输出,但我认为对你来说实际键入和尝试这些菜谱是非常重要的。

祝愿你构建出伟大的东西并玩得开心!

本书涵盖内容

第一章,开始,是您创建第一个 Vue 应用程序并熟悉最常见的功能和开发工具的地方。

第二章,基本 Vue.js 特性,是您轻松构建列表和表单,并学习如何样式化它们的地方。

第三章,过渡和动画,是您了解过渡和动画如何为应用程序带来更多活力的地方。您还将集成外部 CSS 库。

第四章,组件!,是您意识到在 Vue 中一切都是组件,并可以利用它来减少重复并重用您的代码的地方。

第五章,与互联网通信,是您进行第一次 AJAX 调用并创建表单和完整的 REST 客户端(以及服务器!)的地方。

第六章,单页应用程序,是您使用 vue-router 创建静态和动态路由以创建现代 SPA 的地方。

第七章,单元测试和端对端测试,是您通过添加 Karma、Chai、Moka、Sinon.JS 和 nightwatch 来学习创建专业软件,以确保可以自信地重构应用程序的地方。

第八章,组织+自动化+部署=Webpack,是您实际将精确制作的组件发布到 npm 并学习 Webpack 和 Vue 如何共同工作的地方。

【第九章】Advanced Vue.js , 探索指令、插件、功能组件和 JSX。

【第十章】Large Application Patterns with Vuex , 使用 Vuex 对应用程序进行结构化,使用经过测试的模式来确保应用程序的可维护性和性能。

【第十一章】Integrating with External Frameworks , 使用 Vue 和 Electron、Firebase、Feathers 和 Horizon 构建四个不同的应用程序。

阅读本书所需的条件

为了能够跟上本书的内容,您需要一台连接互联网的计算机。您可以选择在线使用 Chrome 完成示例。在某个阶段,您至少需要一个文本编辑器;我强烈推荐 Microsoft Visual Studio Code 来完成这项工作。

本书的读者对象

这本书已经在一些连 JavaScript 都不懂的人身上进行了测试。他们通过阅读第一章就能够学会 Vue!在继续深入学习中,您将会遇到更加高级的概念,即使您已经熟悉 Vue 2,您也可能会发现一些之前不知道的技巧或者对您有所帮助的智慧建议。

这本书,如果你从头到尾按照步骤进行,将能够使你成为一名熟练的 Vue 开发者。另一方面,如果你已经是一名熟练的开发者,它也提供了许多不同功能和技术的良好参考,适用于一些偶尔需要的情况。最后,如果你已经尝试过 Vue 1,并且对变化感到不知所措,这本书也是一个有效的迁移指南。

章节

在本书中,您会经常看到几个标题(准备工作、操作步骤、工作原理、更多信息、另请参阅)。

为了清楚地说明如何完成一个示例,我们将这些章节分以下几个部分:

准备工作

本节告诉您在示例中可以预期的情况,并描述了设置示例所需的任何软件或预备设置的方法。

操作步骤

本节包含了跟随示例所需的步骤。

工作原理

本节通常会对上一节中发生的情况进行详细解释。

更多信息

本节包含有关示例的其他信息,以便读者对示例有更多了解。

另请参阅

本节提供了有关示例的其他有用信息的链接。

约定

在本书中,您会找到一些用来区分不同类型信息的文本样式。以下是一些样式的示例及其含义的解释。

以下是文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 账户的展示方式:“我将要更新EngineTest项目中已存在的ChasePlayerComponent类。”

代码块设置如下:

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

命令行输入或输出以以下方式显示:

新术语重要单词以粗体显示。您在屏幕上看到的词,例如菜单或对话框中的词,以此文本形式出现:“打开 Webstorm 并创建一个新的空项目”

警告或重要提示以此方式显示。提示和技巧以此方式显示。

第一章:使用 Vue.js 入门

本章将介绍以下内容:

  • 使用 Vue.js 编写 Hello World

  • 编写列表

  • 创建一个动态和动画列表

  • 响应事件,如点击和按键

  • 选择开发环境

  • 使用过滤器格式化文本

  • 使用 Mustaches 调试应用程序(例如 JSON 过滤器)

  • 使用 Vue 开发者工具分析应用程序

  • 升级到 Vue.js 2

简介

Vue 是一个非常强大的框架,但其优势之一是它非常轻量级且容易上手。事实上,在第一个示例中,您将在几分钟内构建一个简单但功能齐全的程序,无需任何设置即可完成。

在本章中,您将学习如何创建重复元素的网页列表(如目录)。此外,您将构建一个带有事件监听器的交互式页面。

为了让您更好地选择开发环境,我们还介绍了一些开发环境。您将使用一些调试技巧来快速开发自己的代码,并更好地理解如何解决应用程序中的错误。

请注意,在撰写本文时,ES5 是浏览器中 JavaScript 最好支持的标准。在这一章中,我将使用 ES5,这样即使您的浏览器不支持更新的 ES6,您也可以跟着学习。请记住,在后续章节中将使用 ES6。目前,Chrome 与大多数 ES6 的重要构造兼容,但通常您应该使用Babel使您的应用程序兼容旧版浏览器。当您准备好使用 Babel 时,请参考第八章中的配方如何使用 Babel 编译 ES6,以及组织 + 自动化 + 部署 = Webpack

用 Vue.js 编写 Hello World

让我们使用 Vue.js 创建最简单的程序,即必备的 Hello World 程序。我们的目标是让您熟悉 Vue 如何操作您的网页以及数据绑定是如何工作的。

准备工作

完成这个入门示例,我们只需要使用浏览器。也就是说,我们将使用 JSFiddle 来编写代码:

如果您从未使用过 JSFiddle,请不要担心;您即将成为一名专业的前端开发人员,使用 JSFiddle 将成为您口袋中的有用工具:

  1. 将您的浏览器导航到jsfiddle.net

您将看到一个空白页面分为四个象限。左下方是我们将编写 JavaScript 代码的地方。按顺时针方向,我们有一个 HTML 部分,一个 CSS 部分,最后是我们预览的结果页面。

开始之前,我们应该告诉 JSFiddle 我们想要使用 Vue 库。

  1. 在 JavaScript 象限的右上角,点击齿轮图标并从列表中选择 Vue 2.2.1(你会找到多个版本,“edge”代表最新版本,在撰写时对应的是 Vue 2)。

现在我们准备好编写我们的第一个 Vue 程序了。

具体步骤如下:

  1. 在 JavaScript 部分写入:
        new Vue({el:'#app'})

  1. 在 HTML 象限中,创建<div>
        <div id="app">

          {{'Hello ' + 'world'}}

        </div>

  1. 点击左上角的运行按钮,我们可以看到页面显示 Hello world:

工作原理如下:

new Vue({el:'#app'})将实例化一个新的 Vue 实例。它接受一个选项对象作为参数。这个对象在 Vue 中是核心的,它定义和控制数据和行为。它包含了创建 Vue 实例和组件所需的所有信息。在我们的例子中,我们只指定了el选项,它接受一个选择器或一个元素作为参数。#app参数是一个选择器,将返回页面中以app作为标识符的元素。例如,在这样的页面中:

<!DOCTYPE html> 

<html> 

  <body> 

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

  </body> 

</html>

我们在具有 ID 为app<div>中编写的所有内容都将在 Vue 的范围之内。

现在,JSFiddle 会将我们在 HTML 象限中编写的所有内容包装在 body 标签中。这意味着,如果我们只需要在 HTML 象限中写入<div>,JSFiddle 会负责将其包装在 body 标签中。

还有一点要注意,将#app放置在bodyhtml标签上会抛出错误,因为 Vue 建议我们将应用挂载在普通元素上,选择body也是同样的情况。

花括号(或者叫 handlebars)是告诉 Vue 将其内部的所有内容解析为代码的一种方法。引号是 JavaScript 中声明字面字符串的一种正常方法,所以 Vue 只会返回helloworld的字符串拼接。没有什么花哨的东西,我们只是将两个字符串拼接在一起并显示结果。

更多内容

我们可以利用这一点做一些更有趣的事情。如果我们是外星人,想要同时问候多个世界,我们可以这样写:

We conquered 5 planets.<br/> 

{{'Hello ' + 5 + ' worlds'}}

我们可能会追踪不住我们征服了多少个世界。没问题,我们可以在花括号内进行数学运算。另外,让我们将Helloworlds放在花括号之外:

We conquered {{5 + 2}} planets.<br/> 

Hello {{5 + 2}} worlds

在花括号内使用原始数字表示世界的数量会显得很混乱。我们将使用数据绑定将其放在实例中的一个命名变量中:

<div id="app"> 

  We conquered {{countWorlds}} planets.<br/> 

  Hello {{countWorlds}} worlds 

</div>

new Vue({ 

  el:'#app', 

  data: { 

    countWorlds: 5 + 2 

  } 

})

这是整洁应用程序的实现方式。现在,每次我们征服一个星球,我们只需要编辑countWorlds变量。反过来,每次我们修改这个变量,HTML 将自动更新。

恭喜,您已经完成了进入 Vue 世界的第一步,现在可以使用响应式数据绑定和字符串插值构建简单的交互式应用程序。

编写列表

生产列表的欲望似乎是人类天性中固有的一部分。通过观察一个井然有序的列表在计算机屏幕上滚动,人们可以获得一种深深满足的感觉。

借助 Vue,我们可以使用出色的外观和极大的便利性制作各种类型的列表。

准备工作

在本篇文章中,我们将使用基本的数据绑定,如果您遵循了最初的教程,您已经很熟悉它了。

具体操作如下...

我们将以几种不同的方式构建列表:使用一系列数字、使用数组以及使用对象。

一系列数字

要开始创建列表,请像前面的教程中一样设置您的 JSFiddle,并添加 Vue.js 作为框架。选择 Vue 2.2.1(或 Vue(edge)):

  1. 在 JavaScript 部分编写如下内容:
        new Vue({el:'#app'})

  1. 在 HTML 中编写如下内容:
        <div id="app"> 

          <ul> 

            <li v-for="n in 4">Hello!</li> 

          </ul> 

        </div>

这将导致一个列表,其中*Hello!*重复出现四次。几秒钟后,您的第一个列表就完成了,做得好!

我们可以使用这种技术编写一个倒计时 - 在 HTML 中,将

标签的内容替换为以下内容:

<div id="app"> 

  <ul> 

    <li v-for="n in 10">{{11-n}}</li> 

    <li>launch missile!</li> 

  </ul> 

</div>

数组

  1. 在 HTML 中,为了得到相同的结果,编辑列表以反映以下内容:
        <ul> 

            <li v-for="n in [10,9,8,7,6,5,4,3,2,1]">{{n}}</li> 

            <li>launch missile!</li> 

        </ul>

尽管这个列表与上一个列表相同,但我们不应该在 HTML 标记中放置字面数组。

  1. 最好使用一个包含数组的变量。将前面的代码修改为以下内容:
        <ul> 

          <li v-for="n in countdown">{{n}}</li> 

          <li>launch missile!</li> 

        </ul>

  1. 然后在 JavaScript 中放置数组倒计时:
        new Vue({ 

          el:'#app', 

          data: { 

            countdown: [10,9,8,7,6,5,4,3,2,1] 

          } 

        })

使用索引表示的数组

当枚举一个数组时,我们还可以访问索引,由变量i在下面的代码中代表:

  1. HTML 如下:
        <div id="app"> 

          <ul> 

            <li v-for="(animal, i) in animals">

              The {{animal}} goes {{sounds[i]}}

            </li> 

          </ul> 

        </div>

  1. 在代码部分中,写:
        new Vue({ 

          el: '#app', 

          data: { 

            animals: ['dog', 'cat', 'bird'], 

            sounds: ['woof', 'meow', 'tweet'] 

          } 

        })

对象

前面的例子可以进行重构,以匹配动物的名称和声音,这样索引的意外错位就不会影响我们的列表。

  1. HTML 如下:
        <div id="app"> 

          <ul> 

            <li v-for="(sound, name) in animals"> 

              The {{name}} goes {{sound}} 

            </li> 

          </ul> 

        </div>

  1. 我们需要在 JavaScript 中创建animals对象:
        new Vue({ 

          el: '#app', 

          data: { 

            animals: { 

              dog: 'woof', cat: 'meow', bird: 'tweet' 

            } 

          } 

        })

工作原理...

列表的工作原理非常简单; 这里对语法进行了更多解释。

数字范围

变量n<li>标签内是可见的。为了证明这一点,你可以快速构建一个倒计时列表,如下所示:

<ul> 

  <li v-for="n in 10">{{11 - n}}</li> 

  <li>launch missile!</li> 

</ul>

我们写11而不是10,因为在 Vue 中枚举是从 1 开始计数的;这意味着10中的n将从1开始计数,而不是从0开始计数,而有些人可能会期望从0开始,并一直增加到10。如果我们希望倒计时从10开始,那么我们必须写11。最后一个数将是10,所以在导弹发射前,我们将会有1作为最后一个数字。

v-for="n in 10"的作用是调用枚举; 具体来说,我们正在枚举一个数字范围(从 1 到 10)。

数组

Vue 也允许我们枚举数组。一般的语法如下:

v-for="(element, index) in array"

如上所示,如果我们只想要数组元素,索引和括号可以省略。

这种枚举形式是有序的。换句话说,数组中元素的有序序列将与屏幕上看到的相同;而当枚举对象时则不是这样。

对象

语法是v-for =“(value,property)”,如果你想的话也可以加上索引v-for =“(value,property,index)”。后者不推荐使用,因为如前所述,枚举属性的顺序是不固定的。实际上,在大多数浏览器中,顺序与插入顺序相同,但不保证一定如此。

创建一个动态和动画列表

在 Vue 中,大部分数据都是响应式的。实际上,这意味着如果我们的视图模型中有变化,我们将立即看到结果。这就是让您专注于应用本身,抛开所有绘图逻辑的原因。在本篇中,我们还将了解此系统的一些限制。

准备工作

要完成这个教程,你应该知道如何使用基本的数据绑定(在第一个教程中介绍)以及如何创建列表(第二个教程)。

操作步骤

在之前的教程中,我们为导弹发射倒计时构建了一个列表:

<div id="app"> 

  <ul> 

    <li v-for="n in countdown">{{n}}</li> 

    <li>launch missile!</li> 

  </ul> 

</div>

new Vue({

  el:'#app',

  data: {

    countdown: 

      [10,9,8,7,6,5,4,3,2,1]

  }

})

如果它能被动画化就好了!我们可以调整 JavaScript 代码,以使倒计时在秒数增加时添加数字:

  1. 将上述代码复制到 JSFiddle 的 HTML 和 JavaScript 区域,除了我们将自己填充倒计时,所以将其设置为空数组。

要获取倒计时变量,我们必须通过 Vue 实例本身传递该变量。

  1. 将 Vue 实例分配给一个变量以供以后参考:
        var vm = new Vue({

          el:'#app',

          data: {

            countdown: []

          }

        })

这样我们就可以使用vm来访问 Vue 实例。

  1. 从 10 开始初始化倒计时:
        var counter = 10

  1. 设置一个函数,该函数重复将剩余秒数添加到现在为空的countdown数组中:
        setInterval(function () { 

          if (counter > 0) { 

            vm.countdown.push(counter--) 

          } 

        }, 1000)

它是如何工作的...

我们要做的是获取countdown数组的引用,并借助于setInterval将其填充为递减的数字。

我们通过在vm.countdown.push(counter--)行中设置的vm变量来访问countdown,因此每次向数组添加新数字时,我们的列表都将更新。

这段代码非常简单,只需注意我们必须使用push函数将元素添加到数组中。使用方括号表示法添加元素将无效:

vm.countdown[counter] = counter-- // this won't work

数组将被更新,但是由于 JavaScript 的实现方式,这种赋值方式将跳过 Vue 的响应式系统。

还有更多内容

现在运行代码将一次添加一个倒计时数字;很好,但是最后一个元素发射导弹呢?我们希望它只在最后出现。

为了做到这一点,在 HTML 中我们可以直接进行一个小的技巧:

<ul> 

  <li v-for="n in countdown">{{n}}</li> 

  <li>{{ countdown.length === 10 ? 'launch missile!' : '...' }}</li> 

</ul>

这个解决方案不是我们所能做到的最好的;在v-show的示例中了解更多内容。

我们刚刚了解到,如果我们希望在视图中更新,不能使用方括号表示法向响应式数组中添加元素。对于使用方括号修改元素和手动更改数组长度也是如此:

vm.reactiveArray[index] = 'updated value' // won't affect the view 

vm.reactiveArray.length = 0 // nothing happens apparently

您可以使用 splice 方法克服这个限制:

vm.reactiveArray.splice(index, 1, 'updated value') 

vm.reactiveArray.splice(0)

对于点击和按键等事件的响应

每个应用程序的一个基本部分是与用户的交互。Vue 提供了简化的方式来拦截大多数用户事件,并将它们与相关操作连接起来。

准备工作

要成功完成这个示例,您应该知道如何创建一个列表。如果不知道,请查看第二章的使用计算属性过滤列表这个示例,以及Vue.js 基本特性

如何操作...

以下代码片段显示了如何对click事件作出反应:

  1. 填写以下 HTML:
        <div id="app"> 

          <button v-on:click="toast">Toast bread</button> 

        </div>

  1. 至于 JavaScript,写下以下内容:
        new Vue({el:'#app', methods:{toast(){alert('Tosted!')}}})

  1. 执行代码!一个事件监听器将会安装在按钮上。

  2. 点击按钮,您会看到一个弹出窗口,上面写着Toasted!

它是如何工作的...

运行上述代码将在按钮上安装一个事件处理程序。语法是v-on:DOMevent="methodEventHandler"。处理程序必须是一个方法,即在 methods 选项中的一个函数。在上面的示例中,toast就是处理程序。

双向数据绑定

在大多数情况下,v-on 属性可以满足您的需求,特别是当事件来自元素时。另一方面,对于某些任务来说,它可能有时过于冗长。

例如,如果我们有一个文本框,并且我们想要使用文本框的内容更新一个变量,并确保文本框始终具有变量的更新值(这称为双向数据绑定),我们必须编写几个处理程序。

然而,这个操作是由v-model属性完成的,如下面的代码所示:

<div id="app"> 

  <button v-on:click="toast">Toast bread</button> 

  <input v-model="toastedBreads" /> 

  Quantity to put in the oven: {{toastedBreads}} 

</div>

new Vue({ 

  el: '#app', 

  methods: { 

    toast () { 

      this.toastedBreads++ 

    } 

  }, 

  data: { 

    toastedBreads: 0 

  } 

})

玩一下这个应用程序,并注意到保持文本框同步不需要处理程序。每次更新toastedBreads时,文本也会更新;反之,每次你写一个数字,数量也会更新。

还有更多

如果你遵循本章的第一个示例,你会记得我们向一个变量打招呼,变量可以包含数量不定的单词;我们可以使体验更加互动。让我们建立一个我们想要问候的行星列表:

<div id="app"> 

  <ul> 

    <li v-for="world in worlds">{{world}}</li> 

  </ul> 

</div>

new Vue({ 

  el: '#app', 

  data: { 

    worlds: ['Terran', 'L24-D', 'Ares', 'New Kroy', 'Sebek', 'Vestra'] 

  } 

})

我们希望能够追踪新征服的星球并删除我们摧毁的星球。这意味着在列表中添加和删除元素。考虑以下 HTML:

<ul> 

  <li v-for="(world, i) in worlds"> 

    {{world}} 

  <button @click="worlds.splice(i, 1)">Zap!</button> 

  </li> 

</ul> 

<input v-model="newWorld"/> 

<button @click="worlds.push(newWorld)">Conquer</button>

在这里,@符号是v-on的简写:让我们来看看修改的地方:

  • 我们添加了一个按钮来删除行星(我们需要在v-for中写出索引)

  • 我们放置了一个文本框,它绑定到数据变量newWorld

  • 我们放置了一个相应的按钮,将文本框中的内容添加到列表中

运行这段代码将会起作用。但是如果你看一下控制台,你会看到在更新文本字段时会有一个警告。

[Vue warn]: Property or method "newWorld" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option. (found in root instance)

这是因为我们没有在 Vue 实例中声明newWorld,但这很容易修复:

new Vue({ 

  el: '#app', 

  data: { 

    worlds: ['Terran', 'L24-D', 'Ares', 'New Kroy', 'Sebek', 'Vestra'], 

    newWorld: '' 

  } 

})

选择开发环境

我们将探索一些不同的开发方式,从简单的 JSFiddle 方法到更健壮的 WebStorm 支持方法。由于我们想要使用库来为我们的软件添加新功能,所以我将为您提供一个添加库的指南,无论您选择的开发方法如何。

操作步骤如下:

我将从最简单的方法开始,然后为您呈现一些更复杂的用于大型项目的方法。

仅使用浏览器

有一系列的网站(如 JSFiddle、CodePen 和 JS Bin 等)可以让您直接在浏览器中编写 Vue 应用程序,这些网站非常适合测试新功能并尝试本书中的示例。另一方面,它们在代码组织方面的限制太多,无法开发更复杂的项目。在本章的第一个示例中,使用了这种开发方式,请参考该示例以了解如何仅使用浏览器进行开发。一般来说,您应该通过使用这种方式来学习,并将其转化为更结构化的项目,具体取决于您正在开发的内容。

仅使用浏览器添加依赖项

每当我提到一个外部库时,您可以在互联网上搜索相关的.js文件,最好通过 CDN(内容分发网络)来分发,并将其添加到 JSFiddle 的左侧菜单中。让我们尝试一下 moment.js。

  1. 在浏览器中打开一个新的 JSFiddle(将浏览器指向jsfiddle.net/)。

  2. 在另一个标签页中,在你喜欢的搜索引擎中搜索momentjs CDN

  3. 第一个结果应该会带你到一个 CDN 网站,上面有一列链接;你应该最终能找到一些像https://somecdn.com/moment.js/X.X.X/moment.js的链接,其中X代表版本号。

  4. 复制你找到的链接,然后回到 JSFiddle。

  5. 在左侧边栏的“External Resources”部分,粘贴你的链接,然后按下“Enter”键。

对于许多库来说这样就足够了;有些库不支持这种方式,你就需要用其他方式将它们包含在 JSFiddle 中。

文本编辑器

最简单的方式是使用文本编辑器和浏览器。对于简单的、自包含的组件来说这完全合法。

现在有很多文本编辑器可供选择。我喜欢使用的是 Microsoft Visual Studio Code(github.com/Microsoft/vscode)。如果你使用其他编辑器也没什么大不了的,只是恰巧 Code 有一个针对 Vue 的插件:

  1. 创建一个名为myapp.html的新文件,在其中编写如下内容:
        <!DOCTYPE html> 

        <html> 

          <head> 

            <title>Vue.js app</title> 

          </head> 

          <body> 

            <div id="app"> 

              {{'hello world'}} 

            </div> 

            <script 

              src="https://cdnjs.cloudflare.com/ajax

               /libs/vue/2.0.0/vue.js">

            </script> 

            <script> 

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

            </script> 

          </body> 

        </html>

  1. 在浏览器中打开刚刚创建的文件。

Vue 会从cdnjs.com/下载,然后文本hello world应该会显示出来(如果看到了花括号,则可能出现了问题,请检查控制台是否有错误)。

这种方法类似于 JSFiddle 的方法:在顶部有一个 HTML 部分、一个 JavaScript 部分和一个 CSS 部分。我们只是将所有内容都控制在自己手中。此外,这种方式我们还可以使用 Vue 开发者工具(查看配方“使用 Vue 开发者工具扫描你的应用程序”了解介绍)。

用文本编辑器添加依赖项

在此配置中添加外部库只需将另一个<script>标签添加到你的文件中,然后将源属性设置为相应的链接。如果我们想添加moment.js,我们按照之前解释的方式查找该库,然后将以下代码片段添加到我们的页面中:

<script src="https://somecdn.com/moment.js/X.X.X/moment.js "></script>

请注意,你需要将找到的链接粘贴到前面代码片段中虚假链接的位置。

Node 包管理器(npm)

与 Vue 项目一起工作的规范方式,也是 Vue 社区官方支持的方式,涉及使用 npm,尤其是一个名为vue-cli的 npm 包。

如果您对 npm 不太熟悉,将其列入您计划广泛使用 JavaScript 进行开发的事项清单中是一个好主意。

简而言之,npm 是一个用于组织和共享代码的工具,超越了在项目中使用其他人的代码。更正式地说,它是一个用于 JavaScript 的软件包管理器。我们将在本书中使用一些基本命令,以及一些更高级的命令,但是我鼓励您自己学习更多:

  1. 安装 npm。由于它与 Node.js 捆绑在一起,因此最好直接安装 Node.js。您可以在nodejs.org/en/download/上找到安装说明。

  2. 安装完 npm 后,打开命令行并输入npm install -g vue-cli,这将安装vue-cli。选项-g表示全局安装,这意味着无论您身在何处,都可以输入vue来运行该程序。

  3. 创建一个作为工作区的新目录。我们将把所有项目放在这个目录中。

  4. 输入vue list,我们可以从官方 Vue 模板仓库中获取所有可用的模板。其他来源的模板也可以使用。

simple模板将创建一个类似于前面几段所做内容的页面。我邀请您运行vue init simple并检查一下;请注意它与我们所做的内容之间的差异。我们现在要做的是更进一步。我们将使用更复杂的模板,该模板包括一个打包工具。有一个用于webpackbrowserify的模板;我们选择使用第一个。

如果您对webpackbrowserify不太熟悉,它们是用于控制从源代码和资源(图像、CSS 文件等)到定制捆绑包的 JavaScript 程序的构建过程的程序。例如,对于单个.js文件:

  1. 输入vue init webpack-simple,程序将询问您有关项目的一些问题。如果您不知道如何回答,请按下Enter键使用默认选项。

我们也可以选择等效地选择browserify-simple模板;这两个库可以达到相同的结果。

  1. 完成脚手架后,输入npm install。这将负责下载和安装我们编写 Vue 应用所需的所有 npm 软件包。

完成后,您将已经拥有一个具备功能的演示应用程序。

  1. 输入npm run dev来运行你的应用程序。进一步的指导会在屏幕上出现,并告诉你访问一个特定的网址,但你的浏览器很有可能会自动打开。

  2. 将浏览器定位到指定的地址。你应该能够立即看到演示应用程序。

通过vue-cli创建的源文件中,你会发现两个值得注意的文件。第一个文件是你的应用程序的入口点,src/main.js。它将包含类似以下的内容:

import Vue from 'vue' 

import App from './App.vue'

new Vue({ 

 el: '#app', 

 render: h => h(App) 

})

这段代码加载在你刚刚看到的index.html页面中。它只是告诉主 Vue 实例在被#app选择的元素中(在我们的情况下是一个带有id="app"属性的<div>元素)加载和渲染App组件。

你将在App.vue文件中找到一种自包含的方式来编写 Vue 组件。关于组件的更多内容将在其他教程中介绍,但现在请将其视为一种更进一步划分你的应用程序以保持其有序的方法。

以下代码与官方模板中的代码不同,但概括了一般的结构:

<template> 

  <div id="app"> 

    <img src="./assets/logo.png"> 

    <h1>\{{ msg }}</h1> 

  </div> 

</template>

<script> 

export default { 

  data () { 

    return { 

      msg: 'Hello Vue 2.0!' 

    } 

  } 

} 

</script> 

<style> 

body { 

  font-family: Helvetica, sans-serif; 

} 

</style>

你可以看到将代码划分为 HTML、JavaScript 和 CSS 是一种重复出现的模式。在这个文件中,我们可以看到与我们在第一个例子中在 JSFiddle 中看到的类似的东西。

<template>标签中,我们放置我们的 HTML,在<script>标签中放置 JavaScript 代码,并使用<style>标签为我们的应用程序添加一些样式。

运行npm run dev后,你可以尝试在这个文件中编辑msg变量;在保存修改后,网页会自动重新加载组件。

使用 npm 添加依赖项

要在此配置中添加外部库,只需键入npm install后跟库的名称。然后在你的代码中,使用以下类似的方式使用它:

import MyLibrary from 'mylibrary'

我们可以使用以下命令导入moment.js

npm install moment

然后在我们的 JavaScript 中添加以下行:

import moment from 'moment'

IDE

如果你有一个非常庞大的项目,很有可能你已经在使用 IntelliJ 或 Webstorm 等工具。在这种情况下,我建议你在大部分工作中坚持使用嵌入的控制台,并只使用诸如语法高亮和代码补全等功能。这是因为 Vue 的开发工具还不成熟,你很可能会花更多的时间来配置工具,而不是实际编程:

  1. 打开 Webstorm 并创建一个新的空项目:

  1. 在左下角,你应该能够打开控制台或终端:

  1. 从这个提示中,你应该能够按照前面的段落中所解释的那样使用 npm。如果你还没有看过,请阅读一下。在我们的例子中,我们假设已经安装了 Node 和 vue-cli。

  2. 输入vue init simple并回答问题;你应该得到类似于以下内容的东西:

  1. 双击打开index.html文件。

  2. 将鼠标悬停在index.html文件的右上角,你应该能够看到浏览器图标;点击其中一个:

  1. 你的示例应用程序已经启动运行了!

总结

你可以在专门的案例中了解更多关于这个的工作原理。在这里,我希望你对使用 Vue 进行开发的可能性有一个概述。对于快速原型,你可以使用 JSFiddle。当你需要自己的环境或者只需要使用 Vue 开发工具的时候,使用文本编辑器就足够了。然而,对于大多数严肃的项目,你应该熟悉 npm、webpack 或者 Browserify,并使用 vue-cli 来创建你的新项目。

使用过滤器格式化你的文本

Vue 的第一个版本附带了一些文本过滤器,用于帮助格式化文本和解决一些常见问题。

在这个新版本中,没有内置的过滤器(除了下一个案例中介绍的 JSON 等效过滤器)。我认为这是因为编写自己的过滤器非常容易,而且在专门情况下可以很容易地找到在线库来完成更好的工作。最后,过滤器的用途有些变化:它们现在更多用于后处理,而不是实际的过滤和排序数组。

为了演示创建过滤器有多容易,我们将重新创建 Vue 旧版本中的一个过滤器:capitalize。

准备工作

你不需要任何特殊的知识来完成这个案例。

操作步骤

有时候我们有一些字符串漂浮在我们的变量中,比如标签。当我们把它们放在句子中间时,它们工作得很好,但是在句子或者项目符号的开头,它们看起来就不太好了。

我们想要编写一个过滤器,可以将我们放入其中的任何字符串都变成大写。如果,例如,我们希望字符串hello world以大写字母H开头,我们希望能够这样写:

{{'hello world' | capitalize }}

如果我们尝试在 Vue 应用程序中将其作为 HTML 运行,它会报错[Vue warn]: Failed to resolve filter: capitalize

让我们创建这个过滤器并将它添加到 Vue 的内部过滤器列表中:

  1. 写下以下 JavaScript 代码以注册一个过滤器并实例化 Vue:
        Vue.filter('capitalize', function (string) { 

          var capitalFirst = string.charAt(0).toUpperCase() 

          var noCaseTail = string.slice(1, string.length) 

            return capitalFirst + noCaseTail 

        }) 

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

  1. 在 HTML 部分中,写下以下内容:
        {{'hello world' | capitalize }}

  1. 运行代码并注意到文本现在显示为“Hello world”。

工作原理如下...

竖线表示以下内容是一个过滤器的名称;在我们的例子中,capitalize不在 Vue 的过滤器列表中,因此会有警告。Vue 将按原样打印字符串。

在 Vue 开始之前,它会在资产库中注册我们的过滤器(使用Vue.filter)。Vue 有一个内部过滤器对象,并将创建一个新条目:capitalize。每次遇到竖线符号时,Vue 都会查找相应的过滤器。记得在 Vue 实例的实际创建之前写好它,否则 Vue 将找不到它。

过滤器的工作原理非常基本的 JavaScript,事实上,使用 ES6 来编写这个过滤器会更好:

Vue.filter('capitalize', function (string) { 

  var [first, ...tail] = string 

  return first.toUpperCase() + tail.join('') 

})

如果您不熟悉 ES6,这里有一个简要的解释。第二行被称为解构赋值字符串;我们将字符串解释为一个字符数组,将第一个字符分割为第一个字符,并将所有其他字符放入tail中。这是将数组的不同部分分配给多个变量的更快的方法。可能看起来神秘的另一点是join('')。由于tail现在是一个字符数组,我们需要一些方法将单个字母重新连接成一个紧凑的字符串。join的参数表示单个字符之间的分隔符。我们不想要任何分隔符,所以传递一个空字符串。

在下一章中,您将找到更多关于过滤器的用例,并涵盖其他实际用途。

使用mustaches(例如JSON过滤器)调试应用程序

在前面的用例中,我们全面了解了过滤器,并说 Vue 除了JSON过滤器的等效功能之外没有内置过滤器。这个过滤器非常有用,虽然使用它来调试并不是真正正统的做法,但有时它确实能让生活更轻松。现在我们可以直接使用它而不需要自己编写。

操作步骤如下...

为了看到实际效果,我们可以在 Vue 实例中简单显示一个对象的值。

  1. 编写以下 JavaScript:
        new Vue({ 

          el: '#app', 

          data: { 

            cat: { 

              sound: 'meow' 

            } 

          } 

        })

这只是在我们的代码中创建了一个包含字符串的 cat 对象。

  1. 编写以下 HTML:
        <p>Cat object: {{ cat }}</p>

  1. 运行您的应用程序并注意到 cat 对象以所有其美丽的形式输出,就像 JSON.stringify 一样。

工作原理如下...

Cat 将显示 cat 对象的内容。在旧的 Vue 中,要获得这个结果,我们必须写成 {{ cat | json }}

需要小心的一件事是我们对象中的循环。如果我们的对象包含循环引用,并且用花括号括起来,这将不起作用。这些对象比你想象的更常见。例如,HTML 元素是包含对父节点的引用的 JavaScript 对象;父节点反过来包含对其子节点的引用。任何这样的树结构都会导致花括号打印对象的无限描述。当你实际这样做时,Vue 只是抛出一个错误并拒绝工作。你在控制台中看到的错误实际上是由用于打印 JSON.stringify对象的内部方法抛出的。

使用花括号的一个实际情况是当同一个值在多个位置被改变时,或者当你想快速检查变量的内容时。花括号甚至可以用于演示目的,正如你在本书中将看到的用法那样。

用 Vue 开发者工具对应用进行透视

使用花括号是显示对象内容的一种快捷方式。然而,它也有一些限制;其中一个在前面的示例中详细说明了,就是它默认情况下无法处理包含循环引用的对象。一个不会出现这个限制并且具有更多调试功能的工具是 Vue 开发者工具。有一个 Chrome 扩展程序,可以在开发的每一步中帮助您,可视化组件的状态,它们在页面中的位置以及更多。它还与 Vuex(在后面的示例中介绍)深度集成,并具有一个时间机器功能,可以直接从浏览器中倒回事件流。

准备工作

要安装它,只需在 Chrome Web Store 的扩展类别中下载扩展。只需搜索 Vue.js devtools 即可找到它,点击添加到 Chrome按钮,然后您就可以开始使用了:

不幸的是,您将无法在某些配置中使用它;特别是它目前似乎无法在 iframe 环境和 JSFiddle 中工作,所以为了看到它,您至少要使用在选择开发环境示例中概述的one page approach

怎么做...

  1. 访问 Chrome 开发者工具(通常使用 c md + opt + ICtrl + Shift + I ),你会看到一个新的标签页说 Vue。点击它将呈现出开发者工具。

为了使其能够在通过file://协议打开的页面上运行,您需要在 Chrome 的扩展管理面板中检查允许访问文件 URL 以便为该扩展程序添加权限。

您将看到一个按层次结构排列在页面上的组件树,通过选择它们,您将能够实时深入地查看所有的变量。

  1. 单击树中的各个对象以查看详细信息:

此外,您还将看到一个有用的按钮:检查 DOM 按钮(眼睛)将滚动页面到元素的位置,并在 Chrome 开发人员工具中显示 DOM 表示。此外,当您单击一个组件(详见插图中的根组件)时,您将在控制台中可以使用一个名为$vm0的变量。例如,您可以执行方法或检查变量。

  1. 单击根组件,并在控制台中输入以下内容以探索$vm0.docsUrl属性:

升级到 Vue.js 2

如果您需要将 Vue 应用程序升级到 2 版本,大部分代码都可以正常使用。但是,有几个功能需要进行一些修改。有些是简单的重命名,有些则比较复杂。

操作步骤如下:

为了让您的迁移开始,Chris Fitz(Vue 核心团队成员)创建了一个小助手应用程序,它将扫描您的代码并指导您进行迁移:

  1. 使用以下 npm 命令安装 Vue Migration Helper:
 npm install -g git://github.com/vuejs/vue-migration-helper.git

  1. 导航到您的应用程序文件夹。

  2. 使用以下命令运行程序:

 vue-migration-helper

需要进行更改的所有行将被突出显示。更新完成后,或者如果您仍然有疑问,您应该查看官方文档迁移页面rc.vuejs.org/guide/migration.html

它的工作原理是...

阅读文档将帮助您了解需要更新的关键点。在这里,我将为您提供一些最具挑战性修改的基本原理。

$broadcast、$dispatch 和 events 选项的弃用

方法$broadcast$dispatch现在已与旧版本相同的语法合并到$emit方法中。不幸地是,将每个$broadcast$dispatch实例都替换为$emit并不保证总是有效,因为现在用于管理事件的模式有些不同。

在 Vue 1 中,事件沿着层次结构树向下(对于$broadcast)或向上(对于$dispatch)以及水平(对于$emit)的路径传播。

说实话,我从来都不喜欢有两种(如果算上旧的 $emit ,则是三种)方法来触发事件。即使在最小的上下文中,它也很令人困惑,因为你必须问自己“这个事件是给父级还是子级的?”大部分情况下,这个区分并不重要,你只是想要调用你的方法。但是,不会有免费的午餐;为了使一切在新的范式下运行,我们必须添加一个移动部件到系统中。

现在,所有事件都应该通过一个或多个中央枢纽传递。这个中央枢纽的角色可以由一个 Vue 实例来承担,因为它们实现了必要的接口。

当触发一个v-on消费的事件时,你只需要用$emit替换$broadcast,因为事件不需要传递很远。另一方面,如果你在事件方面定义一个组件的接口,你将不得不告别事件选项,因为它将不再起作用。这是通过所有事件通过一个中央枢纽传递的直接结果 - 事件选项将不知道在哪里注册所有事件。这是只有一个发射方法的代价:它向所有方向触发,但只在一个精确的管道中触发。

假设你有一个专门的空的 Vue 实例作为事件中心:

var eventBus = new Vue()

如果你正在编写一个茶壶组件,并且你想要注册 brew 事件,你可以在 created 钩子中写入以下内容:

new Vue({ 

  el: '#app', 

  components: { 

   comp1: { 

         template: '<div/>', 

         created () { 

         eventBus.$on('brew', () => { 

         console.log('HTTP Error 418: I'm a teapot') 

        }) 

      } 

    }, 

    comp2: { 

         template: '<div/>', 

         created () { 

         eventBus.$emit('brew') 

      } 

    } 

  } 

})

HTML 如下:

<div id="app"> 

  <comp1></comp1> 

  <comp2></comp2> 

</div>

每当使用eventBus.$emit('brew')触发brew事件时,控制台将输出一条消息。

正如你所看到的,这个示例不太可扩展。你不能在 created 钩子中注册很多事件,然后期望轻松跟踪它们的功能以及它们在哪个中央枢纽中注册。对于这些更复杂的场景,建议的做法是使用后面介绍的 Vuex。

你编写的任何组件都可以充当事件中心。你还可以使用 API 方法$off来删除监听器,以及$once来监听事件,但只监听一次。

数组过滤器的弃用

如果你有很多经过过滤的v-for列表,我有个坏消息告诉你。即使在实际情况中,最常见的过滤器用法是与v-for一起使用,社区还是选择移除了这个特性。原因主要是因为有很多过滤器,经常连在一起使用,很难理解和维护。

推荐的过滤列表的新方法是使用计算属性。幸运的是,我们有一整个关于如何做到这一点的示例。在下一章节中查看示例使用计算属性过滤列表

Vue.config.delimiters 的弃用

自定义定界符不作用于组件级别。如果需要,可以创建两个使用不同定界符的不同组件。

这个升级非常简单,并且允许你编写组件,以便在其他模板引擎中使用:

<div id="app"> 

  {!msg!} 

</div>

new Vue({ 

 el: '#app', 

 data: { 

   msg:'hello world' 

 }, 

 delimiters: ['{!','!}'] 

})

生命周期钩子的重命名

生命周期现在有更一致的命名,能够帮助长期记住它们的名称:

旧的钩子 新的钩子
init beforeCreate
created created
beforeCompile created
没有等价项 beforeMount
compiled mounted
ready mounted
attached 没有等价项
detached 没有等价项
没有等价项 beforeUpdate
没有等价项 updated

第二章:Vue.js 的基本功能

本章将介绍以下内容:

  • 学习如何使用计算属性

  • 使用计算属性对列表进行筛选

  • 使用计算属性对列表进行排序

  • 使用过滤器格式化货币

  • 使用过滤器格式化日期

  • 根据条件显示和隐藏元素

  • 根据条件添加样式

  • 通过 CSS 过渡为您的应用程序增添一些乐趣

  • 输出原始 HTML

  • 创建带有复选框的表单

  • 创建带有单选按钮的表单

  • 创建带有选择元素的表单

介绍

在本章中,您将找到开发完全功能、交互式、独立的 Vue 应用程序所需的所有构建块。在第一个示例中,您将创建计算属性,这些属性封装了用于创建更语义化应用程序的逻辑;然后,您将使用过滤器和v-html指令进一步探索一些文本格式化。您将使用条件渲染和过渡创建一个图形吸引人的应用程序。最后,我们将构建一些表单元素,例如复选框和单选按钮。

从现在开始,所有示例都将专门使用 ES6 编写。在撰写本文时,如果您使用 Chrome 9x 和 JSFiddle 进行跟随,它们应该能够无缝运行;如果您将此代码集成到一个更大的项目中,请记得使用 Babel(有关更多信息,请参见第八章中的使用 Babel 编译 ES6示例,组织+自动化+部署=Webpack)。

学习如何使用计算属性

计算属性是 Vue 组件中依赖于其他更原始数据的某些计算的数据。当这些原始数据是响应式的时,计算属性会自动更新并响应式地更新。在这个上下文中,原始数据是一个相对的概念。您当然可以基于其他计算属性构建计算属性。

准备工作

在开始准备这个示例之前,请确保熟悉v-model指令和@event表示法。如果您不确定,可以在前一章中完成对点击和按键等事件做出反应示例。

操作步骤

一个简单的例子将清晰地说明计算属性是什么:

<div id="app"> 

  <input type="text" v-model="name"/> 

  <input type="text" id="surname" value='Snow'/> 

  <button @click="saveSurname">Save Surname</button> 

  <output>{{computedFullName}}</output> 

</div> 

let surname = 'Snow' 

new Vue({ 

  el: '#app', 

  data: { 

    name: 'John' 

  }, 

  computed: { 

    computedFullName () { 

      return this.name + ' ' + surname 

    } 

  }, 

  methods: { 

    saveSurname () { 

      surname = this.$el.querySelector('#surname').value 

    } 

  } 

})

运行此示例将显示两个输入字段:一个用于名字,一个用于姓氏,以及一个专门保存姓氏的按钮。检查 JavaScript 代码将发现,虽然名字是在对象的数据部分声明的,但姓氏是在 Vue 实例之外的开头声明的。这意味着它不会被 Vue 识别为反应性变量。我们可以通过编辑来检查,名字会影响计算值,而编辑姓氏则不会,即使姓氏变量本身实际上发生了变化,我们可以在浏览器控制台中检查到:

  1. 在 JSFiddle 上运行应用程序;你会在输入字段中看到JohnSnow,并且由于computedFullName的结果,你会看到以下内容:

  1. John替换为Johnny,你会看到计算属性实时变化。这是因为变量名是响应式的:

  1. Snow替换为Rain,然后点击“保存姓氏”。不会发生任何事情,因为surname不是响应式的。它不会触发视图的更新。让我们来检查它是否确实被保存了:

  1. John替换Johnny。计算属性中的姓氏立即变为“Rain”。这是因为更改名字触发了计算属性的更新:

我们刚刚实验证实了,尽管变量的更改被保存到内存中,但当编辑非响应式变量时,并不会触发视图刷新。

值得注意的是,对于反应性来说,在这里也存在相同的限制--如果变量是数组,在使用方括号表示法更改元素不起作用,不使用$remove删除元素也不起作用。有关计算属性的其他限制,您应该看一下官方文档vuejs.org/v2/guide/computed.html

还有更多...

在下文中,通过“依赖项”一词,我指的是在计算属性内部使用的反应性变量。当依赖项发生变化时,计算属性会被计算出来。

计算属性不适用于记忆数据,但如果直接设置值而不是通过其依赖项间接操作值更合理的话,可以定义一个 setter。而且,如果计算属性返回一个对象,每次都会是一个新对象,而不是之前版本的修改版。最后,只有所有依赖项都发生了变化,计算属性才会被调用。

这个缓存机制和 setter 的定义将在以下几节中进行分析。

缓存计算属性

虽然在 methods 选项中的函数在每次调用时都会执行,但在 computed 中的函数将根据依赖项进行缓存,而这些依赖项又是由函数中发现的所有响应式内容定义的。

在接下来的示例中,我们将探讨组合计算属性,但您可以很容易地想象出在计算属性上进行非常繁重的计算的情况:

computed: { 

  trillionthDigitOfPi () { 

    // hours of computations and terabytes later... 

    return 2 

  } 

}

然后,您可以反复使用相同的属性,而无需重新计算:

unnecessarilyComplexDoubler (input) { 

  return input * this.trillionthDigitOfPi 

}

每次调用此函数时,我们只需获取trillionthDigitOfPi的缓存值;不需要再次进行计算。

计算属性的 setter

有时,我们有一个计算属性,它真正表示我们模型中的一个明确对象,并且直接编辑它比修改其依赖关系更加清晰。

在表格工厂的背景下,我们希望指定要构建的表格数量或腿的数量:

<div id="app"> 

  <label>Legs: <input v-model="legCount" type="range"></label><br> 

  <label>Tops: <input @input="update" :value="tableCount"></label><br> 

  <output> 

    We are going to build {{legCount}} legs 

    and assembly {{tableCount}} tables. 

  </output> 

</div>

我们的状态仅由legCount确定,并且表格的数量将自动确定。创建一个新的 Vue 实例:

new Vue({ 

  el: '#app', 

  data: { 

    legCount: 0 

  }   

}

要知道表格的数量,我们有一个tableCount计算属性:

computed: { 

  tableCount: { 

    get () { 

      return this.legCount / 4 

    }, 

    set (newValue) { 

      this.legCount = newValue * 4 

    } 

  } 

}

get部分通常是任何时候属性的值,setter 允许我们直接设置表格的数量(以及腿的数量)。然后,我们可以编写update方法,该方法在更改表格数量时触发:

update (e) { 

  this.tableCount = e.target.value 

}

使用计算属性过滤列表

在早期版本的 Vue 中,过滤器在v-for指令中用于提取一些值。它们仍然被称为过滤器,但不再以这种方式使用。它们被降级为用于文本的后处理。老实说,我从来都不真正理解如何在 Vue 1 中使用过滤器来过滤列表,但在版本 2 中使用计算属性是过滤列表的唯一正确方式。

借助这个示例,您可以从最简单的待办事项列表到最复杂的太空船物料清单中对列表进行筛选。

准备工作

您应该对 Vue 列表有一定的了解,并了解计算属性的基础知识;如果不了解,阅读编写列表学习如何使用计算属性这两篇文章将帮助您了解基础知识。

操作步骤:

要开始使用这个食谱,我们需要一个示例列表来筛选我们最喜欢的元素。假设我们在ACME 研究与开发实验室工作,我们负责在任何领域复制一些实验。我们可以从以下列表中选择一个实验:

data: { 

  experiments: [ 

    {name: 'RHIC Ion Collider', cost: 650, field: 'Physics'}, 

    {name: 'Neptune Undersea Observatory', cost: 100, field: 'Biology'}, 

    {name: 'Violinist in the Metro', cost: 3, field: 'Psychology'}, 

    {name: 'Large Hadron Collider', cost: 7700, field: 'Physics'}, 

    {name: 'DIY Particle Detector', cost: 0, field: 'Physics'} 

  ] 

}

让我们使用一个简单的<ul>元素立即打印出列表:

<div id="app"> 

  <h3>List of expensive experiments</h3> 

  <ul> 

    <li v-for="exp in experiments"> 

      {{exp.name}} ({{exp.cost}}m 

) 

    </li> 

  </ul> 

</div>

如果你不是物理学的铁粉,你可能想从这个列表中筛选掉物理实验。为此,我们创建一个新的变量,它将只保存nonPhysics实验。这个变量将作为一个计算属性:

computed: { 

  nonPhysics () { 

    return this.experiments.filter(exp => exp.field !== 'Physics') 

  } 

}

当然,我们现在希望列表从这里绘制一个元素:

<li v-for="exp in nonPhysics"> 

  {{exp.name}} ({{exp.cost}}m 

) 

</li>

如果我们现在启动程序,只有非物理实验会出现在列表中:

它的工作原理是...

nonPhysics计算属性将包含带有指定处理方式的数组副本。它将简单地检查字段不是Physics的实验,并将新数组传递给v-for进行渲染。

正如你所看到的,过滤是完全任意的。我们可以选择从一个变量中获取一个单词,而不是Physics,该变量再从一个文本框中获取:

<input v-model="term"> // HTML 

// inside the Vue instance 

data: { 

  term: '' 

}, 

computed: { 

  allExceptTerm () { 

    return this.experiments 

      .filter(exp => exp.field.indexOf(this.term) === -1) 

  } 

}

更多内容...

事实证明,我们想重现这样的实验,但我们的预算有限;超过 300 万欧元的任何东西都在我们的限制之外。让我们创建一个过滤器:

lowCost () { 

  return this.experiments.filter(exp => exp.cost <= 3) 

}

如果我们使用这个过滤器替换之前的过滤器,我们仍然可以看到自己动手做粒子探测器的物理实验。由于我们不喜欢物理学,我们希望结合这两个过滤器。

在旧版的 Vue 中,你可以在v-for中同时使用两个过滤器;在这里,我们将刚刚创建的计算属性移动到方法部分,并将它们转换成纯函数:

methods: { 

  nonPhysics (list) { 

    return list.filter(exp => exp.field !== 'Physics') 

  }, 

  lowCost (list) { 

    return list.filter(exp => exp.cost <= 3) 

  } 

}

这样,过滤器就是可组合的;我们可以在v-for中这样使用它们:

<li v-for="exp in nonPhysics(lowCost(experiments))"> 

  {{exp.name}} ({{exp.cost}}m 

) 

</li>

减少 HTML 中的逻辑的另一种方法是将所有内容封装在一个专用的计算属性中:

filteredExperiments () { 

  return this.lowCost(this.nonPhysics(this.experiments)) 

}

HTML 变为如下所示:

<li v-for="exp in filteredExperiments"> 

  {{exp.name}} ({{exp.cost}}m 

) 

</li>

最后,在经过所有这些过滤后,列表中唯一剩下的元素是地铁里的小提琴手,而且公平地说,300 万欧元是小提琴的成本,而不是整个实验的成本。

使用计算属性对列表进行排序

在 Vue 1 中,使用过滤器对v-for进行排序是被考虑移除的另一件事情,在当前版本中没有幸存下来。

使用计算属性对列表进行排序提供了更大的灵活性,我们可以实现任何自定义的排序逻辑。在这个示例中,您将创建一个包含一些数字的列表;我们将使用它们对列表进行排序。

准备工作

要完成这个示例,您只需要对列表和计算属性有一些熟悉;您可以通过《编写列表》和《学习如何使用计算属性》这两个示例来了解它们。

操作步骤

让我们编写一个世界上最大的水坝列表。

首先,我们需要一个包含三列(名称,国家,电力)的 HTML 表格:

<div id="app"> 

<table> 

  <thead> 

    <tr> 

      <th>Name</th> 

      <th>Country</th> 

      <th>Electricity</th> 

    </tr> 

  </thead> 

  <tbody> 

  </tbody> 

</table> 

</div>

此外,我们还需要 Vue 实例的 JavaScript 代码,目前只包含了一组小型水坝的数据库、它们的位置以及它们产生的电力:

new Vue({ 

  el: '#app', 

  data: { 

    dams: [ 

      {name: 'Nurek Dam', country: 'Tajikistan', electricity: 3200}, 

      {name: 'Three Gorges Dam', country: 'China', electricity: 22500}, 

      {name: 'Tarbela Dam', country: 'Pakistan', electricity: 3500}, 

      {name: 'Guri Dam', country: 'Venezuela', electricity: 10200} 

    ] 

  } 

})

<tbody>标签内,我们放置了一个v-for,它将简单地迭代我们刚刚创建的水坝列表:

<tr v-for="dam in dams"> 

  <td>{{dam.name}}</td> 

  <td>{{dam.country}}</td> 

  <td>{{dam.electricity}} MegaWatts</td> 

</tr>

这将渲染为以下表格:

我们希望按照已安装的电力对这些水坝进行排序。为此,我们将创建一个计算属性damsByElectricity,它将返回一个有序的水坝集合:

computed: { 

  damsByElectricity () { 

    return this.dams.sort((d1, d2) => d2.electricity - d1.electricity); 

  } 

}

在添加计算属性后,我们只需要在 HTML 中写入damsByElectricity而不是 dams。其他都保持不变,行为也相同:

<tr v-for="dam in damsByElectricity"> 

  <td>{{dam.name}}</td> 

  <td>{{dam.country}}</td> 

  <td>{{dam.electricity}} MegaWatts</td> 

</tr>

它的工作原理是...

我们刚刚创建的计算属性damsByElectricity将返回一个数组,它将是this.dams的一个排序克隆。与计算属性一样,结果将被缓存(或记住);每次我们需要结果时,如果原始列表没有变化,函数将不会被调用,缓存的结果将被返回。

sort函数接受两个参数:列表的两个成员。如果第二个成员在第一个成员之后,则返回值必须是正数;如果相反,则返回值必须是负数。

通过d2.electricity - d1.electricity获得的顺序是降序的;如果我们想要升序的顺序,我们需要交换两个操作数或将它们乘以-1

更多内容...

我们可以通过将点击事件绑定到表头中的字段来扩展我们的列表,以便反转排序,这样当我们点击Electricity时,将以反方向对水坝进行排序。

我们将使用条件样式;如果您对它不熟悉,在完成《有条件地添加样式》这个示例后,您将会了解它。

为了清楚地表明我们的排序方式,我们应引入两个 CSS 类:

.ascending:after { 

  content: "25B2" 

} 

.descending:after { 

  content: "25BC" 

}

在这里,内容是指向上的箭头的 Unicode 表示,表示升序,而指向下的箭头表示降序。

首先,我们应该使用变量 order 跟踪顺序,当升序时 order 为 1,当降序时 order 为-1:

data: { 

  dams: [ 

    // list of dams 

  ], 

  order: 1 // means ascending 

},

条件样式是一个简单的三元运算符。有关条件样式的更多信息,请参阅《条件样式》中的章节:

<th>Name</th> 

<th>Country</th> 

<th v-bind:class="order === 1 ? 'descending' : 'ascending'" 

    @click="sort">Electricity</th>

这里,sort 方法的定义如下:

methods: { 

  sort () { 

    this.order = this.order * -1 

  } 

}

我们需要做的最后一件事是编辑 damsByElectricity 计算属性以考虑顺序:

damsByElectricity () { 

  return this.dams.sort((d1, d2) => 

    (d2.electricity - d1.electricity) * this.order); 

}

这样,当 order 为-1 时,顺序将被反转,表示升序:

使用过滤器格式化货币

在 Vue 1 中,格式化货币有一定的局限性;我们将使用优秀的 accounting.js 库来构建一个更强大的过滤器。

准备工作

过滤器的基础知识在“使用过滤器格式化文本”一章中得到了探讨;在此之前,你需要构建一个基本过滤器,确保你完成了那部分,然后再回到这里。

操作步骤

将 accounting.js 添加到你的页面中。有关如何操作的更多详细信息,请参考openexchangerates.github.io/accounting.js/。不过,如果你在使用 JSFiddle,你可以将其作为外部资源添加到左侧菜单。你可以添加一个 CDN 链接来提供资源,例如cdn.jsdelivr.net/accounting.js/0.3.2/accounting.js

这个过滤器非常简单:

Vue.filter('currency', function (money) { 

  return accounting.formatMoney(money) 

})

你可以在 HTML 中尝试使用一行代码:

I have {{5 | currency}} in my pocket

它将默认显示为美元,并打印"I have $5.00 in my pocket"。

工作原理是这样的...

当你在 JSFiddle 中或手动在页面中添加accounting.js时(或者使用import导入),你将使accounting对象可用。这样,你可以在过滤器中使用外部库(以及代码的任何其他地方)。

还有更多...

货币通常出现在表格中,它们需要对齐;让我们看看如何做到这一点。我们从如下 HTML 表格开始:

<div id="app"> 

<table> 

  <thead> 

    <tr> 

      <th>Item</th> 

      <th>Price</th> 

    </tr> 

  </thead> 

  <tbody> 

    <tr v-for="item in inventory"> 

      <td>{{item.name}}</td> 

      <td>{{item.price}} 

    </td> 

  </tr> 

  </tbody> 

</table> 

</div>

我们正在遍历一个库存,当然,我们需要在 JavaScript 中指定它:

new Vue({ 

  el:'#app', 

  data: { 

    inventory: [ 

      {name: 'tape measure', price: '7'}, 

      {name: 'stamp', price: '0.01'}, 

      {name: 'shark tooth', price: '1.5'}, 

      {name: 'iphone', price: '999'} 

    ] 

  } 

})

这时,我们有一个价格在页面上呈现的表格,但是没有货币符号,没有小数点后的位数一致性,也没有对齐。

我们计划使用我们的过滤器来帮助我们添加这三个。

在继续之前,最敏锐的读者可能会注意到我使用字符串来表示价格。为什么不用数字?这是因为 JavaScript 中的数字是浮点数;换句话说,它们不精确,因为小数位数是“浮动的”。

如果我们的销售中有一个售价为 0.83 欧元的小猫钥匙链,并且我们对此进行 50%的折扣,那么我们应该以 0.415 欧元的价格出售。由于不存在 0.5 分钱,我们将进行一些四舍五入。

一个客户在我们的在线商店上浏览,并对我们关于小猫的折扣感到惊讶。他购买了 3 个。如果你计算一下,应该是 1.245 欧元;我们对其应用Math.round函数,应该会得到 1.25 欧元。我们可以用以下代码进行检查:

Math.round(1.245 * 100) / 100 

// output: 1.25

然而,请考虑到我们编码了所有的计算:

var kittenKeychain = 0.83 

var kittyDiscount = 0.5 

var discountedKittenKeychain = kittenKeychain * kittyDiscount 

var boughtKeychains = discountedKittenKeychain * 3 

Math.round(boughtKeychains * 100) / 100 

// outputs: 1.24

在这个过程中我们损失了一分钱。设想一下,如果有一个大型应用程序处理成千上万的此类交易,或者设想一下如果这不是一个价格而是一个汇率。设想一下你需要将这个结果返回给后端,并且计算结果不匹配。误差可能会累积,最终的数字可能会有很大的差异。这只是一个小例子,但是在处理货币时使用浮点数还有更多的可能出错的地方。

使用字符串(或整数)表示货币可以给您想要的精度级别。

使用我们之前的过滤器将在小数点后引入美元符号和两个数字,但我们还是缺乏我们想要的对齐方式。我们应该为我们的 CSS 添加一个新的样式:

.price { 

  text-align: right 

}

将价格列分配给类名为 price 的类将确保在小数点上对齐。以下是完整的代码:

<div id="app"> 

<table> 

  <thead> 

    <tr> 

      <th>Item</th> 

      <th>Price</th> 

      </tr> 

  </thead> 

  <tbody> 

    <tr v-for="item in inventory"> 

      <td>{{item.name}}</td> 

      <td class="price">{{item.price | dollars}}</td> 

    </tr> 

  </tbody> 

</table> 

</div>

以下是用于 JavaScript 的代码:

Vue.filter('dollars', function (money) { 

  return accounting.formatMoney(money) 

}) 

new Vue({ 

  el:'#app', 

  data: { 

    inventory: [ 

      {name: 'tape measure', price: '7'}, 

      {name: 'stamp', price: '0.01'}, 

      {name: 'shark tooth', price: '1.5'}, 

      {name: 'iphone', price: '999'} 

    ] 

  } 

})

使用过滤器格式化日期

有时,您需要一个比基本过滤器更强大的过滤器。您必须多次使用类似的过滤器,但每次都有微小的变化。过多的过滤器可能会造成混乱。这个关于日期的示例将说明问题和解决方案。

准备工作

在继续之前,通过阅读“在第一章中使用过滤器格式化文本”这一节,使自己更加熟悉过滤器;如果您已经了解过滤器,请继续阅读。

如何操作

假设我们正在策划一个学习历史的互动页面。我们有一个包含以下内容的 Vue 实例和 JavaScript 代码:

new Vue({ 

  el:'#app', 

  data: { 

    bastilleStormingDate: '1789-07-14 17 h' 

  } 

})

在我们的数据中,我们有一个以字符串形式非正式地写入的日期在我们的实例数据中。我们的 HTML 可以包含法国大革命的时间线,并且在某个时候可以包含以下内容:

<div id="app"> 

  The Storming of the Bastille, happened on {{bastilleStormingDate | date}} 

</div>

我们需要一个能够完成句子的过滤器。为此,一个可能的库是优秀的moment.js,而且,为了我们的目的,我们选择了本地化版本:cdnjs.cloudflare.com/ajax/libs/moment.js/2.14.1/moment-with-locales.js

在添加了库之后,编写以下过滤器:

Vue.filter('date', function (date) { 

  return moment(date).format('LL') 

})

这将显示一个格式良好的日期:The Storming of the Bastille, happened on July 14, 1789

如果我们想要一个多语言网站,并且希望日期以法语格式显示呢?moment.js库对于本地化非常好;实际上,让我们用法语写同样的文本:

La prise de la Bastille, survenue le {{bastilleStormingDate | date}}

我们必须使用以下内容修改我们的过滤器:

Vue.filter('date', function (date) { 

  moment.locale('fr') 

  return moment(date).format('LL') 

})

我们的结果是La prise de la Bastille, survenue le 14 juillet 1789,非常好!然而,我们不想在每个页面中硬编码语言。最好的方法是在过滤器中添加一个参数。我们希望可以通过变量将语言传递给过滤器,就像这样:

La prise de la Bastille, survenue le {{bastilleStormingDate | date('fr')}}

为了实现这一点,我们必须在过滤器声明中添加第二个参数:

Vue.filter('date', function (date, locale) { 

  moment.locale(locale) 

  return moment(date).format('LL') 

})

这样,我们就可以通过页面中的一个变量将语言作为参数传递了,例如,根据选择的语言而定。

有条件地显示和隐藏元素

有条件地显示和隐藏网页上的元素对某些设计来说是基本的。你可以有一个弹出窗口,一组你想要逐个显示的元素,或者只在点击按钮时显示的东西。

在这个示例中,我们将使用条件显示,并了解重要的v-ifv-show指令。

准备工作

在进入这个示例之前,请确保你对计算属性有足够的了解,或者请查看《使用计算属性过滤列表》一节。

如何做到这一点...

让我们构建一个只在夜晚可见的幽灵:

<div id="ghost"> 

  <div v-show="isNight"> 

    I'm a ghost! Boo! 

  </div> 

</div>

v-show保证只有在isNighttrue时才会显示<div>幽灵。例如,我们可以写为:

new Vue({ 

  el: '#ghost', 

  data: { 

    isNight: true 

  } 

})

这将使幽灵可见。为了使示例更真实,我们可以将isNight作为一个计算属性来写:

new Vue({ 

    el: '#ghost', 

    computed: { 

      isNight () { 

        return new Date().getHours() < 7 

    } 

  } 

})

如果你在 JSFiddle 中加载这个程序,你将在午夜后和早上 7 点之前看到幽灵。如果你真的等不及要看到幽灵,你可以作弊并在夜间插入一个时间,例如:

return (new Date('4 January 03:30')).getHours() < 7

它是如何工作的...

v-show指令计算isNight计算属性,并在元素的style属性中放置一个display: none

这意味着该元素完全由 Vue 渲染;它只是看不见,就像幽灵一样。

另一个用于条件显示元素的指令是v-if指令。行为与v-show相同,只是在 DOM 中找不到该元素。当v-if评估为true时,该元素将被动态添加,没有元素样式的参与。要试用它,只需将v-show替换为v-if

<div id="ghost"> 

  <div v-if="isNight"> 

    I'm a ghost! Boo! 

  </div> 

</div>

一般来说,如果没有区别,使用v-show更好,因为从长远来看,它需要更少的资源。另一方面,如果你甚至不确定某些元素是否会出现在页面上,使用v-if可以让用户节省一些 CPU 时间(你永远不知道你的应用何时会爆红,并拥有数百万用户;通过选择正确的方式,你可以节省大量能量!)。

顺便说一句,在午夜之前不要等在页面前面。什么都不会发生。计算属性仅在其中的响应式属性发生更改时重新评估。在这种情况下,我们有一个非响应式的Date,因此不会触发任何更新。

有条件地添加样式

现代网页架构的一个伟大特性是可以在 CSS 中打包大量的显示逻辑。这意味着您可以拥有非常干净且表达力强的 HTML,同时通过 CSS 创建令人印象深刻的交互页面。

Vue 在表达 HTML 和 CSS 之间的关系方面特别擅长,并允许您将复杂的逻辑封装为易于使用的函数。

在本示例中,我们将探讨使用 Vue 进行样式设置的基础知识。

操作步骤如下:

我们将构建一个文本区域,当您接近允许的最大字符数时会发出警告:

<div id="app"> 

  <textarea 

    v-model="memeText" 

    :maxlength="limit"> 

  </textarea> 

  {{memeText.length}} 

</div>

所写的文本将与memeText变量绑定,文本的length将通过双大括号写在末尾。

当仅剩下 10 个字符时,我们想要更改背景颜色。为此,我们必须创建一个名为warn的 CSS 类:

.warn { 

  background-color: mistyrose 

}

我们将在textarea上使用此类来表示即将达到的写入上限。让我们看一下 JavaScript 代码:

new Vue({ 

  el: '#app', 

  data: { 

    memeText: 'What if I told you ' + 

              'CSS can do that', 

    limit: 50 

  } 

})

这只是我们的模型;我们想要添加一个名为longText的函数,当我们达到 40 个字符时(距离 50 个字符还有 10 个字符)评估为 true:

computed: { 

  longText () { 

    if (this.limit - this.memeText.length <= 10) { 

        return true 

    } else { 

        return false 

    } 

  } 

}

现在一切就位,来条件性地添加 warn 样式。为此,我们有两个选项:对象语法数组语法。我们先尝试使用对象语法:

<div id="app"> 

  <textarea 

    v-model="memeText" 

    :class="{ warn: longText }" 

    :maxlength="limit"> 

  </textarea> 

  {{memeText.length}} 

</div>

这意味着,每当longText评估为true(或一般为真值)时,类warn将被添加到textarea中。

工作原理...

如果您尝试在文本区域中输入超过 39 个字符,背景色将变为薄雾的玫瑰色。通常,n个类的对象语法如下所示:

:class="{ class1: var1, class2: var2, ..., classn: varn }"

然而,有几种代替此语法的方法。首先,您不需要在 HTML 中编写完整的对象;您还可以绑定到一个对象。一般的做法如下所示:

<div :class="classes"></div> // in HTML 

// in your Vue instance 

data: { 

  classes: { 

    warn: true 

  } 

}

此时,操纵类对象将向<div>添加或移除warn类。一种更聪明的绑定方式是绑定到一个计算属性,该计算属性本身返回一个对象:

<div :class="classes"></div> 

computed: { 

  classes () { 

    return { 

      warn: true 

    } 

  } 

}

当然,将一些自定义逻辑放在计算属性中会更容易:

computed: { 

  classes () { 

    const longText = this.limit - this.memeText.length <= 10 

    return { 

      warn: longText 

    } 

  } 

}

通过 CSS 过渡为应用程序增加一些乐趣

过渡是在元素被插入、更新和从 DOM 中移除时应用的效果。

对于本示例,我们将为朋友们建立一个小谜题。当他们想知道答案时,它将以淡入效果出现。

准备工作

要完成本课程,您应该已经了解条件显示和条件渲染。 条件显示和隐藏元素 将教您如何做到这一点。

具体操作步骤...

让我们在 HTML 中设置谜题:

<div id="app"> 

  <article> 

    They call me fruit.<br> 

    They call me fish.<br> 

    They call me insect.<br> 

    But actually I'm not one of those. 

    <div id="solution" @click="showSolution = true"> 

      I am a <span id="dragon" v-show="showSolution">Dragon</span> 

    </div> 

  </article> 

</div>

Vue 实例的初始化非常简单,您只需编写以下内容:

new Vue({ 

    el: '#app', 

  data: { 

    showSolution: false 

  } 

})

在 CSS 中,我们希望清楚地表明<div>解决方案可以被点击,因此我们添加了以下规则:

#solution { 

  cursor: pointer; 

}

现在应用程序可以工作了,但是您会立即看到 Dragon。我们想为我们的谜题增添一些优雅的效果,并通过淡入效果使龙出现。

我们需要两个 CSS 类;第一个类在解决方案出现时将被应用一次:

.fade-enter { 

  opacity: 0 

}

第二个类将在第一个类之后持续存在:

.fade-enter-active { 

  transition: opacity .5s;  

}

最后,我们将解决方案包装在一个过渡中:

I am a <transition name="fade"> 

  <span id="dragon" v-show="showSolution">Dragon</span> 

</transition>

工作原理...

过渡的名称是 CSS 类选择器的第一个单词(fade ),Vue 将根据元素出现或从屏幕上消失来查找它们。如果未指定名称并且只使用了<transition>,Vue 将使用过渡名称v作为 CSS。

在我们的情况下,之前看不见的龙正出现了,所以fade-enter将在一个刻度中应用(刻度是刷新视图的一个周期,但你可以把它看作是动画中的一个帧)。这意味着在开始时<span>实际上是不可见的,因为透明度将被设置为0

之后,fade-enter类将被移除,而附加在fade-enter上的fade-enter-active现在是唯一剩下的类。从该类的规则可以看出,透明度将在半秒钟内变为1。1 是在哪里指定的?这是默认值。

Vue 在过渡中要寻找的完整类集如下:

  • name-enter:这是enter的起始类;它在元素插入之前应用,并在一帧之后被移除。

  • name-enter-active:这是enter的持续类。它在元素插入之前应用,并在过渡/动画完成时被移除。使用它来定义过渡的特性,如持续时间和缓动。

  • name-enter-to:这是enter的结束类。在移除了name-enter时应用。

  • name-leave:这是leave的起始类。在触发leave过渡时应用,并在一帧之后被移除。

  • name-leave-active:这是leave的持续类。在触发leave过渡时应用,并在过渡/动画完成时被移除。

  • name-leave-to:这取代了name-leave

这里,name是您过渡的名称(在未指定名称时为v)。

还有更多...

过渡很酷,但是在这个示例中有一个遮挡我们视图的树,这破坏了过渡的声誉。为了跟进,请考虑以下 HTML:

<div id="app"> 

  <p> 

    Transitions are awesome, careful<br/> 

    please don't use them always. 

  </p> 

  <transition name="fade"> 

    <img id="tree" 

      src="http://i.imgur.com/QDpnaIE.png" 

      v-show="show" 

      @click="show = false"/> 

  </transition> 

</div>

一小段 CSS 如下所示:

#tree { 

  position: absolute; 

  left: 7.5em; 

  top: 0em; 

  cursor: pointer; 

} 

.fade-leave-active { 

  transition: opacity .5s; 

  opacity: 0 

}

最后,需要一个简单的 Vue 实例:

new Vue({ 

    el: '#app', 

  data: { 

    show: true 

  } 

})

当我们运行应用程序时,我们得到的结果如下所示:

点击树会显示真正的消息。

输出原始 HTML

有时,您需要插入 HTML 内容,例如换行符(<br>),到您的应用程序数据中。这可以通过使用v-html指令轻松实现。

在这个示例中,我们将构建一个感谢信。

准备工作

对于这个示例,您不需要任何特殊的知识,但我们将建立在一些基本的 Vue 功能之上;如果您在本章节或上一章节中完成了一个示例,您就可以开始了。

如何做...

假设你有一个朋友约翰。在收到礼物之前,你想要准备一封格式化的感谢信,但是你不知道他会送给你什么。你预先写了三份文本:

new Vue({ 

    el: '#app', 

  data: { 

    htmlTexts: [ 

    'Dear John,<br/>thank you for the <pre>Batman vs Superman</pre> DVD!', 

    'Dear John,<br/>thank you for <i>Ghostbusters 3</i>!', 

    'Dear John,<br/>thanks, <b>Gods of Egypt</b> is my new favourite!' 

    ] 

  } 

})

考虑到你会将这个变量直接输出在花括号内,如下所示:

<div id="app"> 

  {{htmlTexts[0]}} 

</div>

问题在于,这种情况下,你会得到纯文本和所有的 HTML 乱码:

这不是你要寻找的;你希望你的感谢信按照 HTML 标签的格式进行良好的排版。

你需要做的是使用v-html指令,如下所示:

<div id="app" v-html="htmlTexts[0]"> 

</div>

这样,HTML 标签就不会被 Vue 转义,并且会在我们的组件中按原样解释:

工作原理是这样的...

一般情况下,输出原始 HTML 是非常危险的。解释网站安全性超出了本书的范围,但只是为了让你有一个想法,想象一下你的网站上有一个评论部分,有人在评论中放置了一个<img>标签。如果你将其解释为 HTML 并展示给其他用户,你可能会让你的用户下载一个他们并不一定想要的图片;如果这个图片不属于你,你可能会因此被收取你没有计划的带宽费用。现在你可以扩展这个理由。如果一个用户在评论中放置了一个<script>标签,这将带来更大的风险,因为脚本可以做几乎任何事情。

默认情况下,Vue 通过不让你默认输出 HTML 来避免这个问题;这就是为什么我们需要特殊的v-html指令来查看它。也就是说,始终确保你对输出内容有完全控制。

还有更多...

还有另一种输出原始 HTML 的方法;这种方法更加先进,但更加清晰和可维护,尤其是对于那些严重依赖 HTML 格式化的组件来说。

在这些更加矫揉造作的情况下,你可以选择使用详细介绍在第九章的创建一个函数式组件配方中涵盖的函数式组件,但这里你将找到一个扩展了我们刚才所做的示例。

你应该写的 HTML 如下所示:

<div id="app"> 

  <thanks gift="Batman" decoration="strong"></thanks> 

</div>

你已经可以看到意图很明确:使用 HTML <strong>作为装饰来写一封关于蝙蝠侠礼物的感谢信。创建<thanks>组件的 JavaScript 代码如下所示:

Vue.component('thanks', { 

    functional: true, 

  render: function (createElement, context) { 

    let decoratedGift = 

      createElement(context.props.decoration, context.props.gift) 

    return createElement('p', ['Dear John, thanks for ', decoratedGift]) 

  }, 

  props: { 

    gift: String, 

    decoration: String 

  } 

})

当然,你还需要 Vue 实例。

创建一个带有复选框的表单

在当今的 Web 应用中,询问用户的输入是基本的。向用户展示多个选择使界面更有趣,对于结构化输入是必要的。

在这个教程中,你将学习如何通过构建确认页面来创建复选框,用于你自己的打印店!

准备工作

我们已经了解了 Vue 中的数据绑定是如何工作的,所以你可以开始操作了。否则,请返回第一个教程,收集 200 积分,然后继续阅读第一章中的响应点击和按键事件教程,以了解更多关于v-model指令的内容。

操作步骤

假设你需要为你的火星打印店设置三个不同的打印机:

    • 单色打印机
    • 等离子彩色打印机
    • 3D DNA 克隆打印机

确认页面基本上只是一个表单:

<div id="app"> 

  <form> 

    <!-- list of printers go here --> 

  </form> 

</div>

我们将使用v-model而不是名称来将我们的模型绑定到视图上:

<label> 

  <input type="checkbox" v-model="outputPrinter" value="monochrome"/> 

  Monochrome 

</label>

每个具有相同v-model<input>复选框都将参与一个反应性数组,在实时更新中插入和删除数组中的项。让我们在 Vue 实例中声明这个数组:

new Vue({ 

    el:'#app', 

  data:{ 

    outputPrinter: [] 

  } 

})

这只是一个普通的数组。所有选中的打印机将自动插入和从数组中移除。以下是完整的 HTML 代码:

<div id="app"> 

  <form> 

    <fieldset> 

      <legend>What printers you want to use?</legend> 

      <label> 

        <input type="checkbox" v-model="outputPrinter" value="monochrome"/> 

        Monochrome</label><br> 

      <label> 

        <input type="checkbox" v-model="outputPrinter" value="plasma"/> 

        Plasma Color</label><br> 

      <label> 

        <input type="checkbox" v-model="outputPrinter" value="cloner"/> 

        3D DNA Cloner</label><br> 

      <input type="submit" value="Print now"/> 

    </fieldset> 

  </form> 

</div>

这将生成一个类似下面的表单:

在你的应用程序的任何地方的<div>标签内放置{{ outputPrinter }},并通过选择打印机来实时查看数组的变化。

工作原理...

如果你选中第一个和最后一个打印机,数组将如下所示:

outputPrinter: ['monochrome', 'cloner']

然后,你可以使用这个数组将其通过 AJAX 发送到一个 Web 服务或进一步进行其他操作。

在 Vue 中,复选框只是普通的<input>元素,唯一的区别是我们实际上不需要在传统表单中使用的 name 属性。这是因为我们将不需要第二个页面来提交我们的值(通常使用 name 属性读取值的页面)。

更多内容...

要进入我所说的“第二个页面”,只需点击提交按钮。这是默认行为,在我们的情况下并不是我们想要的;因为我们通常不喜欢在处理 Vue 时需要改变页面,我将向你展示如何阻止默认行为。

现代网站倾向于在同一页上为你的操作提供反馈,有时甚至不会中断你的工作流程(如果你想在同一会话中克隆另外五个或六个实体怎么办?)

让我们将其变得更加有用。首先,我们必须阻止按钮的默认操作,即改变页面;为此,我们使用 prevent 修饰符:

<input type="submit" value="Print now" @click.prevent="printHandler"/>

printHandler将是我们 Vue 实例中的一个方法,它将为我们提供一些反馈。您可以自由地添加处理程序,例如一个弹出窗口告诉您打印正在进行中;也许您可以返回主页。

在这个示例中,我们将使用警报弹出窗口来检查按钮是否正常工作:

methods: { 

  printHandler () { 

    let printers = this.outputPrinter 

    alert('Printing with: ' + 

      (printers.length ? printers.join(', ') : 'none') + '.') 

  } 

}

创建一个带单选按钮的表单

单选按钮让您在多个选项中选择一个选项。当用户选择一个单选按钮时,任何先前选择的单选按钮将被取消选择。其常见用途是在创建注册表单时选择男性或女性。

准备工作

这个案例类似于“创建一个带复选框的表单”案例,因为我们使用了类似的技术。我建议你完成这两个案例,以成为 Vue 表单黑带。

操作步骤…

首先,我们需要一些可选择的内容,所以我们在 Vue 实例中编写一个数组:

new Vue({ 

  el: '#app', 

  data: { 

    genders: ['male', 'female', 'alien'], 

    gender: undefined 

  } 

})

我们将使用变量 gender(单数)来保存所选选项的值。从这里开始,我们可以通过几行代码来设置一个表单:

<div id="app"> 

  <form> 

    <fieldset> 

      <legend>Choose your gender</legend> 

      <label> 

        <input type="radio" v-model="gender" value="male"/> 

          Male 

      </label><br> 

      <label> 

        <input type="radio" v-model="gender" value="female"/> 

          Female 

      </label> <br>

      <label> 

        <input type="radio" v-model="gender" value="alien"/> 

          Alien 

      </label> 

    </fieldset> 

  </form> 

</div>

您可以运行应用程序,它将工作;但是,您需要在 form 后面添加一个胡子括号,以查看发生了什么:

<div> 

  Choosen gender: '{{ gender }}' 

</div>

这样,您可以看到单选按钮的点击如何影响内部数据:

工作原理…

这里我们只插入了三个单选按钮。由于它们都有v-model =“gender”,它们在逻辑上属于同一组。这意味着在任何给定的时间只能选择一个值。我们可以在同一个表单中有任意多个组。

进一步了解...

在这个案例中,单选按钮的值完全是固定的:

<input type="radio" v-model="gender" value="male"/>

我们可以修改value =“male”,使用v-bind:value使其动态响应。这会将值绑定到我们传递给它的任何变量。例如,假设我们的模型中有一个性别数组:

genders: ['male', 'female']

我们可以像这样重写前面的单选按钮:

<input type="radio" v-model="gender"**:value="genders[1]"**

/>

在这里,:valuev-bind:value的简写形式。

为了将我们学到的知识付诸实践,让我们构建一个简单的游戏。

假设您是一位农民,您的农场一开始没有动物。每天,动物市场上都有新的动物出售。您一次只能买一只。我们可以使用单选按钮来表示这个选择!

所以我们在我们的模型中有一个动物数组,一个包含我们每天选择的动物的变量,以及一个表示我们养殖场的农场数组(最初为空)。我们使用i变量添加了一点随机性,以保存表示当天可用动物的随机数:

data:{ 

  animals: ['

', '

', '

'], 

  animal: undefined, 

  farm: [], 

  i: 0 

}

我使用表情符号来表示动物,因为它们非常有趣。如果你不知道在哪里找到它们,只需从emojipedia.org/复制并粘贴它们,然后搜索动物。

我们可以从最初使用的相同 HTML 开始;我们只需要改变图例:

<legend>Today's animals</legend>

此时,我们应该添加一个动物列表供选择,但我们希望它是动态的,也就是说,每天都有不同的动物对:

<label> 

  <input type="radio" v-model="animal" :value="animals[i]"/> 

  {{animals[i]}} 

</label><br> 

<label> 

  <input type="radio" v-model="animal" :value="animals[i+1]"/> 

  {{animals[i+1]}} 

</label><br>

这意味着随着i变量的改变,单选按钮的值(和标签)将会改变。

唯一剩下的就是一种购买动物、将其添加到农场并等待下一天的方法。我们将在提交按钮中总结所有这些:

<input type="submit" value="Add to Farm" @click.prevent="addToFarm"/>

在这里,addToFarm方法由以下内容定义:

addToFarm () { 

    if (this.animal === undefined) { return } 

    this.farm.push(this.animal) 

    this.i = Math.floor(Math.random() * (this.animals.length - 1)) 

  this.animal = undefined 

}

如果没有选择动物,则不执行任何操作;否则,将动物添加到农场,为下一天生成一个随机数,并重置选择。要查看你的农场,请将以下内容添加到你的 HTML 中:

<div> 

  Your farm is composed by {{ farm.join(' ') }} 

</div>

你的应用程序将如下所示:

创建一个带有选择元素的表单

当单选按钮无法满足需求时,选择元素或“下拉列表”用于表单,无论是因为选择太多还是因为无论有多少选项,它们始终占据相同的空间。

准备工作

我建议您在深入研究选择元素之前先完成有关数据绑定或表单的教程。有关单选按钮的教程将使您熟悉单选按钮,其功能类似于选择元素。

如何操作...

在本教程中,我们将创建一个简单的国家选择器。我将首先在没有 Vue 的帮助下编写选择器,只是为了复习 HTML。首先,创建一个form,在其中放置select元素:

<form> 

  <fieldset> 

    <legend>Choose your country</legend> 

      <!-- here goes the select element --> 

  </fieldset> 

</form>

fieldset中,编写select元素的代码:

<select> 

  <option>Japan</option> 

  <option>India</option> 

  <option>Canada</option> 

</select>

运行应用程序。从一开始就有一个可工作的选择元素。结构非常简单。每个<option>将增加可选择的事物列表。

目前,对于这个元素来说,还没有太多可以做的。让我们将选择的国家与 Vue 绑定到一个变量上。您需要编辑您的 HTML:

<select v-model="choosenCountry">

现在,您需要将choosenCountry添加到您的模型中:

new Vue({ 

    el:'#app', 

  data:{ 

    choosenCountry: undefined 

  } 

})

不要忘记用<div id="app">将表单包围起来,否则 Vue 将无法识别它。

现在运行应用程序,你会注意到,之前下拉菜单以日本为默认选择,现在它遵循了我们在代码中的初始化。

这意味着初始状态下没有选择任何国家。我们可以添加一个花括号来查看变量的当前状态:

<div> 

  {{choosenCountry}} 

</div>

国家选择器将如下所示:

它是如何工作的...

当你使用v-model<select>元素绑定时,所选选项将填充绑定的变量。

请注意,如果为选项设置了值,变量将使用该值,而不是标签中写的内容。例如,你可以这样写:

<select> 

  <option value="1">Japan</option> 

  <option value="2">India</option> 

  <option value="7">Canada</option> 

</select>

这样可以确保每个国家都绑定到一个数值。

还有更多...

通常,国家和城市以层次结构的方式排列。这意味着我们需要两个或更多的下拉菜单来确定用户的出生地,例如。在本段中,我们将使用生物学的等价物来选择动物:

clans: { 

  mammalia: { 

    'have fingers': { 

      human: 'human', 

      chimpanzee: 'chimpanzee' 

    }, 

    'fingerless': { 

      cat: 'cat', 

      bear: 'bear' 

    } 

  }, 

  birds: { 

    flying: { 

      eagle: 'eagle', 

      pidgeon: 'pidgeon' 

    }, 

    'non flying': { 

      chicken: 'chicken' 

    } 

  } 

}

我们将把顶层称为clan,第二层称为type,最后一层将是一个动物。我知道这是一种非正统的分类动物的方式,但对于这个例子来说,它是有效的。

让我们为 Vue 模型添加两个保存状态的变量:

clan: undefined, 

type: undefined

现在我们可以添加第一个select元素:

<select v-model="clan"> 

  <option v-for="(types, clan) in clans">{{clan}}</option> 

</select>

这将创建一个下拉菜单,其中包含以下内容:

  • 哺乳动物

  • 鸟类

在这种特殊情况下,变量types实际上没有起作用。

我们希望用特定clantype填充第二个下拉菜单:

<select v-model="type"> 

  <option v-for="(species, type) in clans[clan]">{{type}}</option> 

</select>

当变量clan有值时,这个选择元素将让你选择动物的类型。请注意,尽管我们为物种添加了第三个选择:

<select> 

  <option v-for="(animals, species) in clans[clan][type]">{{species}}</option> 

</select>

它会导致我们的程序出错,因为clans[clan]是未定义的,Vue 将尝试对其进行求值。为了纠正这个问题,我们可能希望只有在第一个和第二个选择有值时才显示第三个选择元素。为此,我们可以使用v-show指令,但问题是 Vue 会渲染带有v-show的元素,只有在渲染之后才会隐藏它们。这意味着错误仍然会被抛出。

正确的方法是使用v-if,如果内部条件不满足,则阻止元素的渲染,如下所示:

<select v-if="clans[clan]"> 

  <option v-for="(animals, species) in clans[clan][type]">{{species}}</option> 

</select>

现在,继续选择你最喜欢的动物层次结构吧!

第三章:过渡和动画

本章将涵盖以下内容:

  • 与第三方 CSS 动画库(如 animate.css)集成

  • 添加自己的过渡类

  • 使用 JavaScript 而不是 CSS 进行动画

  • 在初始渲染时进行过渡

  • 在元素之间进行过渡

  • 在过渡中在进入阶段之前让元素离开

  • 为列表元素添加进入和离开过渡

  • 在列表中移动的元素进行过渡

  • 动画化组件的状态

  • 将可重用的过渡打包成组件

  • 动态过渡

介绍

本章包含与过渡和动画相关的示例。Vue 有自己的标签用于处理元素进入或离开场景时的过渡:<transition><transition-group>。您将学习如何使用它们,以便为您的客户提供更好的用户体验。

Vue 过渡非常强大,因为它们是完全可定制的,并且可以轻松地结合 JavaScript 和 CSS 样式,同时具有非常直观的默认值,这样您就可以在不需要所有花哨效果的情况下编写更少的代码。

即使没有过渡标签,您也可以在组件中动画化大部分内容,只需将状态变量绑定到某些可见属性即可。

最后,一旦您掌握了关于 Vue 过渡和动画的所有知识,您可以轻松地将它们打包到分层组件中,并在整个应用程序中重复使用它们。这不仅使它们强大,而且易于使用和维护。

与第三方 CSS 动画库(如 animate.css)集成

图形界面不仅需要可用性和易于理解,还应提供实惠性和愉悦性。通过提供过渡效果,可以以有趣的方式提供网站的工作方式的提示。在本示例中,我们将介绍如何在应用程序中使用 CSS 库。

准备工作

在开始之前,您可以查看daneden.github.io/animate.css/,如所示,只是为了了解可用的动画效果,但您实际上不需要任何特殊知识来继续:

操作步骤

假设您正在创建一个预订出租车的应用程序。我们将创建一个简单而有趣的界面。

首先,将animate.css库添加到依赖列表中(参考“选择开发环境”教程来学习如何做)。

接下来,我们需要使用我们通常的包装器:

<div id="app"> 

</div>

在包装器内部,我们将放置一个按钮来呼叫出租车:

<button @click="taxiCalled = true"> 

  Call a cab 

</button>

你可以看出我们将使用taxiCalled变量来跟踪按钮是否被按下。

让我们添加一个表情符号,以向用户确认出租车已被呼叫:

<p v-if="taxiCalled">

</p>

此时,我们可以添加一些 JavaScript 代码:

new Vue({ 

  el: '#app', 

  data: { 

    taxiCalled: false 

  } 

})

运行应用程序,当你按下按钮时,你会看到出租车立即出现。我们是一家很酷的出租车公司,所以让我们让出租车通过过渡驶向我们:

<transition  

  enter-active-class="animated slideInRight"> 

  <p v-if="taxiCalled">

</p> 

</transition>

现在运行你的应用程序;如果你呼叫出租车,它将从右侧滑动到你这里:

出租车将从右向左滑动,如图所示:

它是如何工作的...

每个过渡应用四个类。两个类在元素进入场景时应用,另外两个类在元素离开时应用:

名称 应用时机 移除时机
v-enter 元素插入之前 一帧后
v-enter-active 元素插入之前 过渡结束时
v-enter-to 一帧后 过渡结束时
v-leave 过渡开始时 一帧后
v-leave-active 过渡开始时 过渡结束时
v-leave-to 一帧后 过渡结束时

这里,初始的v代表你的过渡名称。如果你没有指定名称,将使用v

虽然过渡的开始是一个明确定义的瞬间,但过渡的结束对于浏览器来说需要一些工作。例如,如果 CSS 动画循环,动画的持续时间只会是一个迭代。此外,这可能会在未来的版本中发生变化,所以请记住这一点。

在我们的例子中,我们想要提供一个第三方的v-enter-active,而不是编写我们自己的。问题是我们的库已经为我们想要使用的动画类(slideInRight)有一个不同的名称。由于我们无法更改类的名称,我们告诉 Vue 使用slideInRight而不是寻找v-enter-active类。

为了做到这一点,我们使用了以下代码:

<transition enter-active-class="animated slideInRight">

这意味着我们的v-enter-active现在被称为animated slideInRight。Vue 将在元素插入之前附加这两个类,并在过渡结束时删除它们。只要注意animatedanimate.css附带的一种辅助类。

添加自己的过渡类

如果您的应用程序具有丰富的动画效果,并且希望通过混合和匹配在其他项目中重用您的 CSS 类,那么这就是适合您的方法。您还将了解一种用于高性能动画的重要技术,称为 FLIP(First Last Invert Play)。虽然后一种技术通常由 Vue 自动触发,但我们将手动实现它以更好地理解其工作原理。

准备工作

要完成此方法,您应该了解 CSS 动画和过渡的工作原理。这超出了本书的范围,但您可以在css3.bradshawenterprises.com/找到一个很好的入门指南。这个网站也很棒,因为它会解释您何时可以使用动画和过渡效果。

操作步骤...

我们将为出租车公司构建一个界面(类似于前面的示例),用户可以通过点击按钮呼叫出租车,并在呼叫出租车时提供一个漂亮的动画反馈。

编写以下 HTML 代码来创建按钮:

<div id="app"> 

  <button @click="taxiCalled = true"> 

    Call a cab 

  </button> 

  <p v-if="taxiCalled">

</p> 

</div>

然后,您在以下 JavaScript 代码中将taxiCalled变量初始化为false

new Vue({ 

  el: '#app', 

  data: { 

    taxiCalled: false 

  } 

})

此时,我们将在 CSS 中创建自定义过渡效果:

.slideInRight { 

  transform: translateX(200px); 

} 

.go { 

  transition: all 2s ease-out; 

}

将您的汽车表情包装在 Vue 过渡中:

<transition  

  enter-class="slideInRight" 

  enter-active-class="go"> 

  <p v-if="taxiCalled">

</p> 

</transition>

当您运行代码并点击“Call a cab”按钮时,您将看到一辆出租车停在旁边。

工作原理...

当我们点击按钮时,taxiCalled变量变为true,Vue 将出租车插入到您的页面中。在实际执行此操作之前,它会读取您在enter-class中指定的类(在本例中仅为slideInRight)并将其应用于包装元素(带有出租车表情的<p>元素)。它还会应用在enter-class-active中指定的类(在本例中仅为go)。

enter-class中的类在第一帧后被移除,enter-class-active中的类在动画结束时也被移除。

此处创建的动画遵循 FLIP 技术,由四个点组成:

  • First (F):您将属性保持为动画的第一帧;在我们的例子中,我们希望出租车从屏幕右侧开始。

  • Last (L):您将属性保持为动画的最后一帧,也就是在我们的例子中出现在屏幕左侧的出租车。

  • 反转(I):您反转了在第一帧和最后一帧之间注册的属性变化。由于我们的出租车向左移动,在最后一帧它将位于-200 像素偏移处。我们反转它,并将slideInRight类的 transform 设置为translateX(200px),这样出租车出现时将位于+200 像素偏移处。

  • 播放(P):我们为每个已触摸的属性创建一个过渡。在出租车示例中,我们使用 transform 属性,因此我们使用writetransition: all 2s ease-out来平滑地过渡出租车。

Vue 在<transition-group>标签内部自动使用此技术使过渡效果正常工作。有关此内容的更多信息,请参阅“为列表元素添加进入和离开过渡”配方。

使用 JavaScript 进行动画而不是 CSS

普遍认为使用 JavaScript 进行动画会更慢,动画应该在 CSS 中完成。事实是,如果使用正确,JavaScript 中的动画可以具有相似或更好的性能。在本配方中,我们将使用简单但强大的 Velocity.js(velocityjs.org/)库创建动画:

准备工作

本配方假设您对 Velocity 库没有任何了解,但假设您对 CSS 中的动画或使用 JavaScript 库(如 jQuery)进行动画非常熟悉。如果您从未见过 CSS 动画,并且想要快速入门,请完成前两个配方,然后您应该能够跟上。

操作步骤...

我们仍在寻找一个完美的过渡效果,用于一家出租车公司(与前一个配方中相同),以在等待出租车时娱乐我们的客户。我们有一个按钮来呼叫出租车,当我们预订时会出现一个小出租车表情符号。

在任何其他操作之前,将 Velocity 库添加为项目的依赖项- cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js

以下是创建界面骨架的 HTML 代码:

<div id="app"> 

  <button @click="taxiCalled = true"> 

    Call a cab 

  </button> 

  <p v-if="taxiCalled">

</p> 

</div>

我们的 Vue 模型非常简单,只包含taxiCalled变量:

new Vue({ 

  el: '#app', 

  data: { 

    taxiCalled: false 

  } 

})

通过将小出租车包装在 Vue 过渡中创建动画:

<transition 

  @enter="enter" 

  :css="false" 

> 

<p v-if="taxiCalled">

</p> 

</transition>

当按下按钮插入出租车表情符号时,将调用 enter 方法。

您需要将 enter 方法添加到 Vue 实例中,代码如下:

methods: { 

    enter (el) { 

      Velocity(el,  

      { opacity: [1, 0], translateX: ["0px", "200px"] }, 

      { duration: 2000, easing: "ease-out" }) 

    } 

  }

运行代码并按下按钮预订出租车!

工作原理...

正如你可能已经注意到的,你的代码中没有 CSS。动画完全由 JavaScript 驱动。让我们来解析一下 Vue 过渡:

<transition 

  @enter="enter" 

  :css="false" 

> 

  <p v-if="taxiCalled">

</p> 

</transition>

尽管这仍然是一个可以使用 CSS 的过渡,但我们希望告诉 Vue 关闭 CSS 并节省宝贵的 CPU 周期,通过设置:css="false"。这将使 Vue 跳过与 CSS 动画相关的所有代码,并防止 CSS 干扰我们纯粹的 JavaScript 动画。

精彩的部分在于@enter="enter"。我们将触发元素插入时的钩子绑定到enter方法上。方法本身如下所示:

enter (el) { 

  Velocity(el,  

    { opacity: [1, 0], translateX: ["0px", "200px"] }, 

    { duration: 2000, easing: "ease-out" }

  ) 

}

在这里,我们调用了 Velocity 库。el参数由 Vue 免费传递,并且它指的是被插入的元素(在我们的例子中,是包含汽车表情符号的<p>元素)。

Velocity 函数的语法如下所示:

Velocity( elementToAnimate, propertiesToAnimate, [options] )

还有其他语法可能,但我们将坚持这个。

在调用这个函数时,我们将段落元素作为第一个参数传递;然后我们说透明度应该从 0 变为 1,并且同时元素应该从 x 轴上的起始位置 200 像素移动到其原点。作为选项,我们指定动画持续时间为两秒,并且我们希望在接近结束时缓和动画。

我认为一切都很清楚,也许除了我们如何传递opacitytranslateX参数。

这就是 Velocity 所谓的forcefeeding--我们告诉 Velocity 透明度应该从 0 开始到 1。同样,我们告诉 VelocitytranslateX属性应该从 200 像素开始,结束于 0 像素。

一般来说,我们可以避免传递数组来指定属性的初始值;Velocity 会计算如何过渡。

例如,我们可以有以下 CSS 类:

p { 

  opacity: 0; 

}

如果我们将 Velocity 调用重写如下:

Velocity(el,  

  { opacity: 1 } 

)

汽车将慢慢出现。Velocity 查询了元素的初始值,然后将其过渡到 1。这种方法的问题在于,由于涉及对 DOM 的查询,某些动画可能会变慢,特别是当您有很多并发动画时。

我们可以通过使用 begin 选项来获得与 force-feeding 相同的效果,如下所示:

Velocity(el,  

  { opacity: 1 }, 

  { begin: () => { el.style.opacity = 0 } } 

)

在动画开始之前(也就是元素插入之前),将不透明度设置为零。这将有助于在较慢的浏览器中,在将元素完全移到右侧并开始动画之前,仍然会显示一闪而过的汽车。

JavaScript 动画的可能钩子在下表中总结如下:

属性 描述
@before-enter 在元素插入之前调用此函数。
@enter 在元素插入时调用此函数。
@after-enter 在元素插入并完成动画时调用此函数。
@enter-cancelled 在动画仍在进行中但元素必须离开时调用此函数。如果使用 Velocity,可以执行类似Velocity(el, "stop")的操作。
@before-leave 在触发离开函数之前调用此函数。
@leave 在元素离开时调用此函数。
@after-leave 在元素离开页面时调用此函数。
@leave-cancelled 在离开调用完成之前必须插入元素时调用此函数。仅适用于 v-show。

请注意,这些钩子对于任何库都有效,不仅仅适用于 Velocity。

还有更多...

我们可以尝试使用此界面实现一个取消按钮。如果用户错误地预订了出租车,点击取消按钮将删除预订,并且通过小出租车表情消失来显示这一点。

首先,让我们添加一个取消按钮:

<button @click="taxiCalled = false">

  Cancel

</button>

这很容易;现在我们添加离开过渡效果:

<transition 

  @enter="enter" 

  @leave="leave" 

  :css="false" 

> 

  <p v-if="taxiCalled">

</p> 

</transition>

这将带我们到离开方法:

leave (el) { 

  Velocity(el, 

    { opacity: [0, 1], 'font-size': ['0.1em', '1em'] }, 

    { duration: 200}) 

}

我们所做的是让表情符号消失并缩小。

如果尝试运行代码,会遇到一些问题。

当点击取消按钮时,应该发生的是离开动画应该开始,出租车应该变小并最终消失。但实际上,什么都没有发生,出租车突然消失了。

取消动画无法按计划播放的原因是,由于动画是用 JavaScript 编写而不是 CSS,Vue 无法判断动画何时完成。特别是,Vue 认为离开动画在开始之前就已经完成了。这就是我们的汽车消失的原因。

诀窍在于第二个参数。每个钩子都调用一个带有两个参数的函数。我们已经看到了第一个参数el,它是动画的主题。第二个参数是一个回调函数,当调用时,告诉 Vue 动画已经完成。

我们将利用 Velocity 有一个名为complete的选项,它期望在动画(从 Velocity 的角度)完成时调用一个函数。

让我们根据这个新信息重写我们的代码:

leave (el, done) { 

  Velocity(el, 

  { opacity: [0, 1], 'font-size': ['0.1em', '1em'] }, 

  { duration: 200 }) 

}

通过在我们的函数中添加done参数,让 Vue 知道我们希望在动画完成时调用一个回调。我们不需要显式地使用回调,因为 Vue 会自己找出来,但是由于依赖默认行为总是一个坏主意(如果它们没有被记录,它们可能会改变),让我们在动画完成时调用done函数:

leave (el, done) { 

  Velocity(el, 

  { opacity: [0, 1], 'font-size': ['0.1em', '1em'] }, 

  { duration: 200, complete: done }) 

}

运行你的代码,然后按下取消按钮来取消你的出租车!

在初始渲染时进行过渡

通过使用appear关键字,我们可以在元素首次加载时为其添加过渡效果。这有助于提高用户体验,因为它给人一种页面更具响应性和加载速度更快的印象,尤其是当你将其应用于许多元素时。

准备工作

这个示例不需要任何特定的知识,但是如果你至少完成了“使用 CSS 过渡为应用程序添加一些乐趣”这个示例,那么这将是小菜一碟。

操作步骤...

我们将构建一个关于美国演员 Fill Murray 的页面;不是 Bill Murray。你可以在www.fillmurray.com找到关于他的更多信息。我们将使用这个网站上的图片来填充我们关于他的页面。

在我们的 HTML 中,让我们写一个标题作为我们页面的标题:

<h1> 

  The Fill Murray Page 

</h1>

在标题之后,我们将放置我们的 Vue 应用程序:

<div id="app"> 

  <img src="https://fillmurray.com/50/70"> 

  <p> 

    The internet was missing the ability to 

    provide custom-sized placeholder images of Bill Murray. 

    Now it can. 

  </p> 

</div>

在浏览器中呈现时,将显示如下:

我们的页面现在非常简单。我们希望 Fill Murray 的图片淡入。我们必须将其包装在一个过渡中:

<transition appear> 

  <img src="https://fillmurray.com/50/70"> 

</transition>

以下是 CSS 类:

img { 

  float: left; 

  padding: 5px 

} 

.v-enter { 

  opacity: 0 

} 

.v-enter-active { 

  transition: opacity 2s 

}

现在运行我们的页面,图片将会慢慢出现,但同时也会移动文本。为了修复这个问题,我们必须提前指定图片的大小:

<transition appear> 

  <img src="https://fillmurray.com/50/70" width="50" height="70"> 

</transition>

这样,我们的浏览器将为图片预留一些空间,以便它慢慢出现。

工作原理...

transition标签中的appear指令将使组件在首次出现时带有关联的过渡效果(如果找到)。

在组件的第一次渲染中,有许多可能的方法来指定过渡效果。在所有情况下,必须指定appear指令。

当存在该指令时,Vue 首先会查找 JavaScript 钩子或标签中指定的 CSS 类:

<transition 

  appear 

  @before-appear="customBeforeAppearHook" 

  @appear="customAppearHook" 

  @after-appear="customAfterAppearHook" 

  appear-class="custom-appear-class" 

  appear-active-class="custom-appear-active-class" 

> 

  <p>My element</p> 

</transition>

之后,如果指定了名称,Vue 将查找该元素的入场过渡效果:

<transition appear name="myTransition"> 

  <p>My element</p> 

</transition>

上述代码将查找以下命名的类:

.myTransition-enter {...} 

.myTransition-enter-active {...}

如果其他方法都失败了,Vue 将查找元素插入的默认 CSS 类(v-enterv-enter-active)。顺便说一句,这就是我们在示例中所做的。

依赖这些默认值并不是一个好的做法;在这里,我们只是作为演示而这样做。您应该始终为过渡效果命名。

也许值得一提的是,为什么我们必须为图像添加宽度和高度。原因是当我们在 HTML 中指定图像 URL 时,浏览器无法提前知道图像的大小,因此默认情况下不会为其保留任何空间。只有通过提前指定图像的大小,浏览器才能在加载图像之前正确地组合页面。

元素之间的过渡

网页上的所有内容都是元素。借助 Vue 的v-ifv-show指令,您可以轻松地使它们出现和消失。通过过渡效果,您甚至可以轻松控制它们的出现方式并添加魔法效果。本示例将解释如何实现。

准备工作

在进行本示例之前,您应该对 Vue 过渡效果和 CSS 的工作原理有一定的了解。如果您完成了第二章中的“使用 CSS 过渡为应用程序增添乐趣”示例,这将很容易实现。

操作步骤

既然我们谈到了魔法,我们将把一只青蛙变成公主。变换本身将是一个过渡效果。

我们将创建一个按钮,当按下时,表示对青蛙的亲吻:

<div id="app"> 

  <button @click="kisses++">

Kiss!</button> 

</div>

每次按下按钮时,变量 kisses 都会增加。变量将初始化为零,如下面的代码所示:

new Vue({ 

   el: '#app', 

  data: { 

   kisses: 0 

  } 

})

接下来,我们需要青蛙和公主,它们将立即添加到按钮之后:

<transition name="fade"> 

  <p v-if="kisses < 3" key="frog">

frog</p> 

  <p v-if="kisses >= 3" key="princess">

princess</p> 

</transition>

淡入淡出过渡效果的 CSS 代码如下:

.fade-enter-active, .fade-leave-active { 

  transition: opacity .5s 

} 

.fade-enter, .fade-leave-active { 

  opacity: 0 

}

为了使其正常工作,我们需要添加一个最后的 CSS 选择器:

p { 

  margin: 0; 

  position: absolute; 

  font-size: 3em; 

}

如果运行应用程序并多次点击亲吻按钮,您应该会看到青蛙变成公主:

这个过渡效果将具有淡入淡出的效果:

青蛙表情符号将变成公主表情符号:

工作原理如下...

当我们写下这两个元素时,我们使用了key属性来指定谁是青蛙,谁是公主。这是因为,否则 Vue 优化系统将会启动。它会发现这两个元素的内容可以互换,而不需要交换元素本身,因此不会发生过渡,因为元素是相同的,只有内容发生了变化。

如果我们移除key属性,我们可以看到青蛙和公主会改变,但没有任何过渡效果:

<transition name="fade"> 

  <p v-if="kisses < 3">

frog</p> 

  <p v-if="kisses >= 3">

princess</p> 

</transition>

考虑到我们使用了两个不同的元素,如下所示:

<p v-if="kisses < 3" >

frog</p> 

<span v-if="kisses >= 3">

princess</span>

同时,我们相应地修改了<p>的 CSS 选择器:

p, span { 

  margin: 0; 

  position: absolute; 

  font-size: 3em; 

  display: block; 

}

现在,如果我们再次启动应用程序,一切都可以正常工作,而不需要使用任何key属性。

即使在不必要的情况下,通常也建议使用键,就像前面的例子一样。特别是当项目具有不同的语义含义时,这一点尤为重要。这样做的原因有几个。主要原因是,当多个人在同一行代码上工作时,修改key属性不会像将span元素切换回p元素那样容易破坏应用程序,这将破坏我们刚刚看到的过渡效果。

还有更多...

在这里,我们介绍了前面一篇文章的两个子案例:在多个元素之间切换和绑定key属性。

在多个元素之间进行过渡

我们可以以一种简单直接的方式扩展刚刚完成的示例。

假设如果我们亲吻公主/青蛙的次数太多,她会变成圣诞老人,这可能会或可能不会吸引人,这取决于你的年龄。

首先,我们添加第三个元素:

<transition name="fade"> 

  <p v-if="kisses < 3" key="frog">

frog</p> 

  <p v-else-if="kisses >= 3 && kisses <= 5" key="princess">

princess</p> 

  <p v-else key="santa">

santa</p> 

</transition>

我们可以立即启动应用程序,当我们亲吻公主/青蛙超过五次时,圣诞老人将以相同的淡入淡出过渡效果出现:

使用这个设置,我们只能使用在前两个元素之间使用的相同过渡效果。

关于此有一个解决方法,在“动态过渡”一章中有解释。

动态设置 key 属性

如果我们已经有一些可用的数据,我们不必为所有的元素编写 key。我们可以以另一种方式编写相同的应用程序,但不重复元素,如下所示:

<transition name="fade">

  <p :key="transformation">{{emoji}}{{transformation}}</p>

</transition>

当然,这意味着我们必须根据亲吻的次数为transformationemoji变量提供一个合理的值。

为了做到这一点,我们将它们绑定到计算属性上:

computed: { 

  transformation () { 

    if (this.kisses < 3) { 

      return 'frog' 

    } 

    if (this.kisses >= 3 && this.kisses <= 5) { 

      return 'princess' 

    } 

    if (this.kisses > 5) { 

      return 'santa' 

    } 

  }, 

  emoji () { 

    switch (this.transformation) { 

      case 'frog': return '

' 

      case 'princess': return '

' 

      case 'santa': return '

' 

    } 

  } 

}

我们在模板中减少了一些复杂性,增加了 Vue 实例中的一些逻辑。如果我们预计未来会有更复杂的逻辑或者转换数量增加,这样做从长远来看是有好处的。

在过渡中让元素在进入阶段之前离开

在“在元素之间过渡”这个示例中,我们探讨了如何在两个元素之间进行过渡。Vue 的默认行为是在第一个元素离开的同时开始进入的元素的过渡;但这并不总是理想的。

在本示例中,您将了解到这个重要的特殊情况以及如何解决它。

准备工作

本示例是在两个元素之间过渡的基础上构建的,解决了一个特定的问题。如果您不知道我们在谈论什么,请返回上一个示例,您很快就能理解。

操作步骤

首先,如果您还没有遇到这个问题,您将看到问题所在。接下来,我们将看到 Vue 为我们提供了什么来解决它。

两个元素的问题

让我们为我们的网站创建一个轮播效果。用户一次只能查看一个产品,然后可以滑动到下一个产品。要滑动到下一个产品,用户需要点击一个按钮。

首先,我们需要在 Vue 实例中有我们的产品列表:

new Vue({ 

  el: '#app', 

  data: { 

    product: 0, 

    products: ['

umbrella', '

computer', '

ball', '

camera'] 

  } 

})

在我们的 HTML 中,我们只需要一个按钮和一个元素的视图:

<div id="app"> 

  <button @click="product++">next</button> 

  <transition name="slide"> 

    <p :key="products[product % 4]">{{products[product % 4]}}</p> 

  </transition> 

</div>

模 4(product%4)只是因为我们希望在产品列表结束时重新开始。

为了设置我们的滑动过渡,我们需要以下规则:

.slide-enter-active, .slide-leave-active { 

  transition: transform .5s 

} 

.slide-enter { 

  transform: translateX(300px) 

} 

.slide-leave-active { 

  transform: translateX(-300px); 

}

此外,为了使一切看起来好看,我们最后加上以下内容:

p { 

  position: absolute; 

  margin: 0; 

  font-size: 3em; 

}

如果您现在运行代码,您将看到一个漂亮的轮播效果:

现在,让我们尝试从最后一个规则中删除position: absolute

p { 

  margin: 0; 

  font-size: 3em; 

}

如果您现在运行代码,您将看到产品之间出现奇怪的跳动:

这就是我们要解决的问题。第二个过渡在第一个产品离开之前就开始了。如果定位不是绝对的,我们将看到一些奇怪的效果。

过渡模式

为了解决这个问题,我们将更改过渡模式。让我们修改<transition>的代码:

<transition name="slide" mode="out-in"> 

  <p :key="products[product%4]">{{products[product%4]}}</p> 

</transition>

现在运行程序,您将看到产品在进入屏幕之前需要更长的时间滑动。它们在进入之前等待上一个项目离开。

工作原理...

总结一下,您有两种不同的方式来管理 Vue 组件之间的过渡。默认方式是同时开始“进入”过渡和“离开”过渡。我们可以通过以下方式明确表示:

<transition mode="in-out"> 

  <!-- elements --> 

</transition>

我们可以通过等待out部分完成后再开始in动画来改变这种默认行为。我们通过以下方式实现了这一点:

<transition mode="out-in"> 

  <!-- elements --> 

</transition>

当元素具有绝对定位样式时,前者很有用,而后者更适用于在页面上放置更多内容之前需要等待有一个清晰的路径的情况。

绝对定位不会在元素重叠时关心它们,因为它们不遵循页面的流动。另一方面,静态定位将在第一个元素之后添加第二个元素,如果两个元素同时显示,过渡会变得尴尬。

为列表中的元素添加进入和离开过渡

我们已经在“创建一个动态的、带动画的列表”一节中看到了动画列表;在这里,我们将尝试添加一种视觉方式来提示列表中的元素被添加或删除。这可以为用户体验增加很多,因为您有机会向用户解释为什么添加或删除了一个元素。

准备工作

如前所述,如果您已经完成了“创建一个动态的、带动画的列表”一节,那么您已经准备好了。如果您对 CSS 和过渡有一些了解,可能会有所帮助。如果您觉得有需要,可以浏览本章中的其他示例。

操作步骤...

我们将建立一个学习编程的教学大纲。当我们完成一个主题时,我们会感到宽慰,并希望通过让主题从教学大纲中漂移出去来体现这种感觉。

列表的数据将在我们的 Vue 实例中:

new Vue({ 

  el: '#app', 

  data: { 

    syllabus: [ 

      'HTML', 

      'CSS', 

      'Scratch', 

      'JavaScript', 

      'Python' 

    ] 

  } 

})

列表将在我们的 HTML 中打印出来,代码如下:

<div id="app"> 

  <h3>Syllabus</h3> 

  <ul> 

    <li v-for="topic in syllabus"> 

      {{topic}} 

    </li> 

  </ul> 

</div>

当我们按下一个按钮时,我们希望主题从列表中消失。为了实现这一点,我们需要修改我们已经编写的代码。

首先,在每个主题之前添加一个 Done 按钮:

<li v-for="topic in syllabus"> 

  <button @click="completed(topic)">Done</button>{{topic}} 

</li>

在这里,completed 方法将如下所示:

methods: { 

  completed (topic) { 

    let index = this.syllabus.indexOf(topic) 

    this.syllabus.splice(index, 1) 

  } 

}

现在运行代码将显示一个简单的应用程序,用于勾选我们已经学习过的主题。然而,我们希望有一个动画让我们感到宽慰。

为此,我们需要编辑列表的容器。我们删除<ul>标签,并告诉<transition-group>编译为<ul>标签:

<transition-group tag="ul"> 

  <li v-for="topic in syllabus" :key="topic"> 

    <button @click="completed(topic)">Done</button>{{topic}} 

  </li> 

</transition-group>

请注意,我们还根据主题为每个列表元素添加了一个键。我们需要做的最后一件事是在我们的 CSS 中添加过渡规则:

.v-leave-active { 

  transition: all 1s; 

  opacity: 0; 

  transform: translateY(-30px); 

}

现在,当点击 Done 按钮时,主题将以过渡的方式消失,如下所示:

工作原理...

<transition-group>标签表示一组同时显示的元素的容器。默认情况下,它表示<span>标签,但通过将标签属性设置为ul,我们使其表示无序列表。

列表中的每个元素必须具有唯一的键,否则过渡效果将无法工作。Vue 会负责对进入或离开的每个元素应用过渡效果。

在列表中移动的过渡元素

在这个示例中,您将构建一个根据列表变化而移动的元素列表。当您希望告诉用户某些内容已更改并且列表已相应更新时,这种特定的动画非常有用。它还将帮助用户确定插入元素的位置。

准备工作

这个示例有点高级;如果您对 Vue 中的过渡效果不太熟悉,我建议您完成本章中的一些示例。如果您可以轻松完成“为列表元素添加进入和离开过渡效果”示例,那么您就可以开始了。

操作步骤...

您将构建一个小游戏-公交车站模拟器!

每当一辆公交车(用表情符号表示)离开车站时,其他所有公交车都会稍微前进一点,以便占据它的位置。每辆公交车都有一个编号,如 Vue 实例数据所示:

new Vue({ 

  el: '#app', 

  data: { 

    buses: [1,2,3,4,5], 

    nextBus: 6 

  } 

})

每当有新的公交车到达时,它将被分配一个递增的编号。我们希望每两秒钟有一辆新的公交车离开或到达。我们可以在组件挂载到屏幕上时挂接一个定时器来实现这一点。在数据之后,写入以下内容:

mounted () { 

  setInterval(() => { 

    const headOrTail = () => Math.random() > 0.5 

    if (headOrTail()) { 

      this.buses.push(this.nextBus) 

      this.nextBus += 1 

    } else { 

      this.buses.splice(0, 1) 

    } 

  }, 2000) 

}

我们应用的 HTML 将如下所示:

<div id="app"> 

  <h3>Bus station simulator</h3> 

  <transition-group tag="p" name="station"> 

    <span v-for="bus in buses" :key="bus">

</span> 

  </transition-group> 

</div>

为了使公交车移动,我们需要在前缀 station 下指定一些 CSS 规则:

.station-leave-active, .station-enter-active { 

  transition: all 2s; 

  position: absolute; 

} 

.station-leave-to { 

  opacity: 0; 

  transform: translateX(-30px); 

} 

.station-enter { 

  opacity: 0; 

  transform: translateX(30px); 

} 

.station-move { 

  transition: 2s; 

} 

span { 

  display: inline-block; 

  margin: 3px; 

}

现在启动应用程序将导致一个有序的公交车队列,每两秒钟离开或到达一辆公交车:

工作原理...

我们应用程序的核心是<transition-group>标签。它管理着所有以其键标识的公交车:

<transition-group tag="p" name="station"> 

  <span v-for="bus in buses" :key="bus">

</span> 

</transition-group>

每当公交车进入或离开场景时,Vue 会自动触发 FLIP 动画(参见“为过渡效果添加自定义过渡类”示例)。

为了明确概念,假设我们有公交车[1, 2, 3],公交车 1 离开场景。接下来发生的是,在动画实际开始之前,将记住第一辆公交车的<span>元素的属性。因此,我们可以检索到描述属性的以下对象:

{ 

  bottom:110.4375 

  height:26 

  left:11 

  right:27 

  top:84.4375 

  width:16 

}

Vue 对<transition-group>标签内的所有键入元素都执行此操作。

在此之后,station-leave-active类将应用于第一辆公交车。让我们简要回顾一下规则是什么:

.station-leave-active, .station-enter-active { 

  transition: all 2s; 

  position: absolute; 

}

我们注意到位置变为绝对定位。这意味着元素从页面的正常流程中移除。这反过来意味着所有在它后面的公交车将突然移动以填补留下的空白空间。Vue 还记录了此阶段所有公交车的属性,这被认为是动画的最终帧。这个帧实际上不是一个真正的显示帧;它只是用作计算元素最终位置的抽象:

Vue 将计算最终帧和起始帧之间的差异,并应用样式使公交车在初始帧中显示,即使它们实际上并不在那里。这些样式将在一帧后被移除。公交车缓慢爬行到它们的最终帧位置而不是立即移动到它们的新位置的原因是它们是span元素,并且我们指定了任何转换样式(Vue 用于伪造它们的位置一帧)必须过渡两秒钟:

.station-move { 

  transition: 2s; 

}

换句话说,在帧-1 时,三辆公交车都在原位,并记录了它们的位置。

在帧 0 中,第一辆公交车从页面流程中移除,其他公交车立即移动到它后面。在同一帧中,Vue 记录它们的新位置并应用一个转换,将公交车移回到它们在帧-1 中的位置,从而看起来没有人移动。

在帧 1 中,转换被移除,但由于我们有一个过渡效果,公交车将缓慢移动到它们的最终位置。

动画化组件的状态

在计算机中,一切都是数字。在 Vue 中,一切都是数字的东西都可以以某种方式进行动画。在这个示例中,您将控制一个弹跳球,它将通过缓动动画平滑地定位自己。

准备工作

要完成这个示例,您至少需要对 JavaScript 有一些了解。JavaScript 的技术细节超出了本书的范围,但我将在“它是如何工作的...”部分为您解释代码,所以不要太担心。

操作步骤

在我们的 HTML 中,我们只会添加两个元素:一个输入框,我们将在其中输入弹跳球的期望位置,以及球本身:

<div id="app"> 

  <input type="number"> 

  <div class="ball"></div> 

</div>

为了正确渲染球,编写以下 CSS 规则,它将显示在屏幕上:

.ball { 

  width: 3em; 

  height: 3em; 

  background-color: red; 

  border-radius: 50%; 

  position: absolute; 

  left: 10em; 

}

我们想要控制条的 Y 位置。为此,我们将绑定球的top属性。

<div id="app"> 

  <input type="number"> 

  <div class="ball" :style="'top: ' + height + 'em'"></div> 

</div>

高度将成为我们 Vue 实例模型的一部分:

new Vue({ 

   el: '#app', 

   data: { 

     height: 0 

   } 

})

现在,由于我们希望每当enteredHeight发生变化时,球都能以新位置进行动画,一个想法是绑定输入元素的@change事件:

<div id="app"> 

  <input type="number" @input="move"> 

  <div class="ball" :style="'top: ' + height + 'em'"></div> 

</div>

移动方法将负责将球的当前高度缓慢过渡到指定值。

在执行此操作之前,您将添加Tween.js库作为依赖项。官方存储库位于github.com/tweenjs/tween.js。如果您使用 JSFiddle,可以添加 README.md 页面中指定的 CDN 链接。

在添加库之后添加移动方法,如下所示:

methods: { 

  move (event) { 

    const newHeight = Number(event.target.value) 

    const _this = this 

    const animate = (time) => { 

      requestAnimationFrame(animate) 

      TWEEN.update(time) 

    } 

    new TWEEN.Tween({ H: this.height }) 

      .easing(TWEEN.Easing.Bounce.Out) 

      .to({ H: newHeight }, 1000) 

      .onUpdate(function () { 

        _this.height = this.H 

      }) 

      .start() 

    animate() 

  } 

}

尝试启动应用程序并在编辑其高度时看到球弹跳:

当我们改变高度时,球的位置也会改变:

工作原理...

这里的一般原则是,您有一个元素或组件的状态。当状态是数字性质时,您可以根据特定的曲线或加速度从一个值“tween”(从中间)到另一个值。

让我们来分解代码,好吗?

首先,我们要做的是将指定的新高度保存到newHeight变量中:

const newHeight = Number(event.target.value)

在下一行中,我们还将 Vue 实例保存在_this助手变量中:

const _this = this

我们这样做的原因一分钟后就会清楚:

const animate = (time) => { 

  requestAnimationFrame(animate) 

  TWEEN.update(time) 

}

在上述代码中,我们将所有动画包装在一个函数中。这是 Tween.js 库的惯用方式,并标识我们将用于动画的主循环。如果我们有其他 tween,这是触发它们的地方:

new TWEEN.Tween({ H: this.height }) 

  .easing(TWEEN.Easing.Bounce.Out) 

  .to({ H: newHeight }, 1000) 

  .onUpdate(function () { 

    _this.height = this.H 

  }) 

.start()

这是对我们库的 API 调用。首先,我们创建一个对象,该对象将保存高度值的副本,而不是我们组件的实际值。通常,在这里放置表示状态本身的对象。由于 Vue 的限制(或 Tween.js 的限制),我们使用了一种不同的策略;我们正在动画化状态的副本,并在每一帧中同步真实状态:

Tween({ H: this.height })

第一行将此副本初始化为球的当前实际高度:

easing(TWEEN.Easing.Bounce.Out)

我们选择了类似弹跳球的缓动效果:

.to({ H: newHeight }, 1000)

此行设置目标高度和动画持续时间(以毫秒为单位):

onUpdate(function () { 

  _this.height = this.H 

})

在这里,我们将动画的高度复制回真实的元素。由于这个函数将 this 绑定到复制的状态,我们被迫使用 ES5 语法来访问它。这就是为什么我们有一个变量来引用 Vue 实例的原因。如果我们使用 ES6 语法,我们将无法直接获取H的值。

将可重用的过渡打包到组件中

我们的网站上可能有一个我们想要在用户流程中重复使用的签名过渡。如果您想要保持代码的组织性,将过渡打包到组件中可能是一个不错的策略。在这个示例中,您将构建一个简单的过渡组件。

准备工作

如果您已经通过 Vue 的过渡工作了一遍,那么按照这个示例是有意义的。此外,由于我们正在使用组件,您至少应该对它们有一个概念。浏览一下下一章,了解一下组件的基本知识。特别是,我们将创建一个功能组件,其解剖学在创建一个功能组件的示例中有详细介绍。

如何做...

我们将为新闻门户网站构建一个签名过渡。实际上,我们将使用一个预制的过渡效果,它在优秀的 magic 库(github.com/miniMAC/magic)中,所以您应该将其作为依赖项添加到您的项目中。您可以在cdnjs.com/libraries/magic找到 CDN 链接(转到该页面查找链接,不要将其复制为链接)。

首先,您将构建网站页面,然后构建过渡本身。最后,您只需将过渡添加到不同的元素中。

构建基本网页

我们的网页将由两个按钮组成,每个按钮都会显示一个卡片:一个是一个食谱,另一个是最新的突发新闻:

<div id="app"> 

  <button @click="showRecipe = !showRecipe"> 

    Recipe 

  </button> 

  <button @click="showNews= !showNews"> 

    Breaking News 

  </button> 

  <article v-if="showRecipe" class="card"> 

    <h3> 

      Apple Pie Recipe 

    </h3> 

    <p> 

      Ingredients: apple pie. Procedure: serve hot. 

    </p> 

  </article> 

  <article v-if="showNews" class="card"> 

    <h3> 

      Breaking news 

    </h3> 

    <p> 

      Donald Duck is the new president of the USA. 

    </p> 

  </article> 

</div>

这些卡片将因为以下 CSS 规则而具有独特的风格:

.card { 

  position: relative; 

  background-color: FloralWhite; 

  width: 9em; 

  height: 9em; 

  margin: 0.5em; 

  padding: 0.5em; 

  font-family: sans-serif; 

  box-shadow: 0px 0px 10px 2px rgba(0,0,0,0.3); 

}

JavaScript 部分将是一个非常简单的 Vue 实例:

new Vue({ 

  el: '#app', 

  data: { 

    showRecipe: false, 

    showNews: false 

  } 

})

运行这段代码将显示您的网页:

构建可重用的过渡

我们决定在显示卡片时,在我们的网站上使用一个过渡效果。由于我们打算在网站的所有内容中重复使用这个动画,最好将它打包到一个组件中。

在 Vue 实例之前,我们声明以下组件:

Vue.component('puff', { 

  functional: true, 

  render: function (createElement, context) { 

    var data = { 

      props: { 

        'enter-active-class': 'magictime puffIn', 

        'leave-active-class': 'magictime puffOut' 

      } 

    } 

    return createElement('transition', data, context.children) 

  } 

})

puffInpuffOut动画在magic.css中定义。

在我们页面的元素中使用我们的过渡

现在,我们只需编辑我们的网页,将<puff>组件添加到我们的卡片中:

<div id="app"> 

  <button @click="showRecipe = !showRecipe"> 

    Recipe 

  </button> 

  <button @click="showNews = !showNews"> 

    Breaking News 

  </button> 

 <puff>

    <article v-if="showRecipe" class="card"> 

      <h3> 

        Apple Pie Recipe 

      </h3> 

      <p> 

        Ingredients: apple pie. Procedure: serve hot. 

      </p> 

    </article> 

 </puff> 

 <puff>

    <article v-if="showNews" class="card"> 

      <h3> 

        Breaking news 

      </h3> 

      <p> 

        Donald Duck is the new president of the USA. 

      </p> 

    </article> 

 </puff>

</div>

当按下带有“puff”效果的按钮时,卡片将出现和消失。

工作原理...

我们代码中唯一棘手的部分是构建<puff>组件。一旦我们完成了这一步,无论我们放入其中的内容都将根据我们的过渡效果出现和消失。在我们的示例中,我们使用了一个已经制作好的过渡效果。在现实世界中,我们可能会创建一个非常复杂的动画,每次以相同的方式应用可能会很困难。将其封装在组件中更容易维护。

有两个因素使<puff>组件作为可重用的过渡效果工作:

props: { 

  'enter-active-class': 'magictime puffIn', 

  'leave-active-class': 'magictime puffOut' 

}

在这里,我们指定了组件在进入和离开时必须采用的类;这里没有什么特别的,我们已经在“与第三方 CSS 动画库集成,如 animate.css”一节中做过了。

最后,我们返回实际的元素:

return createElement('transition', data, context.children)

这一行创建了我们元素的根,即一个只有一个子元素<transition>标签--context.children。这意味着子元素未指定;组件将把实际传递给模板的子元素作为子元素。在我们的示例中,我们传递了一些卡片,它们很快就被显示出来了。

动态过渡效果

在 Vue 中,响应性是一个常见的主题,因此过渡效果可以是动态的。不仅过渡效果本身,而且它们的所有属性都可以绑定到响应式变量上。这使我们能够在任何给定的时刻控制使用哪种过渡效果。

准备工作

本示例是在“元素之间进行过渡”一节的基础上构建的。如果您已经了解过渡效果,就不需要回去了,但如果您觉得有所遗漏,最好先完成那一节。

操作步骤...

我们将用一些吻将青蛙变成公主,但如果我们吻得太多,公主将变成圣诞老人。当然,我们说的是表情符号。

我们的 HTML 设置非常简单:

<div id="app"> 

  <button @click="kisses++">

Kiss!</button> 

  <transition :name="kindOfTransformation" :mode="transformationMode"> 

    <p :key="transformation">{{emoji}}{{transformation}}</p> 

  </transition> 

</div>

只需注意这里的大多数属性都绑定到变量上。以下是 JavaScript 的实现方式。

首先,我们将创建一个带有所有数据的简单 Vue 实例:

new Vue({ 

el: '#app', 

  data: { 

    kisses: 0, 

    kindOfTransformation: 'fade', 

    transformationMode: 'in-out' 

  } 

})

我们所指的淡入淡出效果是以下 CSS:

.fade-enter-active, .fade-leave-active { 

  transition: opacity .5s 

} 

.fade-enter, .fade-leave-active { 

  opacity: 0 

}

变量 transformation 和 emoji 由两个计算属性定义:

computed: { 

  transformation () { 

    if (this.kisses < 3) { 

      return 'frog' 

    } 

    if (this.kisses >= 3 && this.kisses <= 5) { 

      return 'princess' 

    } 

    if (this.kisses > 5) { 

         return 'santa' 

    } 

  }, 

  emoji () { 

    switch (this.transformation) { 

      case 'frog': return '

' 

      case 'princess': return '

' 

      case 'santa': return '

' 

    } 

  } 

}

虽然我们在青蛙和公主之间使用了淡入淡出过渡效果,但我们希望在公主和青蛙之间使用其他过渡效果。我们将使用以下过渡效果类:

.zoom-leave-active, .zoom-enter-active { 

  transition: transform .5s; 

} 

.zoom-leave-active, .zoom-enter { 

  transform: scale(0) 

}

现在,由于我们将过渡的名称绑定到一个变量上,我们可以通过编程方式轻松地切换它。我们可以通过将以下突出显示的行添加到计算属性中来实现这一点:

transformation () { 

  if (this.kisses < 3) { 

    return 'frog' 

  } 

  if (this.kisses >= 3 && this.kisses <= 5) { 

 this.transformationMode = 'out-in'

    return 'princess' 

  } 

  if (this.kisses > 5) { 

 this.kindOfTransformation = 'zoom'

    return 'santa' 

  } 

}

第一行是为了避免在缩放过渡开始时出现重叠(有关详细信息,请参阅“在过渡中让元素在进入阶段之前离开”配方)。

第二行是将动画切换到“zoom”。

为了使一切都呈现正确的方式,我们还需要一个 CSS 规则:

p { 

  margin: 0; 

  position: absolute; 

  font-size: 3em; 

}

这样会好多了。

现在运行应用程序,看看两种不同的过渡是如何动态使用的:

随着亲吻的数量增加,公主会缩小:

通过这个,圣诞老人会放大:

工作原理...

如果你了解 Vue 中的响应性是如何工作的,就没有什么需要补充的了。我们将过渡的名称绑定到kindOfTransformation变量上,并在代码中从淡入切换到缩放。我们还演示了可以动态更改<transition>标签的其他属性。

第四章:Vue 与互联网通信

本章将涵盖以下内容:

  • 使用 Axios 发送基本的 AJAX 请求

  • 在发送数据之前验证用户数据

  • 创建表单并将数据发送到服务器

  • 在请求过程中处理错误

  • 创建 REST 客户端(和服务器!)

  • 实现无限滚动

  • 在发送请求之前处理请求

  • 防止 XSS 攻击您的应用程序

介绍

Web 应用程序很少能够独立工作。使它们有趣的实际上是它们能够以前几年不存在的创新方式与世界进行通信。

Vue 本身并不包含任何机制或库来进行 AJAX 请求或打开 Web 套接字。因此,在本章中,我们将探讨 Vue 如何与内置机制和外部库交互以连接到外部服务。

您将首先使用外部库进行基本的 AJAX 请求。然后,您将探索一些在表单中发送和获取数据的常见模式。最后,有一些适用于实际应用程序的示例以及如何构建 RESTful 客户端的方法。

使用 Axios 发送基本的 AJAX 请求

Axios是 Vue 推荐的用于进行 HTTP 请求的库。它是一个非常简单的库,但它具有一些内置功能,可以帮助您执行常见操作。它实现了使用 HTTP 动词进行请求的 REST 模式,并且还可以在函数调用中处理并发(同时生成多个请求)。您可以在github.com/mzabriskie/axios上找到更多信息。

准备工作

对于这个示例,您不需要对 Vue 有任何特殊的了解。我们将使用 Axios,它本身使用JavaScript promises。如果您从未听说过 promises,您可以在developers.google.com/web/fundamentals/getting-started/primers/promises上了解一些基础知识。

操作步骤

您将构建一个简单的应用程序,每次访问网页时都会给您一条智慧的建议。

您首先需要在应用程序中安装 Axios。如果您使用 npm,只需执行以下命令:

    npm install axios

如果您正在使用单页应用程序,可以从 CDN 导入以下文件,位于unpkg.com/axios/dist/axios.js

很不幸,我们将使用的建议单服务无法与 JSFiddle 一起使用,因为该服务运行在 HTTP 上,而 JSFiddle 运行在 HTTPS 上,您的浏览器很可能会出现错误。您可以在本地 HTML 文件上运行此示例。

我们的 HTML 如下所示:

<div id="app"> 

  <h2>Advice of the day</h2> 

  <p>{{advice}}</p> 

</div>

我们的 Vue 实例如下所示:

new Vue({ 

  el: '#app', 

  data: { 

    advice: 'loading...' 

  }, 

  created () { 

    axios.get('http://api.adviceslip.com/advice') 

      .then(response => { 

        this.advice = response.data.slip.advice 

      }) 

      .catch(error => { 

        this.advice = 'There was an error: ' + error.message 

      }) 

  } 

})

打开您的应用程序,获取一条令人耳目一新的建议:

工作原理...

当我们的应用程序启动时,将调用 created 钩子并运行带有 Axios 的代码。第一行执行一个 GET 请求到 API 端点:

axios.get('http://api.adviceslip.com/advice')

这将返回一个 promise。我们可以在任何 promise 上使用then方法来处理 promise 成功解析的结果:

.then(response => { 

  this.advice = response.data.slip.advice 

})

响应对象将包含有关我们请求结果的一些数据。可能的响应对象如下所示:

{ 

  "data": { 

    "slip": { 

      "advice": "Repeat people's name when you meet them.", 

      "slip_id": "132" 

    } 

  }, 

  "status": 200, 

  "statusText": "OK", 

  "headers": { 

    "content-type": "text/html; charset=UTF-8", 

    "cache-control": "max-age=0, no-cache" 

  }, 

  "config": { 

    "transformRequest": {}, 

    "transformResponse": {}, 

    "timeout": 0, 

    "xsrfCookieName": "XSRF-TOKEN", 

    "xsrfHeaderName": "X-XSRF-TOKEN", 

    "maxContentLength": -1, 

    "headers": { 

      "Accept": "application/json, text/plain, */*" 

    }, 

    "method": "get", 

    "url": "http://api.adviceslip.com/advice" 

  }, 

  "request": {} 

}

我们导航到我们想要交互的属性;在我们的例子中,我们想要response.data.slip.advice,这是一个字符串。我们将字符串复制到实例状态中的变量 advice 中。

最后一部分是当我们的请求或第一个分支中的代码出现问题时:

.catch(error => { 

  this.advice = 'There was an error: ' + error.message 

})

我们将在“恢复请求期间的错误”中更深入地探讨错误处理。现在,让我们手动触发一个错误,只是为了看看会发生什么。

触发错误的最便宜的方法是在 JSFiddle 上运行应用程序。由于浏览器检测到 JSFiddle 处于安全连接状态,而我们的 API 处于 HTTP 上(不安全),现代浏览器将会报错并阻止连接。您应该会看到以下文本:

There was an error: Network Error

这只是您可以尝试的许多可能错误之一。请注意,您可以将 GET 端点编辑为某个不存在的页面:

axios.get('http://api.adviceslip.com/non-existent-page')

在这种情况下,您将收到 404 错误:

There was an error: Request failed with status code 404

有趣的是,即使请求成功进行,但第一个分支中存在错误,您也将进入错误分支。

then分支更改为以下内容:

.then(response => { 

  this.advice = undefined.hello 

})

众所周知,JavaScript 无法读取未定义对象的“hello”属性:

There was an error: Cannot read property 'hello' of undefined

就像我告诉过你的那样。

在发送用户数据之前验证用户数据

一般来说,用户讨厌表单。虽然我们无法改变这一点,但我们可以通过提供有关如何填写表单的相关说明来减少用户的不便。在本示例中,我们将创建一个表单,并利用 HTML 标准为用户提供如何填写表单的良好指导。

准备工作

这个食谱不需要先前的知识就可以完成。虽然我们将构建一个表单(使用 Axios 发送基本的 AJAX 请求食谱),但我们将伪造 AJAX 调用并集中在验证上。

操作步骤如下:

我们将构建一个非常简单的表单:一个用于用户名的字段,一个用于用户电子邮件的字段,以及一个提交信息的按钮。

在 HTML 中输入以下内容:

<div id="app"> 

  <form @submit.prevent="vueSubmit"> 

    <div> 

      <label>Name</label> 

      <input type="text" required> 

    </div> 

    <div> 

      <label>Email</label> 

      <input type="email" required> 

    </div> 

    <div> 

      <label>Submit</label> 

      <button type="submit">Submit</button> 

    </div> 

  </form> 

</div>

Vue 实例非常简单,如下所示:

new Vue({ 

  el: '#app', 

  methods: { 

    vueSubmit() { 

      console.info('fake AJAX request') 

    } 

  } 

})

运行此应用程序,并尝试使用空字段或错误的电子邮件提交表单。您应该会看到浏览器本身提供的帮助:

然后,如果您尝试输入无效的电子邮件地址,您将看到以下内容:

工作原理如下:

我们使用了原生的 HTML5 验证 API,该 API 在内部使用模式匹配来检查我们输入的内容是否符合某些规则。

考虑以下行中的 required 属性:

<input type="text" required>

这确保当我们提交表单时,字段实际上是填充的,而在其他输入元素中具有type="email"确保内容类似于电子邮件格式。

此 API 非常丰富,您可以在developer.mozilla.org/en-US/docs/Web/Guide/HTML/Forms/Data_form_validation上阅读更多信息。

很多时候,问题在于要利用此 API,我们需要触发本机验证机制。这意味着我们不允许阻止提交按钮的默认行为:

<button type="submit" @click.prevent="vueSubmit">Submit</button>

这不会触发本机验证,表单将始终被提交。另一方面,如果我们执行以下操作:

<button type="submit" @click="vueSubmit">Submit</button>

表单将得到验证,但由于我们没有阻止提交按钮的默认行为,表单将被发送到另一个页面,这将破坏单页应用程序的体验。

诀窍是在表单级别拦截提交:

<form @submit.prevent="vueSubmit">

这样,我们就可以拥有表单的本机验证和我们真正喜欢的现代浏览体验。

创建一个表单并将数据发送到服务器

HTML 表单是与用户交互的标准方式。您可以收集他们的数据以在网站中注册,让他们登录,甚至进行更高级的交互。在这个食谱中,您将使用 Vue 构建您的第一个表单。

准备工作

这个食谱非常简单,但它假设您已经了解 AJAX,并且希望将您的知识应用于 Vue。

操作步骤如下:

假设我们有一个博客,我们想要写一篇新文章。为此,我们需要一个表单。以下是您布局 HTML 的方式:

<div id="app"> 

  <h3>Write a new post</h3> 

  <form> 

    <div> 

      <label>Title of your post:</label> 

      <input type="text" v-model="title"> 

    </div> 

    <div> 

      <label>Write your thoughts for the day</label> 

      <textarea v-model="body"></textarea> 

    </div> 

    <div> 

      <button @click.prevent="submit">Submit</button> 

    </div> 

  </form> 

</div>

我们有一个用于标题的框,一个用于新帖子正文的框,以及一个发送帖子的按钮。

在我们的 Vue 实例中,这三个东西以及用户 ID 将成为应用程序状态的一部分:

new Vue({ 

  el: '#app', 

  data: { 

    userId: 1, 

    title: '', 

    body: '' 

  } 

})

此时,我们只需要添加一个方法,当我们点击提交按钮时将数据发送到服务器。由于我们没有服务器,我们将使用一个非常有用的Typicode服务。它基本上是一个虚假的 REST 服务器。我们将发送一个请求,服务器将以逼真的方式响应,即使实际上什么都不会发生。

这是我们的方法:

methods: { 

  submit () { 

    const xhr = new XMLHttpRequest() 

    xhr.open('post', 'https://jsonplaceholder.typicode.com/posts') 

    xhr.setRequestHeader('Content-Type',  

                         'application/json;charset=UTF-8') 

    xhr.onreadystatechange = () => { 

    const DONE = 4 

    const CREATED = 201 

    if (xhr.readyState === DONE) { 

      if (xhr.status === CREATED) { 

          this.response = xhr.response 

        } else { 

          this.response = 'Error: ' + xhr.status 

        } 

      } 

    } 

    xhr.send(JSON.stringify({ 

      title: this.title, 

      body: this.body, 

      userId: this.userId 

    })) 

  } 

}

为了查看服务器的实际响应,我们将将响应变量添加到我们的状态中:

data: { 

  userId: 1, 

  title: '', 

  body: '', 

 response: '...'

}

在我们的 HTML 表单之后,添加以下内容:

<h3>Response from the server</h3> 

<pre>{{response}}</pre>

当您启动页面时,您应该能够与服务器进行交互。当您写一篇帖子时,服务器将回显该帖子并回答帖子 ID:

它是如何工作的...

大部分魔法发生在submit方法中。在第一行中,我们创建了一个XMLHttpRequest对象,这是一种用于进行 AJAX 请求的本机 JavaScript 机制:

const xhr = new XMLHttpRequest()

然后,我们使用opensetRequestHeader方法配置一个新的连接;我们想要发送一个 POST 请求,并且我们将随之发送一些 JSON:

xhr.open('post', 'http://jsonplaceholder.typicode.com/posts') 

xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8')

由于我们正在与 RESTful 接口交互,POST 方法意味着我们期望我们的请求修改服务器上的数据(特别是创建一个新的帖子),并且多次发出相同的请求将每次获得不同的结果(即我们将创建一个新的、不同的帖子 ID)。

这与更常见的 GET 请求不同,GET 请求不会修改服务器上的数据(除了日志之外),并且始终会产生相同的结果(前提是服务器上的数据在请求之间不会更改)。

有关 REST 的更多详细信息,请参阅*创建 REST 客户端(和服务器!)*配方。

以下行都与响应有关:

xhr.onreadystatechange = () => { 

  const DONE = 4 

  const CREATED = 201 

  if (xhr.readyState === DONE) { 

    if (xhr.status === CREATED) { 

      this.response = xhr.response 

    } else { 

      this.response = 'Error: ' + xhr.status 

    } 

  } 

}

这将在我们的对象中安装一个处理程序,每当我们的对象发生某种变化时。如果readyState更改为DONE,这意味着我们从服务器获得了响应。接下来,我们检查状态码,它应该是201,以表示已创建新资源(我们的新帖子)。如果是这样,我们设置我们放在花括号中的变量以获得快速反馈。否则,我们将接收到的错误消息放入同一变量中。

在设置事件处理程序之后,我们需要做的最后一件事是实际发送请求以及我们的新帖子的数据:

xhr.send(JSON.stringify({ 

  title: this.title, 

  body: this.body, 

  userId: this.userId 

}))

还有更多...

另一种解决相同问题的方法是使用 Axios 发送 AJAX 请求。如果你需要了解 Axios 是什么,请查看“使用 Axios 发送基本的 AJAX 请求”这个教程。

submit方法的代码将变为以下内容(记得将 Axios 添加为依赖项):

submit () { 

  axios.post('http://jsonplaceholder.typicode.com/posts', { 

    title: this.title, 

    body: this.body, 

    userId: this.userId 

  }).then(response => { 

    this.response = JSON.stringify(response,null,'  ') 

  }).catch(error => { 

    this.response = 'Error: ' + error.response.status 

  }) 

}

这段代码与使用原生浏览器对象完全等效,但比原生浏览器对象更具表达力和简洁性。

在请求过程中处理错误

从计算机的角度来看,对外部服务的请求需要很长时间。从人类的角度来看,就像是将卫星发送到木星并等待其返回地球。你无法百分之百确定旅行是否会完成,以及旅行实际需要多长时间。网络经常不稳定,最好做好准备,以防我们的请求无法成功完成。

准备工作

这个教程有点复杂,但不涉及高级概念。然而,你应该熟悉使用 Vue。

我们将在这个教程中使用 Axios。如果你不确定它具体是什么,请完成“使用 Axios 发送基本的 AJAX 请求”这个教程。

如何实现...

你将为在珠穆朗玛峰上订购披萨的网站构建一个网站。该地区的互联网连接非常差,因此在放弃订购披萨之前,我们可能希望重试几次。

这是我们的 HTML 代码:

<div id="app"> 

  <h3>Everest pizza delivery</h3> 

  <button @click="order"  

          :disabled="inProgress">Order pizza!</button> 

  <span class="spinner" v-show="inProgress">

</span> 

  <h4>Pizza wanted</h4> 

  <p>{{requests}}</p> 

  <h4>Pizzas ordered</h4> 

  <span v-for="pizza in responses"> 

    {{pizza.id}}:{{pizza.req}} 

  </span> 

</div>

我们有一个按钮用于下订单,在订单进行中时将被禁用 - 一个进行中的订单列表(目前只包含一个订单)和一个已经订购的披萨列表。

我们可以添加一个旋转的小披萨来使等待更加愉快。添加以下 CSS 代码使小披萨旋转:

@keyframes spin { 

  100% {transform:rotate(360deg);} 

} 

.spinner { 

  width: 1em; 

  height: 1em; 

  padding-bottom: 12px; 

  display: inline-block; 

  animation: spin 2s linear infinite; 

}

我们的 Vue 实例将跟踪一些事物;编写以下代码来开始构建实例:

new Vue({ 

  el: '#app', 

  data: { 

    inProgress: false, 

    requests: new Object(null), 

    responses: new Object(null), 

    counter: 0, 

    impatientAxios: undefined 

  } 

})

我想使用 JavaScript 的 set 来处理请求和响应;不幸的是,在 Vue 中,set 不是响应式的;我们可以使用最接近的对象,目前为空对象,即我们将请求和响应初始化为空对象。

impatientAxios变量将在创建时填充。通常,Axios 会等待浏览器等待响应的时间。由于我们不耐烦,我们将创建一个 Axios,在 3 秒后断开连接:

created () { 

  this.impatientAxios = axios.create({ 

    timeout: 3000  

  }) 

}

我们需要构建的最后一件事是 order 方法。由于我们没有一个 Web 服务器来进行实际的请求,我们将使用http://httpstat.us/200端点,该端点对我们的所有请求都简单地回答 200 OK。

methods: { 

  order (event, oldRequest) { 

    let request = undefined 

    if (oldRequest) { 

      request = oldRequest 

    } else { 

      request = { req: '

', id: this.counter++} 

   } 

   this.inProgress = true 

   this.requests[request.id] = request 

   this.impatientAxios.get('http://httpstat.us/200') 

    .then(response => { 

      this.inProgress = false 

      this.responses[request.id] = this.requests[request.id] 

      delete this.requests[request.id] 

    }) 

    .catch(e => { 

      this.inProgress = false 

      console.error(e.message) 

      console.error(this.requests.s) 

      setTimeout(this.order(event, request), 1000) 

    }) 

}

为了按照预期运行此程序,请在 Chrome 中打开它,并使用Cmd + Opt + I(在 Windows 上为F12)打开开发者工具:

切换到 Network 选项卡,打开下拉菜单,您会看到 No Throttling:

点击它以显示下拉菜单:

添加一个名为Everest的新自定义限速,下载和上传速度为1kb/s,延迟为1,000毫秒,如下图所示:

然后,您可以选择该类型的限速并尝试订购一些比萨。如果幸运的话,由于 Axios 的持久性,您最终应该能够订购一些比萨。

如果您没有成功或者所有的比萨都被正确订购,请尝试调整参数;这个过程的很大一部分实际上是随机的,并且高度依赖于机器。

工作原理...

处理不稳定连接的方法有很多,有许多库与 Axios 集成,并具有更高级的重试和重新尝试策略。在这里,我们只看到了一种基本策略,但是像Patience JS这样的库有更高级的策略,并且它们并不难使用。

创建一个 REST 客户端(和服务器!)

在这个示例中,我们将学习有关 REST 以及如何构建 REST 客户端。要构建一个 REST 客户端,我们需要一个公开 REST 接口的服务器;我们也将构建它。等一下!在一本关于 Vue 的书的示例中,整个 REST 服务器只是一个附注?跟着做,您不会失望的。

准备工作

从某种意义上说,这个示例相当高级,因为您需要熟悉客户端和服务器的架构,并且至少听说过或阅读过 REST 接口。您还需要熟悉命令行并安装 npm。您可以在选择开发环境示例中了解更多信息。

还需要安装 Axios;在本章的第一个示例中可以了解更多信息。

如何操作...

我记得几年前,构建一个 REST 服务器可能需要几天甚至几周的时间。您可以使用Feather.js,它将快速且(希望是)无痛。打开命令行并使用以下命令通过 npm 安装它:

    npm install -g feathers-cli

安装完成后,创建一个目录,在其中运行服务器,进入该目录,并启动 Feathers:

    mkdir my-server

    cd my-server

    feathers generate app

回答所有问题时使用默认值。当进程完成时,输入以下命令创建一个新的资源:

 feathers generate service

其中一个问题是资源的名称;将其称为messages,但除此之外,其他问题都使用默认值。

使用exit命令退出 feathers-cli,并使用以下命令启动新的服务器:

    npm start

几秒钟后,您的 REST 服务器应该已经启动,并且应该在端口3030上监听。你能诚实地说这很困难吗?

上述命令序列适用于 Feathers 版本 2.0.0。

您可能正在使用其他版本,但使用较新版本仍然可以轻松获得相同的结果;请查看在线安装指南feathersjs.com/

接下来,您将构建一个与服务器无缝通信的 Vue 应用程序。现在,由于服务器通过 HTTP 在本地环境中运行,您将无法使用 JSFiddle,因为它只支持 HTTPS,并且认为 HTTP 是不安全的。您可以使用之前描述的其他方法,或者使用 HTTP 上的服务,例如codepen.io或其他服务。

您将编写一个管理粘性消息的应用程序。我们希望能够查看、添加、编辑和删除它们。

在 HTML 中键入以下内容:

<div id="app"> 

  <h3>Sticky messages</h3> 

  <ol> 

    <li v-for="message in messages"> 

      <button @click="deleteItem(message._id)">Delete</button> 

      <button @click="edit(message._id, message.text)"> 

        edit 

      </button> 

      <input v-model="message.text"> 

    </li> 

  </ol> 

  <input v-model="toAdd"> 

  <button @click="add">add</button> 

</div>

我们的 Vue 实例状态将包含一个记录消息的列表,以及要添加到列表中的临时消息:

new Vue({ 

  el: '#app', 

  data: { 

    messages: [], 

    toAdd: '' 

  }, 

})

我们想要做的第一件事是向服务器请求消息列表。编写用于此的 created 钩子:

created () { 

  axios.get('http://localhost:3030/messages/') 

    .then(response => { 

      this.messages = response.data.data 

    }) 

},

为了创建新消息,编写一个方法,该方法绑定到添加按钮的点击事件,并将输入框中的内容发送到服务器:

methods: { 

  add () { 

    axios.post('http://localhost:3030/messages/', { 

      text: this.toAdd 

    }) 

      .then(response => { 

        if (response.status === 201) { 

          this.messages.push(response.data) 

          this.toAdd = '' 

        } 

      }) 

  } 

}

类似地,编写一个用于删除消息和编辑消息的方法:

deleteItem (id) { 

  console.log('delete') 

  axios.delete('http://localhost:3030/messages/' + id) 

    .then(response => { 

      if (response.status < 400) { 

        this.messages.splice( 

          this.messages.findIndex(e => e.id === id), 1) 

      } 

    }) 

}, 

edit (id, text) { 

  axios.put('http://localhost:3030/messages/' + id, { 

    text 

  }) 

    .then(response => { 

      if (response.status < 400) { 

        console.info(response.status) 

      } 

    }) 

}

启动应用程序,您将能够管理您的粘性消息板:

为了向自己证明您确实与服务器通信,您可以刷新页面或关闭并重新打开浏览器,您的笔记仍将存在。

工作原理...

REST表示REpresentational State Transfer,意思是您将传输某个资源状态的表示。在实践中,我们使用一组动词来传输我们消息状态的表示。

使用 HTTP 协议,我们可以使用以下动词:

动词 属性 描述
GET 幂等,安全 用于检索资源的表示
POST 用于上传新资源
PUT 幂等 用于上传现有资源(修改它)
DELETE 幂等 用于删除资源

幂等意味着如果我们使用相同的动词两次,资源不会发生任何变化,而安全则意味着根本不会发生任何变化。

在我们的应用程序中,我们只在创建过程中使用 GET 动词。当我们看到列表因其他操作而改变时,这只是因为我们在前端上镜像了服务器上的操作。

POST 动词用于向列表中添加新消息。请注意,即使在粘性消息中使用相同的文本,它也不是幂等的,因为当按下添加按钮时,我们仍然会创建一个与 ID 不同的新消息。

按下编辑按钮会触发 PUT,而删除按钮,嗯,你可以想象它使用 DELETE 动词。

Axios 通过使用动词本身命名其 API 的方法来使这一点非常清晰。

实现无限滚动

无限滚动是使用 Vue 和 AJAX 可以实现的一个很好的例子。它也非常流行,并且可以改善某些类型内容的交互。您将构建一个与无限滚动一起工作的随机单词生成器。

准备工作

我们将使用 Axios。查看“使用 Axios 发送基本 AJAX 请求”这个示例,了解如何安装它以及它的基本功能。除此之外,您不需要了解太多内容就可以跟随进行。

如何实现...

为了使我们的应用程序工作,我们将从www.setgetgo.com/randomword/get.php端点请求随机单词。每次将浏览器指向此地址时,您都会得到一个随机单词。

整个页面将仅由一个无限列表组成。编写以下 HTML:

<div id="app"> 

  <p v-for="word in words">{{word}}</p> 

</div>

随着我们向下滚动,单词列表需要增长。因此我们需要两样东西:了解用户何时到达页面底部,以及获取新单词。

为了知道用户何时到达页面底部,我们在 Vue 实例中添加一个方法:

new Vue({ 

  el: '#app', 

  methods: { 

    bottomVisible () { 

      const visibleHeight = document.documentElement.clientHeight 

      const pageHeight = document.documentElement.scrollHeight 

      const scrolled = window.scrollY 

      const reachedBottom = visibleHeight + scrolled >= pageHeight 

      return reachedBottom || pageHeight < visibleHeight 

    } 

  } 

})

如果页面滚动到底部或页面本身小于浏览器,则返回true

接下来,我们需要添加一个机制,将此函数的结果绑定到状态变量bottom并在用户滚动页面时更新它。我们可以在created钩子中实现这一点:

created () { 

  window.addEventListener('scroll', () => { 

    this.bottom = this.bottomVisible() 

  }) 

}

状态将由bottom变量和随机单词列表组成:

data: { 

  bottom: false, 

  words: [] 

}

现在我们需要一个方法来将单词添加到数组中。将以下方法添加到现有方法中:

addWord () { 

  axios.get('http://www.setgetgo.com/randomword/get.php') 

    .then(response => { 

      this.words.push(response.data) 

      if (this.bottomVisible()) { 

        this.addWord() 

      } 

    }) 

}

该方法将递归调用自身,直到页面有足够的单词填满整个浏览器视图。

由于这种方法需要在到达底部时调用,我们将观察底部变量,并在其为true时触发该方法。在 Vue 实例的data之后添加以下选项:

watch: { 

  bottom (bottom) { 

    if (bottom) { 

      this.addWord() 

    } 

  } 

}

我们还需要在created钩子中调用addWord方法来启动页面:

created () { 

  window.addEventListener('scroll', () => { 

    this.bottom = this.bottomVisible() 

  }) 

 this.addWord()

}

如果现在启动页面,您将获得一个无限流的随机单词,这在您需要创建新密码时非常有用!

工作原理...

在这个示例中,我们使用了一个名为watch的选项,它使用以下语法:

watch: { 

 'name of sate variable' (newValue, oldValue) { 

   ... 

  } 

}

这是计算属性的对应物,当我们对一些响应式变量的更改后不感兴趣时使用。实际上,我们只是用它来触发另一个方法。如果我们对一些计算结果感兴趣,我们将使用计算属性。

在发送请求之前处理请求

本示例将教您如何使用拦截器在请求发送到互联网之前编辑请求。在某些情况下,这可能非常有用,例如当您需要在所有请求到服务器的请求中提供授权令牌时,或者当您需要一个单一点来编辑 API 调用的执行方式时。

准备工作

本示例使用了 Axios(使用 Axios 发送基本 AJAX 请求示例);除此之外,完成在发送用户数据之前验证用户数据的方法示例将非常有用,因为我们将构建一个小型表单进行演示。

操作步骤...

在本示例中,您将为一个假设的评论系统构建一个脏话过滤器。假设我们的网站上有一篇文章可能引发争论:

<div id="app"> 

  <h3>Who's better: Socrates or Plato?</h3> 

  <p>Technically, without Plato we wouldn't have<br> 

  much to go on when it comes to information about<br> 

  Socrates. Plato ftw!</p>

在那篇文章之后,我们放置了一个评论框:

  <form> 

    <label>Write your comment:</label> 

    <textarea v-model="message"></textarea> 

    <button @click.prevent="submit">Send!</button> 

  </form> 

  <p>Server got: {{response}}</p> 

</div>

我们还在表单之后添加了一行用于调试从服务器获取的响应。

在我们的 Vue 实例中,我们编写了所有支持代码将评论发送到我们的服务器,这里的服务器将是jsonplaceholder.typicode.com/comments,一个行为类似真实服务器的虚拟 REST 接口。

这是在按下提交按钮时触发的提交方法:

methods: { 

  submit () { 

    axios.post('http://jsonplaceholder.typicode.com/comments', 

    { 

      body: this.message 

    }).then(response => { 

      this.response = response.data 

    }) 

  } 

}

Vue 实例的状态只有两个变量:

data: { 

  message: '', 

  response: '...' 

}

像往常一样,我们希望将其挂载到<div>应用程序中:

new Vue({ 

  el: '#app', 

...

一旦实例被挂载,我们希望在 Axios 中安装单词过滤器;为此,我们利用 Vue 的mounted钩子:

mounted () { 

  axios.interceptors.request.use(config => { 

    const body = config.data.body.replace(/punk/i, '***') 

    config.data.body = body 

    return config 

  }) 

}

现在我们可以启动应用程序并尝试编写我们的脏话评论:

工作原理...

mounted钩子中,我们正在安装一个所谓的拦截器。特别是,它是一个请求拦截器,这意味着它会在将请求发送到互联网之前对其进行操作:

axios.interceptors.request.use(config => { 

  const body = config.data.body.replace(/punk/i, '***') 

  config.data.body = body 

  return config 

})

config对象包含许多我们可以编辑的内容。它包含头部和 URL 参数。它还包含 Axios 配置变量。您可以查看 Axios 文档以获取最新列表。

我们正在获取与 POST 请求一起发送的数据部分,并检查是否找到了punk这个词。如果是这样,它将被星号替换。返回的对象将成为当前请求的新配置。

防止 XSS 攻击您的应用程序

编写没有考虑安全性的应用程序将不可避免地导致漏洞,特别是如果它必须在 Web 服务器上运行。跨站脚本攻击(XSS)是当今最流行的安全问题之一;即使您不是安全专家,也应该了解它的工作原理以及如何在 Vue 应用程序中防止它。

准备工作

除了 Axios 之外,这个示例不需要任何先前的知识。您可以在使用 Axios 发送基本 AJAX 请求的示例中找到有关 Axios 以及如何安装它的更多信息。

操作步骤...

您应该首先了解后端如何提供 CSRF 令牌(在下一段中会详细介绍)。我们假设服务器会在您的浏览器中放置一个名为 XSRF-TOKEN 的 cookie。

您可以模拟您的服务器,在浏览器控制台(开发者工具)中使用document.cookie = 'XSRF-TOKEN=abc123'命令设置一个 cookie。

Axios 会自动读取这样的 cookie,并在下一次请求中传输它。

考虑到我们在代码中调用了一个 Axios 的get请求,如下所示:

methods: { 

  sendAllMoney () { 

    axios.get('/sendTo/'+this.accountNo) 

  } 

}

Axios 将获取该 cookie 并添加一个名为 X-XSRF-TOKEN 的新头部到请求中。您可以通过在 Chrome 的开发者工具中点击请求的名称,在网络选项卡中查看这样的头部:

工作原理...

为了防止 XSS 攻击,您必须确保没有用户输入可以出现在您的应用程序中作为代码。这意味着您必须非常小心地使用v-html属性(输出原始 HTML的示例)。

不幸的是,您无法控制页面外发生的事情。如果您的用户收到一封包含与您的应用程序中的操作相对应的链接的虚假电子邮件,点击电子邮件中的链接将触发该操作。

让我们举一个具体的例子;你开发了一个银行应用程序VueBank,你的应用程序的用户收到了以下虚假电子邮件:

Hello user!

Click here to read the latest news.

正如你所看到的,这封邮件甚至与我们的应用程序无关,here超链接隐藏在邮件本身的 HTML 中。实际上,它指向http://vuebank.com?give_all_my_money_to_account=754839534地址。

如果我们已经登录了 VueBank,该链接可能会立即生效。这对我们的财务状况不利。

为了防止这类攻击,我们应该让后端为我们生成一个CSRF跨站请求伪造)令牌。我们将获取该令牌并将其与请求一起发送,以证明该请求来自用户。前面的链接将变为

http://vuebank.com?give_all_my_money_to_account=754839534&csrf=s83Rnj

由于令牌每次都是随机生成的,邮件中的链接无法正确伪造,因为攻击者不知道服务器给网页的令牌。

在 Vue 中,我们使用 Axios 发送令牌。通常,我们不会将其作为链接的一部分发送,而是作为请求的头部;实际上,Axios 会为我们执行此操作,并自动将令牌放入下一个请求中。

您可以通过设置axios.defaults.xsrfCookieName变量来更改 Axios 将获取的 cookie 的名称,并且可以通过操作axios.defaults.xsrfHeaderName变量来编辑返回令牌的头部的名称。

第五章:单页应用程序

在本章中,将涵盖以下内容:

  • 使用 vue-router 创建 SPA

  • 在切换路由之前获取数据

  • 使用命名动态路由

  • 在页面中使用多个 router-view

  • 按层次组织路由

  • 使用路由别名

  • 在路由之间添加过渡效果

  • 管理路由错误

  • 在加载页面时添加进度条

  • 如何重定向到另一个路由

  • 在返回时保存滚动位置

介绍

许多现代应用程序都基于 SPA(单页应用程序)模型。从用户的角度来看,这意味着整个网站在单个页面上看起来类似于一个应用程序。

这是好的,因为如果做得正确,它可以增强用户体验,主要是减少等待时间,因为没有新页面需要加载-整个网站都在一个页面上。这就是 Facebook、Medium、Google 和许多其他网站的工作方式。

URL 不再指向 HTML 页面,而是指向应用程序的特定状态(通常看起来像不同的页面)。在实践中,在服务器上,假设您的应用程序位于index.html页面中,这是通过将请求“关于我”的用户重定向到index.html来实现的。

后者页面将采用 URL 的后缀,并将其解释为路由,而路由将创建一个类似页面的组件,其中包含个人简介信息。

使用 vue-router 创建 SPA

Vue.js 通过其核心插件 vue-router 实现了 SPA 模式。对于 vue-router,每个路由 URL 对应一个组件。这意味着我们将告诉 vue-router 当用户访问特定 URL 时如何行为,以其组件为基础。换句话说,在这个新系统中,每个组件都是旧系统中的一个页面。

准备工作

对于这个示例,您只需要安装 vue-router 并对 Vue 组件有一些了解。

要安装 vue-router,请按照以下说明操作:router.vuejs.org/en/installation.html

如果您正在使用 JSFiddle 进行跟随,可以添加类似于unpkg.com/vue-router/dist/vue-router.js的链接。

操作步骤

我们正在为一家餐厅准备一个现代化的网站,并将使用 SPA 模式。

该网站将包括三个页面:主页、餐厅菜单和酒吧菜单。

整个 HTML 代码将如下所示:

<div id="app">

  <h1>Choppy's Restaurant</h1>

  <ul>

    <li>Home</li>

    <li>Menu</li>

    <li>Bar</li>

  </ul>

  <router-view></router-view>

</div>

<router-view>组件是 vue-router 的入口点。它是组件显示为页面的地方。

列表元素将变成链接。目前,它们只是列表元素;要将它们转换为链接,我们可以使用两种不同的语法。将第一个链接包装在以下行中:

<li><router-link to="/">

Home</router-link>

</li>

另一个示例如下:

<li><router-link to="/menu">

Menu</router-link>

</li>

我们可以使用的另一种语法是以下内容(用于 Bar 链接):

<li>

  <router-link

    tag="li" to="/bar"

      :event="['mousedown', 'touchstart']"

    >

    <a>Bar</a>

  </router-link>

</li>

这种更冗长但更明确的语法可以用于将自定义事件绑定到特定的路由。

要告诉 Vue 我们要使用 vue-router 插件,请在 JavaScript 中写入以下内容:

Vue.use(VueRouter)

我们在开始时列出的三个页面的部分将由这三个虚拟组件扮演(将它们添加到 JavaScript 中):

const Home = { template: '<div>Welcome to Choppy's</div>' }

const Menu = { template: '<div>Today we have cookies</div>' }

const Bar = { template: '<div>We serve cocktails</div>' }

现在,您终于可以创建路由器了。其代码如下:

const router = new VueRouter({})

这个路由器没有做太多事情;我们必须添加路由(对应于 URL)及其关联的组件:

const router = new VueRouter({

 routes: [ 

 { path: '/', component: Home }, 

 { path: '/menu', component: Menu }, 

 { path: '/bar', component: Bar } 

 ] 

})

现在我们的应用程序几乎完成了;我们只需要声明一个简单的Vue实例:

new Vue({

  router,

  el: '#app'

})

我们的应用程序现在可以工作了;在启动之前,添加此 CSS 规则以获得稍微更好的反馈:

a.router-link-active, li.router-link-active>a {

  background-color: gainsboro;

}

当您打开应用程序并点击 Bar 链接时,您应该看到类似以下屏幕截图的内容:

它是如何工作的...

您的程序首先要做的是将 vue-router 注册为插件。vue-router 反过来注册路由(它们是 URL 的一部分)并将组件连接到每个路由。

当我们首次访问应用程序时,浏览器上的 URL(您无法在 JSFiddle 中看到它在更改,因为它在 iframe 中)将以index.html/#/结尾。井号后面的所有内容都是 vue-router 的路由。在这种情况下,它只是一个斜杠(/),因此它与第一个主页路由匹配。

当我们点击链接时,<router-view>的内容会根据我们与该路由关联的组件而更改。

还有更多...

敏锐的读者肯定会发现可以解释为错误的问题-在运行应用程序之前,我们添加了一些 CSS 样式。.router-link-active类会在<router-link>组件中自动注入,每当页面对应于实际指向的链接时。

当我们点击“菜单”和“栏目”时,背景颜色会发生变化,但似乎仍然会停留在“主页”链接上。这是因为<router-link>组件执行的匹配不是精确的。换句话说,/bar/menu包含/字符串,因此/总是匹配的。

一个快速修复的方法是添加与第一个<router-link>完全相同的属性:

<li><router-link to="/" exact

>Home</router-link></li>

现在,只有当路由完全匹配主页链接时,“主页”链接才会被突出显示。

还要注意的一点是规则本身:

a.router-link-active, li.router-link-active>a {

  background-color: gainsboro;

}

为什么我们要匹配两个不同的东西?这取决于你如何编写路由链接。

<li><router-link to="/" exact>Home</router-link></li>

前面的代码将被翻译为以下 DOM 部分:

<li><a href="#/" class="router-link-active"

>

Home</a></li>

而:

<router-link tag="li" to="/" exact>Home</router-link>

变成:

<li class="router-link-active"

>Home</li>

请注意,在第一种情况下,类被应用于子锚点元素;在第二种情况下,它被应用于父元素。

在切换路由之前获取数据

在 Vue 的早期版本中,我们有一个专门的方法来从互联网上获取数据,然后再切换路由。在 Vue 2 中,我们有一个更通用的方法来处理这个问题,以及在切换路由之前可能需要处理的其他事情。

准备工作

为了完成这个示例,您需要已经了解 vue-router 的基础知识以及如何进行 AJAX 请求(在最后一章中会详细介绍)。

如何做到这一点…

我们将编写一个由两个页面组成的简单网页作品集:一个主页和一个关于我页面。

为了完成这个示例,我们需要将 Axios 添加为依赖项。

基本布局可以从以下 HTML 代码中清楚地看出:

<div id="app">

  <h1>My Portfolio</h1>

  <ul>

    <li><router-link to="/" exact>Home</router-link></li>

    <li><router-link to="/aboutme">About Me</router-link></li>

  </ul>

  <router-view></router-view>

</div>

在 JavaScript 中,您可以开始构建您的AboutMe组件:

const AboutMe = {

  template: `<div>Name:{{name}}<br>Phone:{{phone}}</div>`

}

它只显示一个姓名和一个电话号码。让我们在组件的data选项中声明这两个变量,如下所示:

data () {

  return {

    name: undefined,

    phone: undefined  

  } 

}

在实际加载组件到场景之前,vue-router 将在我们的对象中查找一个名为beforeRouteEnter的选项;我们将使用它来从服务器加载姓名和电话号码。我们使用的服务器将提供一些虚假数据,仅用于显示,如下所示:

beforeRouteEnter (to, from, next) {

  axios.post('https://schematic-ipsum.herokuapp.com/', {

    "type": "object",

    "properties": {

      "name": {

        "type": "string",

        "ipsum": "name"

      },

      "phone": {

        type": "string",

        "format": "phone"

      }

    }

  }).then(response => {

    next(vm => {

      vm.name = response.data.name

      vm.phone = response.data.phone 

    })

  })

}

对于另一个组件,主页,我们只需编写一个小组件作为占位符:

const Home = { template: '<div>This is my home page</div>' }

接下来,您需要注册router及其paths

Vue.use(VueRouter)

const router = new VueRouter({

  routes: [

    { path: '/', component: Home },

    { path: '/aboutme', component: AboutMe },  

  ] 

})

当然,您还需要注册一个Vue根实例,如下所示:

new Vue({

  router,

  el: '#app'

})

当您启动应用程序并点击“关于我”链接时,您应该看到类似于以下内容的东西:

请注意,当您点击链接时,页面不会重新加载,但显示个人简介仍然需要一些时间。这是因为它正在从互联网获取数据。

工作原理如下:

beforeRouteEnter钩子函数接受三个参数:

  • to:这是一个表示用户请求的路由的Route对象。

  • from:这也是一个表示当前路由的Route对象。在出现错误时,用户将保留在该路由上。

  • next:这是一个函数,当我们准备继续切换路由时可以使用它。如果使用 false 调用此函数,将阻止路由的更改,在出现错误时非常有用。

当调用上述函数时,我们使用 Axios 调用了一个 Web 服务,该服务提供了一个名称字符串和一个电话号码字符串。

当我们在此钩子函数内部时,重要的是要记住我们无法访问this。这是因为此钩子函数在组件实际实例化之前运行,因此没有this可供引用。

当服务器响应时,我们在then函数内部,并且希望将服务器返回的名称和电话赋值给变量,但是正如前面所说,我们无法访问this。next 函数接收到我们的组件的引用作为参数。我们使用它来将变量设置为接收到的值:

...

}).then(response => {

  next(vm => {

    vm.name = response.data.name

    vm.phone = response.data.phone

  })

})

使用命名动态路由

手动注册所有路由可能会耗费时间,并且当路由事先未知时,这是不可能的。vue-router 允许您使用参数注册路由,以便您可以为数据库中的所有对象创建链接,并覆盖其他用户选择路由的用例,遵循某种模式,这将导致需要手动注册太多的路由。

准备工作

除了 vue-router 的基础知识(参考“使用 vue-router 创建单页应用程序”配方),您不需要任何其他信息来完成此配方。

操作步骤如下:

我们将开设一个在线餐厅,提供十种不同的菜肴。我们将为每道菜创建一个路由。

我们网站的 HTML 布局如下:

<div id="app">

  <h1>Online Restaurant</h1>

  <ul>

    <li>

      <router-link :to="{ name: 'home' }" exact>

        Home

      </router-link>

    </li>

    <li v-for="i in 10">

      <router-link :to="{ name: 'menu', params: { id: i } }">

        Menu {{i}}

      </router-link>

    </li>

    </ul>

  <router-view class="view"></router-view>

</div>

这将创建 11 个链接,一个用于主页,十个用于菜肴。

在 JavaScript 部分注册VueRouter之后,代码如下:

Vue.use(VueRouter)

创建两个组件;一个将作为主页的占位符:

const Home = { template: `

  <div>

    Welcome to Online Restaurant

  </div>

` }

其他路由将连接到一个Menu组件:

const Menu = { template: `

  <div>

    You just ordered

    <img :src="'http://lorempixel.com/200/200/food/' + $route.params.id">

  </div>

` }

在前面的组件中,我们使用$route引用全局路由对象,并从 URL 中获取id参数。Lorempixel.com是一个提供示例图片的网站。我们为每个id连接不同的图片。

最后,使用以下代码创建路由本身:

const router = new VueRouter({

  routes: [

    { path: '/', name:'home', component: Home }, 

    { path: '/menu/:id', name: 'menu', component: Menu },

  ]

})

你可以看到菜单的路径包含/:id,这是一个占位符,用于表示 URL 中的id参数。

最后,编写一个根Vue实例:

new Vue({

  router,

  el: '#app'

})

现在你可以启动应用程序,应该能够看到所有的菜单项。点击其中任何一个应该会点菜:

工作原理...

代码中有两个主要部分贡献于创建不同菜品的路由。

首先,我们使用冒号语法注册了一个通用路由,并为其指定了一个名称,代码如下:

{ path: '/menu/:id', name: 'menu', component: Menu }

这意味着我们可以有一个以/menu/82结尾的 URL,Menu组件将显示,并且$route.params.id变量将设置为82。所以,以下行应该根据以下内容进行更改:

<img :src="'http://lorempixel.com/200/200/food/' + $route.params.id">

在渲染的 DOM 中,上述行将被以下行替换:

<img src="'http://lorempixel.com/200/200/food/82">

不要在现实生活中寻找这样的图片。

请注意,我们还为此路由指定了一个名称。这并不是必需的,但它使我们能够编写代码的第二个主要部分,如下所示:

<router-link :to="{ name: 'menu', params: { id: i } }

">

  Menu {{i}}

</router-link>

我们可以传递一个对象给to属性,而不是写一个字符串,并指定params。在我们的例子中,参数由v-for包装给出。这意味着,例如,在v-for的第四个循环中:

<router-link :to="{ name: 'menu', params: { id: 4} }">

  Menu 4

</router-link>

这将导致 DOM 如下所示:

<a href="#/menu/4" class="">Menu 4</a>

在页面中有多个<router-view>

拥有多个<router-view>可以让您拥有可以使用更复杂布局组织的页面。例如,您可以拥有侧边栏和主视图。本篇介绍了这方面的内容。

准备工作

本篇不使用任何高级概念。但建议您熟悉 vue-router 并学习如何安装它。请参阅本章的第一篇文章以了解更多信息。

操作步骤

本篇将使用大量代码来说明问题。但是,机制非常简单。

我们将构建一个二手硬件商店。我们将拥有一个主视图和一个侧边栏;这些将是我们的router-view。侧边栏将包含我们的购物清单,以便我们始终知道我们要购买的物品,并且没有干扰。

整个 HTML 代码非常简短,因为它只包含一个标题和两个router-view组件:

<div id="app">

  <h1>Second-Hand Hardware</h1>

    <router-view name="list"></router-view>

    <router-view></router-view>

</div>

在这种情况下,列表被命名为router-view。第二个没有名称,因此默认命名为Vue

在 JavaScript 中注册vue-router

Vue.use(VueRouter)

之后,注册路由:

const router = new VueRouter({

  routes: [

    { path: '/',

      components: {

        default: Parts,

        list: List

      }

    },

    { path: '/computer',

      components: {

        default: ComputerDetail,

        list: List

      }

    }

  ]

})

组件不再是一个单独的对象,而是一个包含两个组件的对象:一个用于list,另一个用于默认的router-view

按照示例编写list组件,放在路由代码之前:

const List = { template: `

  <div>

    <h2>Shopping List</h2>

      <ul>

        <li>Computer</li>

      </ul>

  </div>

` }

这将只显示计算机作为我们应该记得购买的项目。

部件组件如下所示;在路由代码之前编写它:

const Parts = { template: `

  <div>

    <h2>Computer Parts</h2>

    <ul>

      <li><router-link to="/computer">Computer</router-link></li>

      <li>CD-ROM</li>

    </ul>

  </div>

` }

它包含一个链接,用于查看有关正在销售的计算机的更多信息;下一个组件绑定到该页面,因此在路由代码之前编写它:

const ComputerDetail = { template: `

  <div>

    <h2>Computer Detail</h2>

    <p>Pentium 120Mhz, CDs sold separately</p>

  </div>

` }

当然,不要忘记添加Vue实例:

new Vue({

  router,

  el: '#app'

})

启动应用程序时,您应该看到两个路由视图一个在另一个上方。如果您希望它们并排显示,可以添加一些 CSS 样式:

工作原理...

在将<router-view>组件添加到页面时,您只需记住为其添加一个名称以便在路由注册期间引用:

<router-view name="view1"></router-view>

<router-view name="view2"></router-view>

<router-view></router-view>

如果不指定名称,路由将被称为默认路由:

routes: [

  { path: '/',

    components: {  

      default: DefaultComponent,

      view1: Component1,

      view2: Component2

    }

  }

]

这样,组件将显示在各自的router-view元素中。

如果未为命名视图指定一个或多个组件,则与该名称关联的router-view将为空。

按层次组织您的路由

在许多情况下,您的网站的组织树可能很复杂。在某些情况下,可能存在明确的分层组织,您可以遵循并使用嵌套路由,vue-routes 可以帮助您保持一切井然有序。最好的情况是,URL 的组织方式与组件的嵌套方式完全对应。

准备工作

在本示例中,您将使用 Vue 的组件和其他基本功能。您还将使用动态路由。请参阅使用命名动态路由示例以了解更多信息。

操作步骤...

在本示例中,您将为一个虚构的世界构建一个在线会计网站。我们将有两个用户-StarkLannister-我们将能够看到这两个用户拥有多少黄金和士兵。

我们网站的 HTML 布局如下:

<div id="app">

  <h1>Kindoms Encyclopedia</h1>

  <router-link to="/user/Stark/">Stark</router-link>

  <router-link to="/user/Lannister/">Lannister</router-link>

  <router-view></router-view>

</div>

我们有一个标题和两个链接-一个用于Stark,一个用于Lannister-最后是router-view元素。

我们将VueRouter添加到插件中:

Vue.use(VueRouter)

然后,我们注册路由:

const router = new VueRouter({

  routes: [

    { path: '/user/:id', component: User,

      children: [ 

        {

          path: 'soldiers',

          component: Soldiers

        },

        {

          path: 'gold',

          component: Gold

        }

      ]

    }

  ]

})

我们所说的是注册一个动态路由/user/:id,在User组件内部,将有另一个router-view,其中包含金币和士兵的嵌套路径。

刚才提到的三个组件按照所示编写,在路由代码之前添加它们:

const User = { template: `

  <div class="user">

    <h1>Kindoms Encyclopedia</h1>

    User {{$route.params.id}}

    <router-link to="gold">Gold</router-link>

    <router-link to="soldiers">Soldiers</router-link>

    <router-view></router-view>

  </div>

`}

正如预期的那样,在User组件内部还有另一个router-view入口,其中包含嵌套的routes组件。

然后,在路由代码之前编写SoldiersGold组件:

const Soldiers = { template: `

  <div class="soldiers">

    <span v-for="soldier in $root[$route.params.id].soldiers"> 

    </span>

  </div>

`}

const Gold = { template: `

   div class="gold">

    <span v-for="coin in $root[$route.params.id].gold">

    </span>

  </div>

`}

这些组件将根据 Vue 根实例数据选项中的金币或士兵变量显示相应数量的表情符号。

这是 Vue 根实例的样子:

new Vue({

  router,

  el: '#app',

  data: {

    Stark: {

      soldiers: 100,

      gold: 50  

    },

    Lannister: {

      soldiers: 50,

      gold: 100

    }

  }

})

启动应用程序将使您能够直观地表示两个用户的金币和士兵数量:

工作原理...

为了更好地理解嵌套路由的工作原理,可以看一下以下图表:

我们的示例中只有两个级别。第一个级别是顶级,由大的包裹矩形表示,对应于/user/:id路由,意味着每个可能匹配的 ID 都在同一级别上。

而内部矩形则是一个嵌套路由和嵌套组件。它对应于金币路由和 Gold 组件。

当嵌套路由对应于嵌套组件时,这是正确的选择。还有两种其他情况需要考虑。

当我们有嵌套组件但没有嵌套路由时,我们只需在嵌套路由前加上斜杠/。这将使其表现得像顶级路由。

例如,考虑将我们的代码更改为以下内容:

const router = new VueRouter({

  routes: [

    { path: '/user/:id', component: User,

      children: [

        {

          path: 'soldiers',

          component: Soldiers

        },

        {

          path: '/gold'

,

          component: Gold

        }

      ] 

    }

  ]

})

/gold路由前加上斜杠将使Gold组件在我们将浏览器指向/goldURL 时出现,而不是/user/Lannister/gold(在这种情况下将导致错误和空白页面,因为未指定用户)。

另一种相反的情况是有嵌套路由但没有相同级别的组件。在这种情况下,只需使用常规语法注册路由。

使用路由别名

有时需要有多个指向同一页的 URL。这可能是因为页面更改了名称,或者因为页面在站点的不同部分中有不同的引用方式。

特别是当页面更改名称时,也非常重要在许多设置中保留以前的名称。链接可能会断开,页面可能无法从网站的某些部分访问。在这个示例中,您将防止出现这种情况。

准备工作

对于这个示例,您只需要对 vue-router 组件有一些了解(如何安装和基本操作)。有关 vue-router 的更多信息将从“使用 vue-router 创建 SPA”示例开始。

操作步骤

假设我们有一个时尚网站,负责给服装命名的员工 Lisa 为两件衣服创建了两个新链接:

<router-link to="/green-dress-01/">Valentino</router-link>

<router-link to="/green-purse-A2/">Prada</router-link>

开发人员在 vue-router 中创建了相应的路由:

const router = new VueRouter({

  routes: [

    {

      path: '/green-dress-01',

      component: Valentino01

    },

    {

      path: '/green-purse-A2',

      component: PradaA2

    }

  ]

})

后来发现这两件衣服不是绿色的,而是红色的。Lisa 并不怪罪,因为她是色盲。

现在您负责更改所有链接以反映列表的真实颜色。首先要做的是更改链接本身。在您编辑后,HTML 布局如下所示:

<div id="app">

  <h1>Clothes Shop</h1>

  <router-link to="/red-dress-01/">Valentino</router-link>

  <router-link to="/red-purse-A2/">Prada</router-link>

  <router-view></router-view>

</div>

您将VueRouter插件添加到Vue中:

Vue.use(VueRouter)

然后,注册新的routes以及旧路由的别名:

const router = new VueRouter({

  routes: [

    {

      path: '/red-dress-01',

      component: Valentino01,

      alias: '/green-dress-01'

    },

    {

      path: '/red-purse-A2',

      component: PradaA2,

      alias: '/green-purse-A2'

    }

  ]

})

以下是所提到的组件的样子:

const Valentino01 = { template: '<div class="emoji">

</div>' }

const PradaA2 = { template: '<div class="emoji">

</div>' }

在启动应用程序之前,请记得实例化一个Vue实例:

new Vue({

  router,

  el: '#app'

})

您可以添加一个 CSS 规则,使表情符号看起来像图片,如下面的屏幕截图所示:

.emoji {

  font-size: 3em;

}

工作原理

即使我们更改了所有链接,我们也无法控制其他实体如何链接到我们的页面。对于搜索引擎(如 Google),没有办法告诉它们删除对旧页面的链接并使用新页面。这意味着如果我们不使用别名,我们可能会遇到大量的破损链接和 404 页面,甚至可能来自我们支付费用链接到不存在页面的广告商的负面宣传。

在路由之间添加过渡效果

我们在“第三章”中详细探讨了过渡效果和动画。在这里,我们将在更改路由时使用它们,而不是更改元素或组件。同样的观察也适用于这里。

准备工作

在尝试这个示例之前,我强烈建议您完成“第三章”中的一些示例,即“过渡和动画”,以及本示例。这个示例是到目前为止学到的概念的混合体。

操作步骤

在这个教程中,我们将为一个餐厅的网站建立一个鬼魂餐厅的网站。除了页面必须淡出而不是立即出现的要求外,它与普通餐厅的网站没有太大的区别。

让我们先来布置一些 HTML 布局:

<div id="app">

  <h1>Ghost's Restaurant</h1>

  <ul>

    <li><router-link to="/">Home</router-link></li>

    <li><router-link to="/menu">Menu</router-link></li>  

  </ul>

  <transition mode="out-in">

  <router-view></router-view>

  </transition>

</div>

注意我们如何用transition标签包裹了主路由显示端口。设置out-in模式是因为我们希望消失的组件的动画在其他组件出现之前完成。如果我们没有设置这个,两个淡出的组件会在短暂的时间内叠加在一起。有关更详细的讨论,您可以参考在过渡中让元素在进入阶段之前离开的教程。

现在,让我们创建两个页面/组件:

const Home = { template: '<div>Welcome to Ghost's</div>' }

const Menu = { template: '<div>Today: invisible cookies</div>' }

现在,让我们注册routes

Vue.use(VueRouter)

const router = new VueRouter({

  routes: [

    { path: '/', component: Home },

    { path: '/menu', component: Menu }

  ]

})

在启动应用程序之前,实例化一个Vue对象:

new Vue({

  router,

  el: '#app'

})

为了使过渡效果生效,您需要添加一些 CSS 规则:

.v-enter-active, .v-leave-active {

  transition: opacity .5s;

}

.v-enter, .v-leave-active {

  opacity: 0

}

现在启动您的应用程序。您成功地在页面切换之间添加了淡入淡出的过渡效果。

工作原理...

将整个<router-view>包装在一个过渡标签中将为所有组件执行相同的过渡效果。

如果我们想为每个组件设置不同的过渡效果,我们有另一种选择:我们必须将各个组件自己包装在过渡中。

例如,假设我们有两个过渡效果:spooky 和 delicious。我们希望在Home组件出现时应用第一个过渡效果,在Menu组件出现时应用第二个过渡效果。

我们需要修改我们的组件,如下所示:

const Home = { template: `

  <transition name="spooky">

    <div>Welcome to Ghost's</div>

  </transition>

` }

const Menu = { template: `

  <transition name="delicious">

    <div>Today: insisible cookies!</div>

  </transition>

` }

管理路由错误

如果我们访问的页面不存在或无法正常工作,那么前往链接就没有太多意义。传统上,当发生这种情况时,我们会看到一个错误页面。在 SPA 中,我们更强大,我们可以完全阻止用户前往那里,并显示一个礼貌的消息,说明页面不可用。这极大地增强了用户体验,因为用户可以立即采取其他操作,而无需返回。

准备工作

为了跟上,您应该完成在切换路由之前获取数据的教程。

本教程将在此基础上进行构建,并假设您已经将所有相关代码放置在适当的位置。

如何操作...

正如前面所说,我们将编辑在切换路由之前获取数据教程的结果代码来管理错误。只是为了提醒您,当我们转到/aboutme页面时,我们正在从互联网上加载信息。我们希望在信息不可用的情况下避免转到该页面。

对于这个示例,像之前的示例一样将 Axios 添加为依赖项。

首先,使用下面的代码来丰富 HTML 布局:

<div id="app">

  <h1>My Portfolio</h1>

  <ul>

    <li><router-link to="/" exact>Home</router-link></li>

    <li><router-link to="/aboutme">About Me</router-link></li>

  </ul>

  <router-view></router-view>

 <div class="toast" v-show="showError"> 

 There was an error 

 </div> 

</div>

这是一个弹出消息,每当出现错误时都会显示在屏幕上。使用以下 CSS 规则为其添加一些样式:

div.toast {

  width: 15em;

  height: 1em;

  position: fixed;

  bottom: 1em;

  background-color: red;

  color: white;

  padding: 1em;

  text-align: center;

}

接下来要做的是有一个全局机制将showError设置为true。在 JavaScript 代码的顶部,声明vm变量:

let vm

然后,将我们的Vue根实例分配给它:

vm =

 new Vue({

  router,

  el: '#app',

 data: { 

 showError: false 

 } 

})

我们还将showError变量添加到数据选项中。

最后要做的是在显示个人简介信息之前,实际上管理数据检索时的错误。

将下面的代码添加到beforeRouteEnter钩子中:

beforeRouteEnter (to, from, next) {

  axios.post('http://example.com/

', {

    "type": "object",

    "properties": {

      "name": {

        "type": "string",

        "ipsum": "name"

      },

      "phone": {

        "type": "string",

        "format": "phone"

      }

    }

  }).then(response => {

  next(vm => {

    vm.name = response.data.name

    vm.phone = response.data.phone

  })

}).catch(error => { 

 vm.showError = true 

 next(false) 

 }) 

}

接下来的(false)命令将使用户停留在原地,我们还将端点编辑为example.com,这将在POST请求上返回错误代码:

工作原理...

Axios 将从example.com接收到一个错误,这将触发对我们调用 post 时创建的 promise 的拒绝。promise 的拒绝将反过来触发传递给 catch 的函数。

值得注意的是,在代码的这一点上,vm指的是根Vue实例;这是因为该代码总是在Vue实例初始化并分配给vm之后执行的。

为页面添加进度条以加载页面

确实,使用 SPA,用户不必等待新页面加载,但他仍然需要等待数据加载。在在切换路由之前获取数据示例中,我们在点击按钮进入/aboutme页面后还需要等待一段时间。没有任何提示数据正在加载,然后突然页面出现了。如果用户至少有一些反馈页面正在加载,那不是很好吗?

准备工作

为了跟上,您应该完成在切换路由之前获取数据示例。

本示例将在此基础上构建,并假设您已经有了所有相关的代码。

如何操作...

如前所述,我假设您已经有了从在切换路由之前获取数据示例中得到的所有代码,并且已经正常工作。

对于这个示例,我们将使用一个额外的依赖项--NProgress,一个在屏幕顶部显示加载进度条的小工具。

将以下两行代码添加到页面的头部或 JSFiddle 的依赖项列表中(也有一个 npm 包):

<link rel="stylesheet" href="https://cdn.bootcss.com/nprogress/X/nprogress.css">

<script src="https://cdn.bootcss.com/nprogress/X/nprogress.js"></script>

在这里,XNProgress的版本。在编写时,版本号为 0.2.0,但你可以在网上查找。

完成这一步后,下一步是定义我们希望进度条具有的行为。

首先,我们希望在点击链接后立即显示进度条。为此,我们可以为点击事件添加一个事件监听器,但如果有一百个链接,这将是一个很差的设计。一个更可持续和清晰的方法是通过为路由创建一个新的钩子,并将进度条的出现与路由的切换连接起来。这也将具有提供一致的应用体验的优势:

router.beforeEach((to, from, next) => {

  NProgress.start()

  next()

})

类似地,我们希望在加载成功后进度条消失。这意味着我们希望在回调函数中执行这个操作:

beforeRouteEnter (to, from, next) {

  axios.post('http://schematic-ipsum.herokuapp.com/', {

    "type": "object",

    "properties": {

      "name": {

        "type": "string",

        "ipsum": "name"

      },

      "phone": {

        "type": "string",

        "format": "phone"

      }

    }

  }).then(response => {

 NProgress.done() 

    next(vm => {

      vm.name = response.data.name

      vm.phone = response.data.phone

    })

  })

}

现在你可以启动应用程序,你的进度条应该已经工作了:

它是如何工作的...

这个示例还证明了利用外部库并不难,只要它们易于安装。

由于NProgress组件如此简单且有用,我在这里报告它的 API 作为参考:

  • NProgress.start():显示进度条

  • NProgress.set(0.4):设置进度条的百分比

  • NProgress.inc():稍微增加进度条

  • NProgress.done():完成进度

我们使用了前面两个函数。

作为预防措施,我还建议不要依赖于各个组件调用done()函数。我们在then函数中调用它,但如果下一个开发人员忘记了呢?毕竟,我们在任何路由切换之前都会启动进度条。

最好是在router中添加一个新的钩子:

router.afterEach((to, from) => {

  NProgress.done()

})

由于done函数是幂等的,我们可以随意调用它多次。因此,这不会修改我们应用程序的行为,并且将确保即使将来的开发人员忘记关闭进度条,它也会在路由改变后自动消失。

如何重定向到另一个路由

你可能有无数个原因希望重定向用户。你可能希望用户在访问页面之前先登录,或者页面已经移动,你希望用户注意到新的链接。在这个示例中,你将重定向用户到一个新的主页,作为快速修改网站的一种方式。

准备工作

这个示例只会使用关于 vue-router 的基本知识。如果你已经完成了使用 vue-router 创建单页应用的示例,你就可以开始了。

如何做到这一点...

假设我们有一个在线服装店。

这将是网站的 HTML 布局:

<div id="app">

  <h1>Clothes for Humans</h1>

  <ul>

    <li><router-link to="/">Home</router-link></li>

    <li><router-link to="/clothes">Clothes</router-link></li>

  </ul>

  <router-view></router-view>

</div>

它只是一个带有指向服装列表的链接的页面。

让我们注册VueRouter

Vue.use(VueRouter)

我们的网站有三个页面,分别由以下组件表示:

const Home = { template: '<div>Welcome to Clothes for Humans</div>' }

const Clothes = { template: '<div>Today we have shoes</div>' }

const Sales = { template: '<div>Up to 50% discounts! Buy!</div>' }

它们代表主页、服装列表和去年使用的带有一些折扣的页面。

让我们注册一些routes

const router = new VueRouter({

  routes: [

    { path: '/', component: Home }

    { path: '/clothes', component: Clothes },

    { path: '/last-year-sales', component: Sales }

  ]

})

最后,我们添加一个根Vue实例:

new Vue({

  router,

  el: '#app'

})

您可以启动应用程序,它应该可以正常工作,没有任何问题。

黑色星期五明天就要到了,我们忘记了这是全球时尚界最大的活动。我们没有时间重写主页,但去年销售的那个页面可以解决问题。我们将把访问我们主页的用户重定向到那个页面。

为了实现这一点,我们需要修改我们注册的routes

const router = new VueRouter({

  routes: [

    { path: '/', component: Home, redirect: '/last-year-sales'

 },

    { path: '/clothes', component: Clothes },

    { path: '/last-year-sales', component: Sales }

  ]

})

只需添加该重定向,我们就能挽救这一天。现在,每当您访问主页时,都会显示销售页面。

它是如何工作的...

当匹配到根路由时,Home组件将不会被加载。而是匹配到/last-year-sales路径。我们也可以完全省略组件,因为它永远不会被加载:

{ path: '/', redirect: '/last-year-sales' }

还有更多...

在 vue-router 中,重定向比我们刚才看到的更强大。在这里,我将尝试通过重定向为刚刚创建的应用程序增加更多功能。

重定向到 404 页面

通过在最后一个路由中添加一个捕获所有路由,可以重定向找不到的页面。它将匹配到其他路由未匹配到的所有内容:

...

{ path: '/404', component: NotFound },

{ path: '*', redirect: '/404' }

命名重定向

重定向可以与命名路由结合使用(参考“使用命名动态路由”配方)。我们可以通过名称指定目标:

...

{ path: '/clothes', name: 'listing', component: Clothes },

{ path: '/shoes', redirect: { name: 'listing' }}

带参数的重定向

在重定向时,您还可以保留参数:

...

{ path: '/de/Schuh/:size', redirect: '/en/shoe/:size' },

{ path: '/en/shoe/:size', component: Shoe }

动态重定向

这是最终的重定向。您可以访问用户尝试访问的路由,并决定要将其重定向到何处(但无法取消重定向):

...

{ path: '/air', component: Air },

{ path: '/bags', name: 'bags', component: Bags },

{ path: '/super-shirt/:size', component: SuperShirt },

{ path: '/shirt/:size?', component: Shirt},

{ path: '/shirts/:size?',

  redirect: to => {

    const { hash, params, query } = to

    if (query.colour === 'transparent') {

      return { path: '/air', query: null }

    }

    if (hash === '#prada') {

      return { name: 'bags', hash: '' }

    }

    if (params.size > 10) {

      return '/super-shirt/:size'

    } else {

      return '/shirt/:size?'

    }

  }

}

在返回时保存滚动位置

在 vue-router 中,有两种导航模式:hashhistory。默认模式和前面的示例中使用的模式是history。传统上,当您访问一个网站,向下滚动一点并点击链接到另一个页面时,新页面从顶部显示。当您点击浏览器的返回按钮时,页面从先前滚动的高度显示,并且您刚刚点击的链接可见。

当你在 SPA(单页应用)中时,这并不是真实的,或者至少不是自动的。vue-router 的历史模式可以让你模拟这一点,甚至更好的是,可以对滚动行为进行精细控制。

准备中

为了完成这个步骤,我们需要切换到历史模式。历史模式只在应用程序在正确配置的服务器上运行时才有效。如何为 SPA 配置服务器超出了本书的范围(但原则是每个路由都从服务器端重定向到index.html)。

我们将使用一个 npm 程序来启动一个小型服务器;我们期望您已经安装了 npm(您可以查看“选择开发环境”这个教程来了解更多关于 npm 的信息)。

如何做到这一点...

首先,您将安装一个紧凑的服务器用于单页应用程序,以便历史模式能够正常工作。

在您喜欢的命令行中,进入包含您的应用程序的目录。然后,输入以下命令:

    npm install -g history-server

    history-server .

运行服务器后,您需要将浏览器指向http://localhost:8080,如果您的目录中有一个名为index.html的文件,它将被显示出来;否则您将看不到太多内容。

创建一个名为index.html的文件,并填写一些样板内容,就像在“选择开发环境”一节中所示。我们希望得到一个只有Vuevue-router作为依赖项的空白页面。我们的空白画布应该如下所示:

<!DOCTYPE html>

<html>

<head>

  <script src="https://unpkg.com/vue/dist/vue.js"></script>

  <script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>

</head>

<body>

  <div id="app">

  </div>

  <script>

    new Vue({

      router,

      el: '#app'

    })

  </script>

</body>

</html>

作为 HTML 布局,将以下内容放在body中:

<div id="app">

  <h1>News Portal</h1>

    <ul>

      <li><router-link to="/">Home</router-link></li>

      <li><router-link to="/sports">Sports</router-link></li>

      <li><router-link to="/fashion">Fashion</router-link></li>

    </ul>

  <router-view></router-view>

</div>

我们有一个带有三个链接和一个router-view入口点的标题。我们将为体育和时尚页面创建两个长页面:

const Sports = { template: `

  <div>

    <p v-for="i in 30">

      Sample text about sports {{i}}.

    </p>

    <router-link to="/fashion">Go to Fashion</router-link>

    <p v-for="i in 30">

      Sample text about sports {{i + 30}}.

    </p>

  </div>

` }

const Fashion = { template: `

  <div>

    <p v-for="i in 30">

      Sample text about fashion {{i}}.

    </p>

    <router-link to="/sports">Go to Sports</router-link>

    <p v-for="i in 30">

      Sample text about fashion {{i + 30}}.

    </p>

  </div>

` }

对于主页组件,我们只需要一个存根:

const Home = { template: '<div>Welcome to BBCCN</div>' }

为这个新闻网站编写一个合理的路由器:

Vue.use(VueRouter)

const router = new VueRouter({

  routes: [

    { path: '/', component: Home },

    { path: '/sports', component: Sports },

    { path: '/fashion', component: Fashion } 

  ]

})

如果您现在使用浏览器访问之前指定的地址,您应该能看到网站正在运行。

转到体育页面,向下滚动直到看到链接,然后点击它。

注意您正在访问的页面不是从开始显示的。这在传统网站中不会发生,也是不可取的。

点击返回按钮,注意我们回到了上次离开页面的位置;我们希望保留这种行为。

最后,注意页面的 URL 看起来不太自然,而是带有哈希符号;我们希望 URL 看起来更好:

为了实现这一点,让我们将路由器代码修改为以下内容:

const router = new VueRouter({

 mode: 'history',

  routes: [

    { path: '/', component: Home },

    { path: '/sports', component: Sports },

    { path: '/fashion', component: Fashion }

  ],

 scrollBehavior (to, from, savedPosition) { 

 if (savedPosition) { 

 return savedPosition 

 } else { 

 return { x: 0, y: 0 } 

 } 

 } 

})

我们添加了一行指定新模式为 history(链接中没有哈希),并定义了scrollBehavior函数以返回到上次位置(如果存在);如果是新页面,它应该滚动到左上角。

您可以通过刷新浏览器并返回到主页来尝试此操作。

打开体育页面,并点击页面中间的链接。新页面现在从开始显示。

点击返回,savedPosition被恢复。

注意现在 URL 看起来更好了:

工作原理...

当您在浏览器中使用包含哈希符号的 URL 时,浏览器将发送一个不包含哈希后缀的 URL 的请求,也就是说,当您在页面内部有一个事件,该事件转到相同页面但具有不同的哈希后缀时:

http://example.com#/page1

 on  http://example.com#/page2

浏览器不会重新加载页面;这就是为什么当用户点击一个只修改哈希而不重新加载页面的链接时,vue-router 可以修改页面的内容。

当您将模式从hash更改为history时,vue-router 将删除哈希符号,并利用“history.pushState()”函数。

这个函数添加了另一个虚拟页面并将 URL 更改为其他内容:

http://example.com/page1

 =pushState=> http://example.com/page2

浏览器不会发送 GET 请求来查找page2;实际上,它什么都不会做。

当您按下返回按钮时,浏览器会恢复 URL,并且 vue-router 会接收到一个事件。然后它将读取 URL(现在是“page1”)并匹配相关的路由。

我们紧凑的历史服务器的作用是将每个 GET 请求重定向到index.html页面。这就是为什么当我们尝试直接访问http://localhost:8080/fashion时,我们不会得到 404 错误的原因。

第六章:单元测试和端到端测试

本章将介绍以下内容:

  • 使用 Jasmine 进行 Vue 测试

  • 将 Karma 添加到工作流程中

  • 测试应用程序的状态和方法

  • 测试 DOM

  • 测试 DOM 异步更新

  • 使用 nightwatch 进行端到端测试

  • 在 nightwatch 中模拟双击

  • 不同风格的单元测试

  • 使用 Sinon.JS 对外部 API 调用进行存根

  • 测量代码的覆盖率

介绍

测试是真正区分专业软件和业余软件的关键。根据行业经验和研究,发现软件成本的很大一部分在于在软件投入生产时纠正错误。测试软件可以减少生产中的错误,并使纠正这些错误的成本大大降低。

在本章中,您将学习如何设置测试工具和编写单元测试和集成测试,以加快应用程序开发速度,并使其在复杂性增加时不留下错误。

在完成这些示例后,您将熟悉最流行的测试框架和术语;您将能够自信地发布按预期工作的软件。

使用 Jasmine 进行 Vue 测试

Jasmine 是一个用于测试的库,非常易于使用,并且能够直接在浏览器中显示测试结果。在这个示例中,您将构建一个简单的 Vue 应用程序,并使用 Jasmine 进行测试。

准备工作

希望您不是从这个示例开始学习 Vue,因为我将假设,就像本章的其他部分一样,您已经了解了在 Vue 中构建简单应用程序的基础知识。

您还应该能够在互联网上找到四个文件。我将在写作时提供链接,但是当然,它们可能会发生变化:

您可以方便地从cdnjs.com/libraries/jasmine页面复制粘贴所有链接。

这些文件彼此依赖,因此添加它们的顺序很重要!特别是,boot.js 依赖于 jasmine-html.js,而 jasmine-html.js 又依赖于 jasmine.js

如何做到这一点...

Jasmine 是一个由各种模块组成的库。为了使其工作,您需要安装一些与 Jasmine 相关的依赖项。我假设您正在使用 JSFiddle 进行操作。如果您使用的是 npm 或其他方法,您应该能够根据原则简单地推导出需要更改的内容。

要在您的应用程序中安装 Jasmine,您将需要四个不同的依赖项,其中一个仅用于 CSS 样式。

这四个文件的顺序(按依赖关系排序)是:

  • jasmine.css

  • jasmine.js 是一个用于 JavaScript 测试的开源框架。它提供了一套简洁的语法和功能,用于编写和执行单元测试和集成测试。jasmine.js 可以帮助开发人员轻松地编写可靠的测试用例,以确保代码的质量和稳定性。无论是在前端还是后端开发中,jasmine.js 都是一个非常有用的工具。

  • jasmine-html.js(依赖于前面的 js 文件)

  • boot.js(依赖于前面的 js 文件)

你应该能够在 CDNJS 或其他 CDN 上找到所有这些文件。按照显示的顺序安装它们,否则它们将无法正常工作。

当你把所有文件放好后,写下以下 HTML 代码:

<div id="app">

  <p>{{greeting}}</p>

</div>

然后,将以下脚本添加为 JavaScript 部分:

new Vue({

  el: '#app',

  data: {

    greeting: 'Hello World!'

  }

})

现在可以启动应用程序了,正如预期的那样,屏幕上会出现Hello World的消息。

我们希望在对应用程序进行修改和添加新功能时,能够确保我们的应用程序始终显示这条消息。

在这方面,Jasmine 将帮助我们。在 Vue 实例之后,我们编写以下 JavaScript 代码:

describe('my app', () => {

  it('should say Hello World', () => {

    expect(document.querySelector('p').innerText)

      .toContain('Hello World')

  })

})

为了使其在 JSFiddle 中工作,需要将 Load Type 设置为 No wrap - in 。如果保持默认的 Load Type onLoad,它将在 Vue 有机会启动之前加载 Jasmine。

现在尝试启动应用程序。您将在页面末尾看到 Jasmine 的详细报告,告诉您应用程序是否有问题。

如果一切如预期,您应该会看到一个快乐的绿色条,如下所示:

工作原理...

您为 Vue 应用程序编写了第一个单元测试。如果您已经编写了单元测试,那么一切都应该很清楚,因为我们没有使用任何 Vue 特有的功能来编写测试。

无论如何,让我们花点时间分析我们编写的代码;之后,我将提供一些关于在编写真实应用程序时何时编写类似测试的考虑事项。

我们编写的测试在网页上读作“我的应用程序应该说 Hello World”。

这是一条相当通用的消息;然而,让我们仔细看一下代码:

expect(document.querySelector('p').innerText)

  .toContain('Hello World')

将其作为一个英语短语来阅读-我们期望文档中的<p>元素包含文本Hello World

document.querySelector('p')代码选择页面内的第一个p元素,确切地说。innerText查找 HTML 元素内部并返回可读的文本。然后,我们验证该文本是否包含Hello World

在实际应用中,你不会将测试代码直接写在网页下方。测试对于开发者来说非常重要,可以在每次代码更改后自动验证每个功能是否正常工作,而无需手动验证。另一方面,你不希望用户看到测试结果。

通常情况下,您将拥有一个专门的页面,只有开发人员可以访问,该页面会为您运行所有的测试。

还有更多...

在软件开发中有一种广泛的实践叫做TDD或者测试驱动开发。它鼓励你将软件的功能视为测试。这样一来,你可以通过测试本身的工作来确保软件中的功能正常运行。

在这一部分中,我们将使用 TDD 为我们的食谱添加一个功能。我们希望页面上有一个标题,上面写着“欢迎”。

首先,在 hello world 测试之后,我们将为describe函数内的功能编写一个(失败的)测试。

it('should have an header that says `Welcome`', () => {

  expect(document.querySelector('h1').innerText)

    .toContain('Welcome')

})

当我们启动测试时,我们应该看到它失败:

现在,不要太关注堆栈跟踪。你应该注意的重要事情是,我们有一个测试失败的名称(另一个测试仍然有效)。

在实现功能本身之前,编写测试并确保它失败是很重要的。要理解为什么,试着想象一下,我们在实现功能之前编写了测试,然后我们启动它,然后它成功了。这意味着测试实际上并没有起作用,因为我们从一开始就没有实现功能。

如果你认为这只是奇怪和不可能的,请再次思考。在实践中,经常发生这样的情况,一个看起来完全正常的测试实际上并没有测试任何东西,并且无论功能是否损坏,它总是成功的。

在这一点上,我们已经准备好实际实现功能了。我们编辑 HTML 布局,像这样:

<div id="app">

  <h1>Welcome</h1>

  <p>{{greeting}}</p>

</div>

当我们启动页面时,结果应该类似于这样:

为你的工作流添加一些 Karma

Karma 是一个 JavaScript 测试运行器。这意味着它将为您运行测试。软件往往会迅速增长,Karma 为您提供了一种同时运行所有单元测试的方法。它还为您提供了添加监视测试覆盖率和代码质量的工具的能力。

Karma 在 Vue 项目中传统上被使用,并且作为一个工具存在于官方 Vue 模板中。学习 Karma 是您 JavaScript 工具箱的一个很好的补充,即使您不使用 Vue 也是如此。

准备工作

我认为已经完成了“使用 Jasmine 测试 Vue”的先决条件。由于 Karma 是一个测试运行器,所以您应该首先能够编写测试。

在这个教程中,我们将使用 npm,所以在继续之前,请确保已经安装了它,并阅读有关如何使用它的基础知识。

如何操作...

对于这个教程,我们将需要命令行和 npm,所以在继续之前,请确保已经安装了它。

在一个新的文件夹中,创建一个名为package.json的文件,并在其中写入以下内容:

{

  "name": "my-vue-project",

  "version": "1.0.0"

}

只要将此文件放在您的文件夹中,就会创建一个新的 npm 项目。我们稍后会编辑这个文件。

在命令行中,进入项目所在的目录,并在其中输入以下命令来安装必要的依赖项:

npm install --save-dev vue karma jasmine karma-jasmine karma-chrome-launcher

这将安装 Vue 以及 Karma、Jasmine 和 Karma 的一些插件作为我们项目的依赖项。

如果您现在查看package.json,您会看到它已相应地更改。

下一个命令将创建一个名为karma.conf.js的文件,其中包含 Karma 的配置:

./node_modules/karma/bin/karma init

这将询问您一些问题,除了询问源文件和测试文件的位置时,其他问题都选择默认值。对于该问题,只需写入*.js。完成后,您应该能够在目录中看到karma.conf.js文件。打开它并快速查看您通过回答问题设置的所有设置。

由于 Karma 不知道 Vue,您需要进行一些小的修改,将 Vue 添加为 Karma 的依赖项。有几种方法可以做到这一点;最快的方法可能是在要加载的文件列表中添加一行。在karma.conf.js文件中,在files数组中添加以下行:

...    

    // list of files / patterns to load in the browser 

files:

 [ 

'node_modules/vue/dist/vue.js'

 **,** 

'*.js' 

 ],

... 

请注意,当您回答问题时,您也可以直接添加该行。

下一步是编写我们要测试的应用程序。

在您的文件夹中,创建一个名为myApp.js的文件;在其中写入以下内容:

const myApp = {

  template: `

    <div>

      <p>{{greetings}}</p>

    </div>

  `,

  data: {

    greetings: 'Hello World'

  }

}

我们分配给myApp的对象只是一个简单的 Vue 实例。

接下来,我们将为其创建一个测试。具体来说,我们将检查组件中是否包含Hello World文本。

创建一个名为test.js的文件,并在其中写入以下内容:

describe('my app', () => {

  beforeEach(() => {

    document.body.innerHTML = `

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

    `

    new Vue(myApp)

      .$mount('#app')

  })

  it('should say Hello World', () => {

    expect(document.querySelector('p').innerText)

      .toContain('Hello World')

  })

})

beforeEach块将在每个测试之前运行(现在我们只有一个测试),在检查其他功能之前重置我们的 Vue 应用程序的状态。

现在,我们可以运行我们的测试了。在终端中输入以下命令:

./node_modules/karma/bin/karma start

你应该看到 Chrome 启动,如果你回到命令行,你应该收到类似于以下消息:

这意味着你的测试成功了。

工作原理...

在你的配方完成后,你应该注意你的应用程序的一般结构。你有应用程序本身在myApp.js中,然后你有你的测试在test.js中。你有一些配置文件,如karma.conf.jspackage.json,你的库在node_modules目录中。所有这些文件一起工作,使你的应用程序可测试。

在一个真实的应用程序中,你可能会有更多的源代码和测试文件,而配置文件通常增长得更慢。

在整个设置中,你可能会想知道如何启动应用程序本身。毕竟,没有 HTML,我们只启动了测试;我们从来没有见过这个Hello World程序。

实际上,你是对的;这里没有要启动的程序。事实上,我们必须在测试的beforeEach中为 HTML 布局编写一个固定装置:

beforeEach(() => {

  document.body.innerHTML = `

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

  `

  new Vue(window.myApp)

    .$mount('#app')

})

在上述代码中,我们注入了 HTML,它只包含一个<div>元素(其余的布局在myApp.js中)在页面中。

然后,我们创建一个新的 Vue 实例,传递在myApp.js中定义的myApp变量中包含的选项对象;然后我们使用$mount('#app') Vue API,在我们刚刚注入的<div>元素中实际地实现应用程序。

还有更多...

每次从node_modules目录中调用 Karma 可能会很烦人。有两种方法可以使这更愉快:我们可以全局安装 Karma,或者我们可以将 Karma 添加到我们的 npm 脚本中;我们将两者都做。

首先,让我们将 Karma 添加到我们的 npm 脚本中。进入package.json文件,并添加以下代码块:

...

"version": "1.0.0",

  "scripts": {

    "test": "./node_modules/karma/bin/karma start"

  },

"devDependencies": {

...

现在,你可以输入npm run test,Karma 将自动启动。接下来,我们可以使用以下命令全局安装 Karma:

npm install -g karma

现在我们可以编写诸如karma initkarma start之类的命令,并且它们将被识别。我们还可以编辑我们的package.json,像这样:

...

"version": "1.0.0",

  "scripts": {

    "test": "karma start"

  },

"devDependencies": {

...

测试应用程序的状态和方法

在这个示例中,我们将编写一个单元测试来直接触摸和检查我们的 Vue 实例的状态。测试组件状态的优势是我们不必等待 DOM 更新,即使 HTML 布局发生变化,状态变化也会慢得多,从而减少了测试所需的维护量。

准备工作

在尝试这个示例之前,您应该完成将一些 Karma 添加到您的工作流程,因为我们将描述如何编写测试,但我们不会提及测试环境的设置。

如何做...

假设我们有一个应用程序,它用Hello World!来问候您,但它还有一个按钮可以将问候语翻译成意大利语,即Ciao Mondo!

为此,您需要在一个新文件夹中创建一个新的 npm 项目。在那里,您可以使用以下命令安装此示例所需的依赖项:

npm install --save-dev vue karma jasmine karma-jasmine karma-chrome-

   launcher

要设置 Karma,就像在前一个示例中一样,运行以下命令:

./node_modules/karma/bin/karma init

除了问题您的源文件和测试文件的位置是什么?,保留默认答案;对于这个问题,您应该用以下两行回答:

  • node_modules/vue/dist/vue.js

  • *.js

创建一个名为test.js的文件,并在其中编写一个beforeEach,以便将应用程序恢复到其起始状态,以便可以独立于其他测试进行测试:

describe('my app', () => {

  let vm

  beforeEach(() => {

    vm = new Vue({

      template: `

        <div>

          <p>{{greetings}}</p>

          <button @click="toItalian">

            Translate to Italian

          </button>

        </div>

      `,

      data: {

        greetings: 'Hello World!'

      },

      methods: {

        toItalian () {

          this.greetings = 'Ciao Mondo!'

        }

      } 

    }).$mount()

  })

})

请注意,您在开始时声明了vm变量来引用我们的 Vue 实例。

beforeEach之后(但在describe内部),添加以下(目前为空)测试:

it(`should greet in Italian after

  toItalian is called`, () => {

})

在测试的第一部分中,您将使组件达到所需的状态(在调用toItalian之后):

it(`should greet in Italian after

    toItalian is called`, () => {

 vm.toItalian()

})

现在,我们想要检查问候语是否已更改:

it(`should greet in Italian after

    toItalian is called`, () => {

  vm.toItalian()

 expect(vm.greetings).toContain('Ciao Mondo')

})

现在,为了证明每个测试之前状态都被重置了,添加以下内容:

it('should greet in English', () => {

  expect(vm.greetings).toContain('Hello World')

})

如果状态真的被重置了,它应该包含英文问候语,如果你启动测试(使用./node_modules/karma/bin/karma start命令),你会发现(如果没有错误的话)确实是这样的。

工作原理...

由于我们有 Vue 实例本身的引用,我们可以直接在测试中访问方法和状态变量。

我希望你花一些时间欣赏测试的名称。第一个标题为should greet in Italian after toItalian is called。它没有提到页面或图形,并且没有对前提条件做任何假设。请注意,按钮从未被点击过,事实上,在测试标题中也没有提到按钮。

如果我们将测试标题命名为should display 'Ciao Mondo' when Translate button is clicked on,那么我们就会撒谎,因为我们从未检查问候语是否实际显示,并且我们在测试中从未点击按钮。

在真实应用程序中,正确命名测试非常重要,因为当你有成千上万个测试时,如果有一个测试失败,你首先读到的是标题或测试应该检查的内容。如果标题误导了,你将花费很多时间追逐一个错误的线索。

测试 DOM

在这个示例中,您将学习一种技术,可以快速测试 DOM 或网页本身是否符合预期,即使 Vue 组件不在页面中。

准备工作

对于这个示例,您应该已经有一个已经设置好并且工作正常的测试环境;如果您不知道这是什么意思,请完成使用 Jasmine 进行 Vue 测试示例。

我假设您已经安装了 Jasmine 并且可以执行测试。

基本上,您只需要一个网页(JSFiddle 可以)和这四个已安装的依赖项:

  • jasmine.css

  • jasmine.js

  • jasmine-html.js

  • boot.js

如果您正在使用 JSFiddle 或手动添加它们,请记住按指定的顺序添加它们。

在“使用 Jasmine 进行 Vue 测试”配方中找到这些文件的链接。

操作步骤如下:

假设您正在编写一个显示“Hello World!”问候语的组件;您希望测试该问候语是否实际显示,但您正在测试的网页已经足够复杂,您希望在隔离环境中测试您的组件。

事实证明,您不必实际显示组件来证明它的工作。您可以在文档之外显示和测试您的组件。

在您的测试文件或页面的测试部分中,编写以下设置来显示问候语:

describe('my app', () => {

  let vm

  beforeEach(() => {

    vm = new Vue({

      template: '<div>{{greetings}}</div>',

      data: {

        greetings: 'Hello World'

      }

    })

  })

})

为了将我们的 Vue 实例实现为一个文档之外的元素,我们只需要添加$mount() API 调用:

beforeEach(() => {

    vm = new Vue({

      template: '<div>{{greetings}}</div>',

      data: {

        greetings: 'Hello World'

      }

    }).$mount()

  })

由于我们有对vm的引用,我们现在可以测试我们的组件以访问在文档之外渲染的元素:

it('should say Hello World', () => {

  expect(vm.$el.innerText).toContain('Hello World')

})

vm.$el元素代表我们的组件,但无法从正常的 DOM 中访问。

工作原理如下:

在初始化时,Vue 实例会检查是否有el选项。在我们的示例中,我们通常包含一个el选项,但这次我们有一个模板:

vm = new Vue({

  template: '<div>{{greetings}}</div>',

  data: {

    greetings: 'Hello World'

  }

}).$mount()

当 Vue 实例具有el选项时,它会自动挂载到该元素(如果找到);在我们的情况下,Vue 实例等待$mount调用。我们不提供任何参数给函数,因此组件会在文档之外渲染。

此时,从 DOM 中检索它的唯一方法是通过$el属性。一旦组件被挂载,$el属性始终存在,无论组件是手动挂载还是自动挂载。

从那里,我们可以像访问任何普通组件一样访问它,并测试一切是否符合我们的预期。

测试 DOM 的异步更新。

在 Vue 中,当组件的状态发生变化时,DOM 会相应地发生变化;这就是为什么我们称之为响应式状态的原因。唯一需要注意的是,更新不是同步的;我们必须等待额外的时间来实际传播这些变化。

准备中

对于这个配方,我假设你已经完成了“使用 Jasmine 进行 Vue 测试”的配方,并且知道如何编写基本测试。

如何做到这一点...

我们将编写的测试是 Vue 更新机制工作原理的示例。从那里,您将能够自己编写异步测试。

在我们的测试套件的beforeEach函数中,编写以下 Vue 实例:

describe('my app', () => {

  let vm

  beforeEach(() => {

    vm = new Vue({

      template: `

        <div>

          <input id="name" v-model="name">

          <p>Hello from 

            <span id="output">{{name}}</span>

          </p>

        </div>

      `,

      data: {

        name: undefined

      }

    }).$mount()

  })

})

这将创建一个组件,其中包含一个文本框和一个 span 元素,该 span 元素将包含Hello from ...短语以及文本框中输入的任何内容。

我们将如何测试这个组件是,在文本框中编写Herman(通过编程方式,而不是手动),然后等待 DOM 更新。当 DOM 更新后,我们检查是否出现了Hello from Herman这个短语。

让我们从beforeEach函数之后的一个空测试开始:

it('should display Hello from Herman after Herman is typed in the text-box', done => {

  done()

})

前面的测试已经通过了。请注意,我们正在接收done参数,然后将其作为函数调用。只有在调用done()之后,测试才会通过。

<span>元素分配给一个变量以方便操作,然后将文本Herman插入到文本框中。

it('should display Hello from Herman after Herman is typed in the text-box', done => {

 const outputEl = vm.$el.querySelector('#output')

 vm.$el.querySelector('#name').value = 'Herman'

  done()

})

当我们修改状态时,我们必须等待 DOM 更新,但反之则不然;当我们修改了 DOM 时,我们可以立即检查name变量是否已更改。

it('should display Hello from Herman after Herman is typed in the text-box', done => {

  const outputEl = vm.$el.querySelector('#output')

  vm.$el.querySelector('#name').value = 'Herman'

 expect(vm.name = 'Herman')

  done()

})

在您编辑测试时,启动它以检查是否正常工作。

接下来,我们将为Vue组件的下一个更新周期安装一个监听器,称为 tick。

it('should display Hello from Herman after Herman is typed in the text-box', done => {

  const outputEl = vm.$el.querySelector('#output')

  vm.$el.querySelector('#name').value = 'Herman'

  expect(vm.name = 'Herman')

 vm.$nextTick(() => {

    done()

 })

})

$nextTick块中的所有内容只有在 DOM 更新后才会运行。我们将检查<span>元素的内容是否已更改。

it('should display Hello from Herman after Herman is typed in the text-box', done => {

  const outputEl = vm.$el.querySelector('#output')

  vm.$el.querySelector('#name').value = 'Herman'

 expect(outputEl.textContent).not.toContain('Herman')

  expect(vm.name = 'Herman')

  vm.$nextTick(() => {

 expect(outputEl.textContent).toContain('Herman')

    done()

  })

})

请注意,在进行下一次操作之前,我们还会验证 DOM 是否未更改。

工作原理如下...

官方文档中指出:

Vue 以异步方式执行 DOM 更新。每当观察到数据变化时,它将打开一个队列并缓冲在同一事件循环中发生的所有数据变化。

因此,许多测试需要使用$nextTick辅助函数。然而,目前正在努力创建更好的工具来处理测试和同步性,因此,尽管本文档说明了问题,但可能不是处理测试的最新方法。

使用 Nightwatch 进行端到端测试

有时单元测试并不能满足需求。我们可能需要集成两个独立开发的功能,并且尽管每个功能都经过了单元测试并且可以正常工作,但没有简单的方法来同时测试它们。此外,这也违背了单元测试的目的-测试软件的原子单元。在这种情况下,可以进行集成测试和端到端(end-to-end)测试。Nightwatch 是一种模拟用户在网站上点击和输入的软件。这可能是我们想要的最终验证整个系统是否正常工作的方式。

准备工作

在开始进行这个稍微高级的示例之前,您应该已经熟悉命令行和 npm。如果您对它们不熟悉,请查看选择开发环境示例。

操作步骤...

为这个示例创建一个新文件夹,并在其中创建一个名为index.html的新文件。

这个文件将包含我们的 Vue 应用程序,也是我们要测试的内容。在这个文件中写入以下内容:

<!DOCTYPE html>

<html>

<head>

  <title>Nightwatch tests</title>

  <script src="https://unpkg.com/vue/dist/vue.js"></script>

</head>

<body>

  <div id="app">

  </div>

  <script>

  </script>

</body>

</html>

正如您所看到的,这只是一个小型 Vue 应用程序的常规样板。在<div>标签内放置一个标题和一个按钮;当我们点击按钮时,将显示文本Hello Nightwatch!

<div id="app">

  <h2>Welcome to my test page</h2>

  <button @click="show = true">Show</button>

  <p v-show="show">Hello Nightwatch!</p>

</div>

<script>标签内,写入以下 JavaScript 代码使其工作:

<script>

  const vm = new Vue({

    el: '#app',

    data: {

      show: false

    }

  })

</script>

我们的应用程序已经完成;现在我们进入了示例的测试部分。

执行以下命令来安装您的依赖项:

npm install -g selenium-standalone http-server nightwatch

这将安装 Selenium 服务器,这是自动化浏览器操作所必需的,也是使 nightwatch 工作的真正原因。http-server命令将有助于在不必记住长文件路径的情况下提供我们的工作网站。最后,它将安装 nightwatch 本身,它在很大程度上是 Selenium 的包装器和 JavaScript API。

当 npm 完成安装所有这些工具后,创建一个名为nightwatch.json的新文件,其中包含 nightwatch 的配置,并在其中写入以下内容:

{

  "src_folders" : ["tests"],

  "test_settings" : {

    "default" : {

      "desiredCapabilities": {

        "browserName": "chrome"

      }

    }

  }

}

第一个设置表示您将在名为 tests 的文件夹中编写所有测试(我们将创建该文件夹);第二个设置只是将 Chrome 设置为我们运行测试的默认浏览器。

现在,在test目录下创建一个test.js文件。在该文件中,我们将测试应用程序。我们将验证当应用程序启动时,<p>标签是不可见的,并且当我们点击按钮时,它应该出现。

一个空的测试看起来像这样:

module.exports = {

  'Happy scenario' :client => {}

}

在这里,客户端是浏览器(在本例中为 Chrome)。

我们将在http://localhost:8080地址上提供我们的应用程序,所以首先我们希望浏览器转到这个地址。为此,我们将编写以下代码:

module.exports = {

  'Happy scenario' :client => {

    client

 .url('http://localhost:8080')

  }

}

接下来,我们等待页面加载;我们通过等待具有id="app"<div>出现来间接实现这一点:

module.exports = {

  'Happy scenario' :client => {

    client

      .url('http://localhost:8080')

 .waitForElementVisible('#app', 1000)

  }

}

第二个参数是在考虑测试失败之前愿意等待的毫秒数。

接下来,我们希望确保标题也正确显示,并且没有可见的<p>元素:

module.exports = {

  'Happy scenario' :client => {

    client

      .url('http://localhost:8080')

      .waitForElementVisible('#app', 1000)

      .assert.containsText('h2', 'Welcome to')

 .assert.hidden('p')

  }

}

然后,我们点击按钮并断言<p>元素是可见的并且包含单词Nightwatch

module.exports = {

  'Happy scenario' :client => {

    client

      .url('http://localhost:8080')

      .waitForElementVisible('#app', 1000)

      .assert.containsText('h2', 'Welcome to')

      .assert.hidden('p')

      .click('button')

 .waitForElementVisible('p', 1000)

 .assert.containsText('p', 'Nightwatch')

 .end();

  }

}

end()函数将标记测试已成功,因为没有更多需要检查的内容。

要实际运行此测试,您需要运行以下命令:

selenium-standalone install

这将安装 Selenium,然后打开三个不同的命令行。在第一个命令行中,使用以下命令启动 Selenium 服务器:

selenium-standalone start

在第二个命令行中,进入你的食谱文件夹的根目录,即index.html所在的位置,并启动http-server

http-server .

启动后,它会告诉你你的网站在http://localhost:8080上提供服务。这就像我们在测试中写的地址一样。你现在可以导航到该地址查看应用程序的运行情况:

最后,在第三个命令行中,再次进入你的食谱文件夹,并输入以下命令:

nightwatch

如果一切顺利,你会看到浏览器在你眼前闪烁,并在一瞬间(取决于你的计算机速度)显示应用程序,在控制台中,你应该看到类似于这样的内容:

工作原理...

如果这个食谱看起来很费劲,不要灰心,Vue 模板已经在其中解决了所有的设置。你知道所有这些机制是如何工作的,但是当我们在后面的食谱中使用 Webpack 时,你只需要一个命令来运行端到端测试,因为一切都已经设置好了。

注意端到端测试的标题是相当通用的,它指的是一个特定的操作流程,而不是详细描述上下文和期望的内容。这在端到端测试中很常见,因为通常最好先构建用户故事,然后将它们分支,并为每个分支命名一个特定的场景。所以,举个例子,如果我们期望从服务器得到一个响应,但没有返回,我们可以测试一个场景,在这个场景中我们会出现一个错误,并将测试称为服务器错误场景

在 nightwatch 中模拟双击

这个食谱对于那些在 nightwatch 中模拟双击而苦苦挣扎的人来说是一种享受。作为其中之一,我对此表示同情。事实证明,在 nightwatch 中有一个doubleClick函数,但至少在作者的意见中,它并不像预期的那样工作。

准备就绪

这个配方适用于刚开始使用 nightwatch 并且在这个特定问题上遇到困难的开发人员。你想学习如何模拟双击进行测试,但你不了解 nightwatch 吗?回到上一个配方。

我假设你已经设置好了 nightwatch,并且可以启动测试。我还假设你已经安装了前面配方中的所有命令。

工作原理...

假设你有一个 Vue 应用程序,它在一个index.html文件中:

<!DOCTYPE html>

<html>

<head>

  <title>7.6</title>

  <script src="https://unpkg.com/vue/dist/vue.js"></script>

</head>

<body>

  <div id="app">

    <h2>Welcome to my test page</h2>

    <button id="showBtn" @dblclick="show = true">

      Show

    </button>

    <p v-show="show">Hello Nightwatch!</p>

  </div>

</body>

</html>

<div>元素之后,添加以下脚本:

<script>

  const vm = new Vue({

    el: '#app',

    data: {

      show: false

    }

  })

</script>

你可以使用http-server来提供你的应用程序。在浏览器中打开http://localhost:8080,然后尝试双击按钮以使文本出现。

现在,如果我们想要测试这个,我们查看 nightwatch 的 API,发现它有一个名为doubleClick()的函数调用。

然后我们可以编写一个类似于前面配方中的测试:

'Happy scenario' : function (client) {

  client

    .url('http://localhost:8080')

    .waitForElementVisible('#app', 1000)

    .assert.containsText('h2', 'Welcome to')

    .assert.hidden('p')

    .doubleClick('button') // not working

    .waitForElementVisible('p', 1000)

    .assert.containsText('p', 'Nightwatch')

    .end();

 }

除了这个不会按预期工作。正确的方法是:

'Happy scenario' : function (client) {

  client

    .url('http://localhost:8080')

    .waitForElementVisible('#app', 1000)

    .assert.containsText('h2', 'Welcome to')

    .assert.hidden('p')

    .moveToElement('tag name', 'button', 0, 0)

 .doubleClick()

    .waitForElementVisible('p', 1000)

    .assert.containsText('p', 'Nightwatch')

    .end();

 }

只有在你首先移动到你想要双击的元素上时,双击才起作用;只有这样,你才能调用doubleClick而不带任何参数。

工作原理...

moveToElement函数的参数如下:

  • selector:我们使用tag name作为选择器

  • tag / selector:我们寻找button标签;如果我们在这里使用了另一个选择器,我们会使用不同的格式

  • xoffset:这是虚拟鼠标在 x 坐标上的位置;对我们来说,0 是可以的,因为即使在按钮的边缘,点击也是有效的

  • yoffset:这与前面的参数类似,但在 y 轴上

在正确位置后,有一系列的命令可以释放事件。我们使用了doubleClick,但还有其他命令。

不同风格的单元测试

我们在之前的示例中发现并使用了 Jasmine。在这个示例中,我们将探索和比较不同的单元测试风格。这是特别相关的,因为 Vue 模板预装了 Mocha 和 Chai。Chai 使您能够以三种不同的风格编写测试。

准备工作

这个示例不需要任何特定的先前知识,但我强烈建议您完成“使用 Jasmine 进行 Vue 测试”的示例。

操作步骤

为了使这个示例工作,您需要两个依赖项:Mocha 和 Chai。您可以在 Google 上很快找到它们;只需记住,Mocha 有两个不同的文件:mocha.jsmocha.css。如果您希望显示得漂亮,您必须同时添加它们。

如果您正在使用 JSFiddle,请按照通常的方式继续;否则,请确保依赖项中也有 Vue。

我们的 HTML 布局将如下所示:

<div id="app">

  <p>{{greeting}}</p>

</div>

<div id="mocha">

</div>

mocha 部分是显示所有结果的地方。

在 JavaScript 部分,编写最简单的Vue应用程序并将其分配给一个变量:

const vm = new Vue({

  el: '#app',

  data: {

    greeting: 'Hello World!'

  }

})

我们将编写一个测试来查看Hello world文本是否真的被显示出来。

Vue应用程序完成后,写入以下内容:

mocha.setup('bdd')

chai.should()

describe('my app', () => {

  it('should say Hello World', () => {

    vm.$el.innerText.should.contain('Hello World')

  })

})

mocha.run()

上述代码准备了mochachai(通过安装describeitshould函数),然后断言我们组件的内部文本应该包含Hello World。相当易读,不是吗?

您可以启动应用程序,然后您将看到这个:

chai 还允许我们以另外两种方式编写完全相同的测试。第一种方式如下所示:

 vm.$el.innerText.should.contain('Hello World')

要使用第二种方式,您必须在之前添加const expect = chai.expect

expect(vm.$el.innerText).to.contain('Hello World')

最后,在之前添加const assert = chai.assert行:

assert.include(vm.$el.innerText,

  'Hello World',

  'Component innerText include Hello World')

在 assert 风格中,将消息作为附加参数添加是惯用的做法,以使测试在出现问题时更加详细。

它是如何工作的...

Chai 是一个简单的库,它实现了一些函数,并在某些条件不满足时抛出异常。另一方面,Mocha 运行某些代码片段,收集异常,并尝试以友好的方式向用户显示它们。

虽然使用哪种风格主要是品味问题,但这三种风格之间存在一些细微的差别。

  • Should更加雄辩和可读。不幸的是,它扩展了Object,将should函数添加到了所有对象上。如果你不知道如何对待最后一句话,你不应该介意,但正确的行为方式是奔跑并尖叫痛苦;永远不要扩展Object

  • Assert意味着对每个断言编写详细的描述,如果你为每个测试编写多个断言,这通常是很好的。就个人而言,我认为每个测试最多只应该有一个断言,并且应该集中在标题上进行描述。

  • Expect不扩展Object,非常可读且平衡良好,通常我更喜欢使用它而不是其他替代方案。

使用 Sinon.JS 进行外部 API 调用的存根化

通常,在进行端到端测试和集成测试时,您将运行并准备好后端服务器以响应您的请求。我认为有很多情况下这是不可取的。作为前端开发人员,您会抓住每一个机会责怪后端人员。

准备工作

完成这个示例不需要特殊的技能,但您应该将 Jasmine 安装为依赖项;这在使用 Jasmine 进行 Vue 测试示例中有详细说明。

如何操作...

首先,让我们安装一些依赖项。对于这个示例,我们将使用 Jasmine 来运行整个测试;您可以在使用 Jasmine 进行 Vue 测试示例中找到详细的说明(这四个文件分别是jasmine.cssjasmine.jsjasmine-html.jsboot.js,按照这个顺序)。

在继续之前,还要安装 Sinon.JS 和 Axios;您只需要添加与它们相关的js文件。

我们将构建一个在点击按钮时检索帖子的应用程序。在 HTML 部分中,编写以下内容:

<div id="app">

  <button @click="retrieve">Retrieve Post</button>

  <p v-if="post">{{post}}</p>

</div>

相反,JavaScript 部分将如下所示:

const vm = new Vue({

  el: '#app',

  data: {

    post: undefined

  },

  methods: {

  retrieve () {

    axios

      .get('https://jsonplaceholder.typicode.com/posts/1')

      .then(response => {

        console.log('setting post')

        this.post = response.data.body

      })

    }

  }

})

如果您现在启动应用程序,应该能够看到它正在工作:

现在我们想要测试应用程序,但我们不想连接到真实的服务器。这将需要额外的时间,并且不可靠;相反,我们将从服务器获取一个正确的样本响应并使用它。

Sinon.JS 有一个沙盒的概念。这意味着每当一个测试开始时,一些依赖项(如 Axios)都会被覆盖。每个测试结束后,我们可以丢弃沙盒,一切都恢复正常。

使用 Sinon.JS 的空测试如下所示(在Vue实例之后添加):

describe('my app', () => {

  let sandbox

  beforeEach(() => sandbox = sinon.sandbox.create())

  afterEach(() => sandbox.restore())

})

我们想要为 axios 的get函数存根调用:

describe('my app', () => {

  let sandbox

  beforeEach(() => sandbox = sinon.sandbox.create())

  afterEach(() => sandbox.restore())

  it('should save the returned post body', done => {

    const promise = new Promise(resolve => 

 resolve({ data: { body: 'Hello World' } })

 )

 sandbox.stub(axios, 'get').returns(promise)

 ...

 done()

 })

})

我们在这里覆盖了 axios。我们说现在get方法应该返回resolved的 promise:

describe('my app', () => {

  let sandbox

  beforeEach(() => sandbox = sinon.sandbox.create())

  afterEach(() => sandbox.restore())

 it

('

should save the returned post body'

,

 done

 =>

 {

    const promise = new Promise(resolve => 

      resolve({ data: { body: 'Hello World' } })

    )

    sandbox

.

stub

(

axios

,

 'get'

).

returns

(

promise

)

    vm

.

retrieve

()

    promise.then(() => {

      expect

(

vm

.

post

).

toEqual

(

'Hello World'

)

      done

()

    }) 

  }) 

})

由于我们返回了一个 promise(我们需要返回一个 promise,因为retrieve方法正在调用它的then方法),所以我们需要等待它解析。

我们可以启动页面并查看它是否工作:

如果您使用 JSFiddle,请记住将加载类型设置为 No wrap - in <body>,否则 Vue 将没有机会启动。

它是如何工作的...

在我们的案例中,我们使用沙盒来存根其中一个依赖项的方法。这样,axios 的get方法就不会被触发,我们会收到一个类似于后端将给我们的对象。

存根 API 响应将使您与后端及其怪癖隔离开来。如果出现问题,您不会在意,而且您可以在不依赖于后端正确运行的情况下运行测试。

有许多库和技术可以存根 API 调用,不仅与 HTTP 相关。希望这个示例为您提供了一个起步。

测量代码的覆盖率

代码覆盖率是评估软件质量最常用和易于理解的指标之一。如果一个测试执行了特定的代码部分,那么该代码被认为是被覆盖的。这意味着该特定代码部分正常工作且包含错误的可能性较小。

准备就绪

在测量代码覆盖率之前,请确保完成“将一些 Karma 添加到你的工作流程”这个步骤,因为我们将使用 Karma 来帮助我们。

如何操作...

创建一个新目录,并在其中放置一个名为package.json的文件。在其中写入以下内容:

{

 "name": "learning-code-coverage",

 "version": "1.0.0"

}

这将创建一个 npm 项目。在同一目录中,运行以下命令来安装我们的依赖项:

npm install vue karma karma jasmine karma-jasmine karma-coverage karma-chrome-launcher --save-dev

package.json文件会相应地更改。

karma-coverage插件使用底层软件 Istanbul 来测量和显示我们的测试覆盖率。

为了使下一步更容易一些,我们将全局安装 Karma(如果你还没有安装)。运行以下命令:

npm install -g karma

当安装了 Karma 后,在你的目录中运行以下命令;它将创建一个 Karma 配置文件:

karma init

除非它要求你加载文件,否则回答所有问题的默认值;在这种情况下,写下以下两行:

  • node_modules/vue/dist/vue.js

  • *.js

在此之后留一个空行以确认。

这将加载 Vue 和以js扩展名结尾的所有文件到目录的根目录中。

打开 Karma 创建的文件;它应该被称为karma.conf.js,并且应该与其他文件一起在你的目录中。

应该有一个类似以下的部分:

preprocessors: {

},

在 preprocessors 对象中,插入 coverage,如下所示:

preprocessors: {

  'myApp.js': ['coverage']

},

这意味着我们想要使用 coverage 预处理器对myApp.js文件进行预处理。myApp.js文件将包含我们要测试的应用程序。

紧接着,在reporters数组中添加 coverage:

reporters: ['progress', 'coverage'

],

这将使 coverage 报告生成一个包含覆盖率测量的网页。

为了使设置正常工作,您需要在frameworksfiles之间设置另一个属性,称为plugins

plugins: [

 'karma-jasmine',

 'karma-coverage',

 'karma-chrome-launcher'

],

接下来,我们将编写一个简单的 Vue 应用程序进行测试。

创建一个名为myApp.js的文件;我们将创建一个猜数字游戏。

在文件中写入以下内容:

const myApp = {

  template: `

    <div>

      <p>

        I am thinking of a number between 1 and 20.

      </p>

      <input v-model="guess">

      <p v-if="guess">{{output}}</p>

    </div>

  `

}

用户将输入一个数字,输出将显示一个提示或者一个文本来庆祝胜利,如果数字正确。将以下状态添加到myApp对象中:

data: {

  number: getRandomInt(1, 20),

  guess: undefined

}

在文件的顶部,您可以添加一个名为getRandomInt的函数,如下所示:

function getRandomInt(min, max) {

  return Math.floor(Math.random() * (max - min)) + min;

}

我们还需要一个计算属性来显示提示:

computed: {

  output () {

    if (this.guess < this.number) {

      return 'Higher...'

    }

    if (this.guess > this.number) {

      return 'Lower...'

    }

    return 'That's right!'

  }

}

我们的应用程序已经完成。让我们测试一下它是否按预期工作。

在目录的根目录下创建一个名为test.js的文件,并编写以下测试:

describe('my app', () => {

  let vm

  beforeEach(() => {

    vm = new Vue(myApp).$mount()

    vm.number = 5

  })

  it('should output That's right! if guess is 5', () => {

    vm.guess = 5

    expect(vm.output).toBe('That's right!')

  })

})

要运行测试,请使用以下命令:

karma start

如果前面的命令在已经安装了karma-coverage插件的情况下没有要求安装该插件,您可以全局安装插件,或者使用本地安装的 Karma 从./node-modules/karma/bin/karma start运行测试。

如果您的浏览器打开了,请返回控制台,当测试完成时,按下Ctrl + C停止 Karma。

如果一切顺利,您应该会看到一个名为 coverage 的新文件夹,其中包含一个名为 Chrome 的目录。您还应该在其中找到一个名为index.html的文件。打开它,您会看到一个类似于这样的页面:

从一开始,我们就可以看到黄色表示出现了问题。我们测试了 100%的函数,但只测试了 50%的 if 分支。

如果您浏览并打开myApp.js文件的详细信息,您会发现我们没有测试if语句的两个分支:

这些分支内部可能会出现错误,我们甚至可能不知道!

尝试在测试文件中添加以下两个测试:

it('should output Lower... if guess is 6', () => {

  vm.guess = 6

  expect(vm.output).toBe('Lower...')

})

it('should output Higher... if guess is 4', () => {

  vm.guess = 4

  expect(vm.output).toBe('Higher...')

})

现在,如果您运行测试并打开报告,它看起来会更加绿色:

工作原理如下...

我们甚至没有打开应用程序,但我们已经非常确定它能正常工作,这要归功于我们的测试。

此外,我们有一份报告显示我们覆盖了 100%的代码。尽管我们只测试了猜数字游戏的三个数字,但我们覆盖了所有可能的分支。

我们永远无法确定我们的软件没有错误,但这些工具对我们开发人员在添加功能到我们的软件时非常有帮助,而不必担心可能会出现问题。

第七章:组织+自动化+部署=Webpack

在本章中,我们将讨论以下主题:

  • 从组件中提取逻辑以保持代码整洁

  • 使用 Webpack 打包您的组件预览

  • 使用 Webpack 组织您的依赖项

  • 在 Webpack 项目中使用外部组件

  • 使用热重载进行连续反馈的开发

  • 使用 Babel 编译 ES6

  • 在开发过程中运行代码检查器

  • 只使用一个命令构建压缩和开发.js 文件

  • 将您的组件发布到公共场所

引言

Webpack 与 npm 结合是一个非常强大的工具。本质上,它只是一个打包工具,将一些文件及其依赖项打包成一个或多个可消费的文件。它现在已经进入第二个版本,并且对于 Vue 开发人员来说比以前更重要。

Webpack 将使您能够方便地在单个文件中编写组件,并可通过命令进行发布。它将使您能够使用不同的 JavaScript 标准,如 ES6,以及其他语言,这都要归功于加载器,这个概念将在后续的示例中反复出现。

从组件中提取逻辑以保持代码整洁

Vue 组件有时可能变得非常复杂。在这些情况下,最好将它们拆分并尝试使用抽象隐藏一些复杂性。放置此类复杂性的最佳位置是外部 JavaScript 文件。这样做的好处是,如果需要的话,更容易与其他组件共享提取的逻辑。

准备工作

这个示例是中级水平的。在来到这里之前,您应该已经完成了第一章中的“选择开发环境”示例,以及“开始使用 Vue.js”,并且应该知道如何使用 npm 设置项目。

还要确保您已经全局安装了vue-cli包,使用以下命令:

npm install -g vue-cli

如何做...

我们将构建一个复利计算器;您将发现在初始投资后您将拥有多少钱。

创建一个干净的 Webpack 项目

创建一个新目录,并使用以下命令在其中创建一个新的Vue项目:

vue init webpack

您可以选择问题的默认值。

运行npm install来安装所有所需的依赖项。

然后,导航到目录结构中的src/App.vue,并删除文件中的几乎所有内容。

最终结果应该如下所示:

<template>

  <div id="app">

  </div>

</template>

<script>

export default {

  name: 'app'

}

</script>

<style>

</style>

我已经为您完成了这个任务,您可以使用以下命令来使用另一个模板:

vue init gurghet/webpack

构建复利计算器。

要构建复利计算器,您需要三个字段:初始资本或本金、年利率和投资期限。然后,您将添加一个输出字段来显示最终结果。以下是相应的 HTML 代码:

<div id="app">

  <div>

    <label>principal capital</label>

    <input v-model.number="principal">

  </div>

  <div>

    <label>Yearly interestRate</label>

    <input v-model.number="interestRate">

  </div>

  <div>

    <label>Investment length (timeYears)</label>

    <input v-model.number="timeYears">

  </div>

  <div>

    You will gain:

    <output>{{final}}</output>

  </div>

</div>

我们使用.number修饰符,否则我们放入的数字将被 JavaScript 转换为字符串。

在 JavaScript 部分,通过编写以下代码声明三个模型变量:

export default {

  name: 'app',

  data () {

    return {

      principal: 0,

      interestRate: 0,

      timeYears: 0

    }

  }

}

为了计算复利,我们采用数学公式:

在 JavaScript 中,可以这样写:

P * Math.pow((1 + r), t)

您需要将此添加到Vue组件中作为计算属性,如下所示:

computed: {

  final () {

    const P = this.principal

    const r = this.interestRate

    const t = this.timeYears

    return P * Math.pow((1 + r), t)

  }

}

您可以使用以下命令运行应用程序(从您的目录启动):

npm run dev

现在我们的应用程序可以工作了,您可以看到将 0.93 美元存入一个年利率为 2.25%的银行账户并沉睡 1000 年后我们将获得多少钱(43 亿美元!):

目前,代码中的公式并不重要。但是,如果我们有另一个组件也执行相同的计算,我们也希望更明确地表明我们正在计算复利,而不关心此范围内的公式实际上是什么。

src文件夹中创建一个名为compoundInterest.js的新文件;在其中编写以下代码:

export default function (Principal, yearlyRate, years) {

  const P = Principal

  const r = yearlyRate

  const t = years

  return P * Math.pow((1 + r), t)

}

然后相应地修改App.vue中的代码:

computed: {

  final () {

    return compoundInterest(

 this.principal,

 this.interestRate,

 this.timeYears

 )

  }

}

另外,请记住在 JavaScript 部分的顶部导入我们刚刚创建的文件:

<script>

 import compoundInterest from './compoundInterest'

  export default {

  ...

它是如何工作的...

在组件中工作或者一般编程时,将代码的范围减小到只有一个抽象层级会更好。当我们编写一个计算函数来返回最终的资本值时,我们只需要担心调用正确的函数-适用于我们目的的函数。公式的内部在更低的抽象层级上,我们不想处理它。

我们所做的是将所有繁琐的计算工作放在一个单独的文件中。然后,我们使用以下代码从文件中导出函数:

export default function (Principal, yearlyRate, years) {

...

这样,当我们从Vue组件导入文件时,默认情况下该函数可用:

import compoundInterest from './compoundInterest'

...

所以,现在compoundInterest是我们在另一个文件中定义的函数。此外,这种关注点的分离使我们能够在代码的任何地方使用此函数来计算复利,甚至在其他文件中(潜在地也可以是其他项目中)。

使用 Webpack 打包您的组件

Webpack 允许您将项目打包成压缩的 JavaScript 文件。然后,您可以分发这些文件或自己使用它们。当您使用vue-cli附带的内置模板时,Webpack 会配置为构建一个完整的工作应用程序。有时我们想要构建一个库以发布或在另一个项目中使用。在这个示例中,您将调整 Webpack 模板的默认配置以发布一个组件。

准备工作

只有在您安装了 npm(参考第一章中的选择开发环境示例,开始使用 Vue.js)并熟悉vue-cli和 Webpack 模板后,这个示例才会有意义。

如何做...

对于这个教程,您将构建一个可重用的组件,可以抖动您放入其中的任何内容;为此,我们将使用优秀的 CSShake 库。

基于 Webpack 模板创建一个新的干净项目。您可以查看之前的教程来了解如何做到这一点,或者您可以使用我制作的预构建模板。您可以通过创建一个新目录并运行此命令来使用我的模板:

vue init gurghet/webpack

如果您不知道它们的含义,请选择默认答案。记得运行npm install来引入依赖项。

首先,让我们重命名一些东西:将App.vue文件重命名为Shaker.vue

在其中,将以下内容作为 HTML 模板写入:

<template>

  <span id="shaker" class="shake">

    <link rel="stylesheet" type="text/css" href="https://csshake.surge.sh/csshake.min.css">

    <slot></slot>

  </span>

</template>

请注意,与原始模板相比,我们将<div>更改为<span>。这是因为我们希望我们的抖动器成为一个内联组件。

组件本身已经完成了;我们只需要在 JavaScript 部分进行一些微小的美化编辑:

<script>

  export default {

    name: 'shaker

'

  }

</script>

为了手动测试我们的应用程序,我们可以按照以下方式修改main.js文件(突出显示的文本是修改后的代码):

// The Vue build version to load with the `import` command

// (runtime-only or standalone) has been set in webpack.base.conf with an alias.

import Vue from 'vue'

import Shaker from './Shaker'

/* eslint-disable no-new */

new Vue({

  el: '#app',

 template: `

    <div>

      This is a <Shaker>test</Shaker>

    </div>

  `, 

  components: { Shaker

 }

})

这将创建一个示例页面,如下图所示,在其中我们可以使用热重载来原型化我们的组件。通过运行以下命令来启动它:

npm run dev

将光标放在单词test上应该使其抖动。

现在,我们希望将此组件打包到一个单独的 JavaScript 文件中,以便将来可以重用。

默认模板中没有此配置,但很容易添加一个。

首先,在build文件夹中的webpack.prod.js文件中进行一些修改。

让我们摆脱一些我们在发布库时不需要的插件;找到文件中的plugins数组。它是一个包含以下代码形式的插件数组:

plugins: [

  new Plugin1(...),

  new Plugin2(...),

  ...

  new PluginN(...)

]

我们只需要以下插件:

  • webpack.DefinePlugin

  • webpack.optimize.UglifyJsPlugin

  • webpack.optimize.OccurrenceOrderPlugin

摆脱所有其他插件,因为我们不需要它们;最终的数组应该是这样的:

plugins: [

  new webpack.DefinePlugin({

    'process.env': env

  }),

  new webpack.optimize.UglifyJsPlugin({

    compress: {

      warnings: false

    }

  }),

  new webpack.optimize.OccurrenceOrderPlugin()

]

第一个允许您添加一些其他配置,第二个插件将文件进行了缩小,第三个插件将优化生成文件的大小。

我们还需要编辑的另一个属性是output,因为我们希望简化输出路径。

原始属性如下所示:

output: {

  path: config.build.assetsRoot,

  filename: utils.assetsPath('js/[name].[chunkhash].js'),

  chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')

}

原始代码会创建一个js目录中的一系列输出文件。方括号中有一些变量;因为您只有一个自包含模块用于我们的应用程序,我们将其称为shaker。我们需要获得以下代码:

output: {

  path: config.build.assetsRoot,

 filename: utils.assetsPath('shaker.js') 

}

由于,正如刚才所说,您希望组件是自包含的,我们需要进行一些其他的修改,这也取决于您的需求。

如果您希望组件内置任何 CSS 样式(在我们的情况下,我们使用的是外部 CSS 库),您应该禁用ExtractTextPlugin;我们已经从列表中删除了该插件,但仍有其他文件在使用它。在vue-loader.conf.js文件中找到extract选项(某些版本中的vue部分)并将其替换为以下代码:

... {

  loaders: utils.cssLoaders({

    ...

    extract: false

  })

}

我们的组件通常会包含 Vue 库;如果您想在 Vue 项目中使用该组件,您不需要这个库,因为它会造成重复的代码。您可以告诉 Webpack 只在外部搜索依赖项而不包含它们。在您刚刚修改的webpack.prod.js文件中的plugins之前添加以下属性:

externals: {

  'vue': 'Vue'

}

这将告诉 Webpack 不将 Vue 库写入捆绑包中,而是只需使用一个全局的名为Vue的变量,在我们的代码中导入vue依赖项时使用它。Webpack 配置几乎完成了;我们只需要在module属性之前添加另一个属性:

var webpackConfig = merge(baseWebpackConfig, {

  entry: {

 app: './src/dist.js'

 },

  module: {

  ...

这将从dist.js文件开始编译代码。等一下,这个文件还不存在。让我们创建它并在内部添加以下代码:

import Vue from 'vue'

import Shaker from './Shaker'

Vue.component('shaker', Shaker)

在最终的 JavaScript 压缩文件中,Vue 依赖将被外部引用,然后我们将组件全局注册。

作为最后的更改,我建议修改保存压缩文件的文件夹。在config/index.js文件中,编辑以下行:

assetsSubDirectory: 'static',

将上述行与以下代码进行交换:

assetsSubDirectory: '.',

现在使用 npm 运行命令来构建压缩文件:

npm run build

您将看到一个类似于以下内容的输出:

为了测试我们的文件,我们可以使用 JSFiddle。

复制您在dist/shaker.js中创建的文件的内容,然后转到gist.github.com/(您可能需要注册),将文件的内容粘贴到文本区域内。将其命名为shaker.js

由于文本是单行的,使用“No wrap”选项时您将看不到太多内容。点击“创建公共 gist”,当您看到下一页时,点击“Raw”,如下图所示:

复制地址栏中的 URL,然后转到rawgit.com/,在那里您可以粘贴链接:

点击并复制右侧获得的链接。恭喜,您刚刚将您的组件发布到了 Web 上!

现在转到 JSFiddle 并选择 Vue 作为库。您现在可以在左侧添加您复制的链接,您的组件就可以使用了:

工作原理...

官方模板中的 Webpack 配置相当复杂。另一方面,不要试图一下子理解所有内容,否则您会陷入困境,也无法学到太多东西。

我们创建了一个UMD通用模块定义)模块,它将尝试查看是否有可用的 Vue 依赖,并将自身安装为一个组件。

您甚至可以为您的组件添加 CSS 和样式,我们配置的 Webpack 将会将样式与组件一起发布。

还有更多...

在本章的“将组件发布到公共场所”示例中,您将学习如何将组件发布到 npm 发布注册表中。我们将使用与此不同的方法,但您可以在那里找到将其发布到注册表的缺失步骤。

使用 Webpack 组织您的依赖关系

Webpack 是一个用于组织代码和依赖关系的工具。此外,它还提供了一种开发和构建 JavaScript 文件的方式,这些文件包含了我们传递给它们的所有依赖和模块。我们将在这个示例中使用它来构建一个小型的 Vue 应用程序,并将所有内容打包到一个单独的文件中。

准备就绪

这个示例不需要任何特殊的技能,只需要使用 npm 和一些命令行的知识。您可以在本章的“使用 Webpack 组织您的依赖关系”示例中了解更多信息。

如何做到...

为您的示例创建一个新的文件夹,并在其中创建一个包含以下内容的package.json文件:

{

 "name": "recipe",

 "version": "1.0.0"

}

这在我们的文件夹中定义了一个 npm 项目。当然,如果你知道你在做什么,你可以使用npm inityarn init

我们将为这个示例安装 Webpack 2。要将其添加到您的项目依赖项中,请运行以下命令:

npm install --save-dev webpack@2

--save-dev选项意味着我们不会在最终产品中发布 Webpack 的代码,而只会在开发过程中使用它。

创建一个新的app目录,并在其中创建一个App.vue文件。

这个文件将是一个简单的Vue组件;它可以像下面这样简单:

<template>

  <div>

    {{msg}}

  </div>

</template>

<script>

export default {

  name: 'app',

  data () {

    return {

      msg: 'Hello world'

    }

  }

}

</script>

<style>

</style>

我们需要告诉 Webpack 如何将.vue文件转换为.js文件。为此,我们在根文件夹中创建一个名为webpack.config.js的配置文件;这个文件将被 Webpack 自动识别。在这个文件中,写入如下内容:

module.exports = {

  module: {

    rules: [

      {test: /.vue$/, use: 'vue-loader'}

    ]

  }

}

规则中的行表示以下内容:

嘿 Webpack,当你看到一个以.vue结尾的文件时,使用vue-loader将其转换为 JavaScript 文件。

我们需要使用以下命令使用 npm 安装这样的加载器:

npm install --save-dev vue-loader

此加载器内部使用其他依赖项,这些依赖项不会自动安装;我们需要通过运行以下命令手动安装它们:

npm install --save-dev vue-template-compiler css-loader

我们还可以趁此机会安装 Vue 本身:

npm install --save vue

现在我们的Vue组件已经准备好了。我们需要编写一个页面来放置它并尝试它。在app文件夹中创建一个名为index.js的文件。我们将在 Vue 实例中实例化该组件。在index.js中写入以下内容:

import Vue from 'vue'

import App from './App.vue'

new Vue({

  el: '#app',

  render: h => h(App)

})

这将在具有id="app"的元素内挂载 Vue 实例,并且它将包含一个单独的组件-我们的App.vue

我们还需要一个 HTML 文件。在根目录中创建index.html,并使用以下代码:

<!DOCTYPE html>

<html>

  <head>

    <title>Webpack 2 demo</title>

  </head>

  <body>

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

    <script src="dist/bundle.js"></script>

  </body>

</html>

我们不想直接引用app/index.js,这是因为index.js本身并不包含太多内容。它有一个浏览器无法识别的import语句。Webpack 可以轻松地创建包含index.js及其所有依赖项的dist/bundle.js。要做到这一点,请运行以下命令:

./node_modules/webpack/bin/webpack.js app/index.js dist/bundle.js

这应该生成类似于以下内容的输出:

现在,您可以打开index.html,您将看到组件正在工作。

然而,每次都运行这个长命令并不那么有趣。Webpack 和 npm 可以做得更好。

webpack.config.js中添加以下属性:

module.exports = {

  entry: './app/index.js',

 output: {

 filename: 'bundle.js',

 path: __dirname + '/dist'

 },

  module: {

  ...

这将指定 Webpack 的入口点和结果文件的保存位置。

我们还可以在package.json中添加一个脚本:

"scripts": {

  "build": "webpack"

}

现在,运行npm run build将产生与我们使用的长命令相同的效果。

它是如何工作的...

在这个示例中,我们基本上创建了一个同时包含 Vue 和我们编写的组件的 JavaScript 文件(bundle.js)。在index.html中,没有 Vue 的痕迹,因为它被嵌入在bundle.js中。

当我们有很多依赖关系时,这种工作方式要好得多。我们不再需要在页面的头部或正文中添加很多标签。此外,我们不必担心加载不需要的依赖关系。

作为额外的奖励,Webpack 具有将我们的最终文件和其他高级优化压缩的能力和灵活性,这是通过手动加载依赖项无法实现的。

在您的 Webpack 项目中使用外部组件

在自己的项目中使用外部 Vue 组件通常很简单。然而,有时候事情并不那么简单。特别是,在官方模板中有一些 Webpack 的配置(奇怪的是)实际上会阻止您使用某些外部组件。在这个示例中,我们将安装 Bulma 项目中的一个模态对话框组件。

准备工作

在这个示例中,我们将调整 Webpack 配置。建议在开始此任务之前完成“使用 Webpack 组织依赖项”示例。

如何操作...

我们将从一个全新的 Webpack 项目开始。您可以使用vue-cli和官方 Webpack 模板创建一个新项目。然而,我建议您使用我的 Webpack 模板开始,这是一个干净的模板。要这样做,请在一个新的目录中运行以下命令:

vue init gurghet/webpack

我们将安装vue-bulma-modal,这是一个使用 Bulma CSS 框架编写的 Vue 组件:

npm install --save vue-bulma-modal bulma

在上述命令中,我们还安装了bulma,其中包含实际的 CSS 样式。

为了使样式起作用,我们需要将它们转换为 Webpack 的 JavaScript 代码;这意味着我们需要安装一些加载器:

npm install --save-dev node-sass sass-loader

SASS 加载器已经配置好了,所以不需要做任何修改。但我们将修改与 Babel 加载器相关的 Webpack 配置(在“使用热重载进行连续反馈开发”示例中了解更多信息)。

在官方模板中(但这可能会改变,请注意),有一行代码阻止 Webpack 编译依赖项。打开build/webpack.base.conf.js文件,找到以下代码块:

{

  test: /.js$/,

  loader: 'babel-loader',

  include: [

    path.join(projectRoot, 'src')

  ],

 exclude: /node_modules/

},

根据您使用的 Webpack 版本,您可能需要稍微调整加载器语法。例如,在旧版本的 Webpack 中,您需要写babel而不是babel-loader

您必须删除突出显示的行,并改为编写以下内容:

{

  test: /.js$/,

  loader: 'babel-loader',

  include: [

    path.join(projectRoot, 'src'),

 path.join(projectRoot, 'node_modules/vue-bulma-modal')

  ]

},

这告诉 Webpack 使用babel-loader编译我们刚刚安装的组件。

现在,在App.vue中编写以下 HTML 布局:

<template>

  <div id="app">

    <card-modal

      @ok="accept"

      ok-text="Accept"

      :visible="popup"

      @cancel="cancel"

    >

      <div class="content">

        <h1>Contract</h1>

          <p>

            I hereby declare I have learned how to

            install third party components in my

            own Vue project.

          </p>

        </div>

      </card-modal>

    <p v-if="signed">It appears you signed!</p>

  </div>

</template>

然后,您可以按照 JavaScript 中所示的逻辑编写代码:

<script>

import { CardModal } from 'vue-bulma-modal'

export default {

  name: 'app',

  components: { CardModal },

  data () {

    return {

      signed: false,

      popup: true

    }

  },

  methods: {

    accept () {

      this.popup = false

      this.signed = true

    },

    cancel () {

      this.popup = false

    }

  }

}

</script>

要实际使用 Bulma 样式,我们需要启用 SASS 加载器并导入bulma文件。添加以下行:

<style lang="sass">

@import '~bulma';

</style>

请注意,我们在第一行中指定了样式的语言(我们编写的是 SCSS,但在这种情况下我们按原样编写)。

如果您现在尝试使用npm run dev命令运行应用程序,您将看到 Bulma 模态对话框以其全部辉煌出现:

工作原理如下...

官方的 Webpack 模板包含了一个配置规则,不要编译node_modules目录中的文件。这意味着 Web 组件的作者被鼓励分发一个已经编译好的文件,否则用户将在他们的项目中导入原始的 JavaScript 文件(因为 Webpack 不会编译它们),从而在浏览器中引发各种错误。就个人而言,我认为这不是一个好的工程实践。这种设置的一个问题是,由于您在项目中导入的文件是针对一个版本的 Vue 进行编译的,所以如果您使用较新版本的 Vue,组件可能无法正常工作(实际上过去确实发生过这种情况)。

更好的方法是导入原始文件和组件,让 Webpack 将它们编译成一个单独的文件。不幸的是,大多数在外部可用的组件都已经编译好了,所以虽然使用官方模板很快就可以导入它们,但更有可能遇到兼容性问题。

在导入外部组件时,首先要做的是检查它们的package.json文件。让我们看看vue-bulma-modal包在此文件中包含了什么:

{

  "name": "vue-bulma-modal",

  "version": "1.0.1",

  "description": "Modal component for Vue Bulma",

 "main": "src/index.js", 

  "peerDependencies": {

    "bulma": ">=0.2",

    "vue": ">=2"

  },

  ...

  "author": "Fangdun Cai <cfddream@gmail.com>",

  "license": "MIT"

}

当我们在 JavaScript 中编写以下行时,main属性引用的文件是我们导入的文件:

import { CardModal } from 'vue-bulma-modal'

src/index.js文件中包含以下代码:

import Modal from './Modal'

import BaseModal from './BaseModal'

import CardModal from './CardModal'

import ImageModal from './ImageModal'

export {

  Modal,

  BaseModal,

  CardModal,

  ImageModal

}

这不是一个编译文件;它是原始的 ES6 代码,我们知道这一点是因为在常规的 JavaScript 中没有定义import。这就是为什么我们需要 Webpack 来为我们编译它。

另一方面,考虑到我们编写了以下内容:

<style lang="sass">

@import '~bulma';

</style>

使用波浪号(~),我们告诉 Webpack 像处理模块一样解析样式,因此,我们真正导入的是bulma包的package.jsonmain属性所引用的文件,如果我们检查一下,它看起来如下:

{

  "name": "bulma",

  "version": "0.3.1",

  ...

 "main": "bulma.sass",

  ...

}

由于我们使用 SASS 语法导入了一个 SASS 文件,所以我们需要在 Vue 组件中指定lang="sass"

使用热重载进行连续反馈的开发

热重载是一项非常有用的技术,它允许您在浏览器中查看结果而无需刷新页面即可进行开发。这是一个非常紧密的循环,可以真正加快开发过程。在官方的 Webpack 模板中,默认安装了热重载。在本教程中,您将学习如何自己安装它。

准备工作

在尝试这个教程之前,您应该对 Webpack 的工作原理有一个大致的了解;本章中的“使用 Webpack 组织依赖项”教程将为您提供帮助。

如何做到这一点...

在一个新的目录中创建一个新的 npm 项目,可以使用npm init -yyarn init -y。我个人更喜欢第二种方法,因为生成的package.json更加简洁。

要安装 Yarn,你可以使用npm install -g yarn命令。Yarn 的主要好处是你将能够将你的依赖锁定到已知版本。这可以防止在团队合作中出现错误,当应用程序从 Git 克隆时,可能会有略微不同的版本引入不兼容性。

你将创建一个数字咒骂罐。每次你说脏话,你就会向一个长期目标的咒骂罐捐赠一定金额的钱。

创建一个名为SwearJar.vue的新文件,并在其中添加以下代码:

<template>

  <div>

    Swears: {{counter}} $$

    <button @click="addSwear">+</button>

  </div>

</template>

<script>

export default {

  name: 'swear-jar',

  data () {

    return {

      counter: 0

    }

  },

  methods: {

    addSwear () {

      this.counter++

    }

  }

}

</script>

你将把这个组件插入到一个网页中。

在同一个目录下创建一个名为index.html的文件,并写入以下代码:

<!DOCTYPE html>

<html>

  <head>

    <title>Swear Jar Page</title>

  </head>

  <body>

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

    <script src="bundle.js"></script>

  </body>

</html>

bundle.js文件将由 Webpack(在内存中)为我们创建。

你需要的最后一个应用程序文件是一个包含 Vue 根实例的 JavaScript 文件。在同一个目录下创建一个名为index.js的文件,并将以下内容放入其中:

import Vue from 'vue'

import SwearJar from './SwearJar.vue'

new Vue({

  el: '#app',

  render: h => h(SwearJar)

})

现在你需要创建一个名为webpack.config.js的文件,告诉 Webpack 一些事情。第一件事是我们应用程序的入口点(index.js)以及我们想要放置编译文件的位置:

module.exports = {

  entry: './index.js',

  output: {

    path: 'dist',

    filename: 'bundle.js'

  }

}

接下来,我们将告诉 Webpack 将.vue文件转换为 JavaScript 文件,使用vue-loader

module.exports = {

  entry: './index.js',

  output: {

    path: 'dist',

    filename: 'bundle.js'

  },

  module: {

 rules: [

 {

 test: /.vue$/,

 use: 'vue-loader'

 }

 ]

 }

}

为了使一切正常工作,我们仍然需要安装我们在代码中隐含的依赖项。我们可以使用以下两个命令来安装它们:

npm install --save vue

npm install --save-dev vue-loader vue-template-compiler webpack webpack-dev-server

特别是最后一个--webpack-dev-server--是一个开发服务器,将帮助我们进行热重载开发。

运行以下命令启动服务器:

./node_modules/webpack-dev-server/bin/webpack-dev-server.js --output-path / --inline --hot --open

实际上,让我们将这个命令放在一个 npm 脚本中。

打开package.json并添加以下行:

"scripts": {

  "dev": "webpack-dev-server --output-path / --inline --hot --open"

}

现在我们可以运行npm run dev,我们将得到与下面的截图中所示相同的结果--一个浏览器将弹出:

点击加号按钮将使计数器增加,但是这个应用程序的样式呢?让我们让它更有吸引力。

打开你的代码编辑器和窗口并排放置,并对SwearJar.vue进行以下修改:

<template>

  <div>

    <p>

Swears: {{counter}} $$</p>

    <button @click="addSwear">Add Swear

</button>

  </div>

</template>

保存文件,你会看到页面自动更新。更好的是,如果计数器已经设置为大于零的值,状态将会保留,这意味着如果你有一个复杂的组件,在每次修改后你不需要手动将其带回相同的状态。尝试将脏话计数设置为某个数字并编辑模板。大多数情况下,计数器不会被重置为零。

它是如何工作的...

Webpack 开发服务器是非常有帮助的软件,它让你能够以非常紧密的反馈循环进行开发。我们使用了很多参数来使其运行:

webpack-dev-server --output-path / --inline --hot --open

所有这些参数在webpack.config.js中是相同的。相反,我们将这些参数放在命令行中是为了方便。--output-path是 Webpack 服务器将服务的bundle.js的位置;在我们的例子中,我们说我们希望它在根路径下服务,所以它将有效地将/bundle.js路径绑定到实际的bundle.js文件。

第二个参数--inline将在我们的浏览器中注入一些 JavaScript 代码,以便我们的应用程序可以与 Webpack 开发服务器通信。

--hot参数将激活热模块替换插件,它将与vue-loader(实际上是与其中的vue-hot-reload-api)通信,并且将重新启动或重新渲染(保留状态)页面中的每个 Vue 模型。

最后,--open只是为我们打开默认的浏览器。

使用 Babel 编译 ES6

ES6 有很多有用的功能,在这个示例中,你将学习如何在你的项目中使用它。值得注意的是,ES6 目前在浏览器上有很好的支持。你不会在 80%的浏览器中遇到兼容性问题,但是根据你的受众,你可能需要甚至要接触仍在使用 Internet Explorer 11 的人,或者你可能只是想最大化你的受众。此外,一些用于开发和 Node.js 的工具仍然不完全支持 ES6,因此即使是开发也需要使用 Babel。

准备就绪

在这个示例中,我们将使用 npm 和命令行。如果你在第一章的选择开发环境示例中完成了开始使用 Vue.js,那么你可能已经准备好了。

如何做...

创建一个带有空的 npm 项目的新目录。你可以使用npm init -y命令,或者如果你已经安装了 Yarn,你可以在目录中使用yarn init -y。这个命令将在目录中创建一个新的package.json文件。(请参考 Yarn 上的使用热重载进行连续反馈开发示例中的注意事项。)

对于这个 npm 项目,除了 Vue 之外,我们还需要一些其他的依赖项:Webpack 和 Babel(以 Webpack 的 loader 形式)。是的,我们还需要vue-loader来使用 Webpack。要安装它们,请执行以下两个命令:

npm install --save vue

npm install --save-dev webpack babel-core babel-loader babel-preset-es2015 vue-loader vue-template-compiler

在同一个目录下,让我们编写一个使用 ES6 语法的组件;让我们称之为myComp.vue

<template>

  <div>Hello</div>

</template>

<script>

var double = n => n * 2

export default {

  beforeCreate () {

    console.log([1,2,3].map(double))

  }

}

</script>

这个组件除了将[2,4,6]数组打印到控制台外,没有做太多事情,但是它在下一行使用了箭头语法:

var double = n => n * 2

这对于一些浏览器和工具来说是无法理解的;我们需要使用 Webpack 编译这个组件,但是我们需要使用 Babel loader 来完成。

创建一个新的webpack.config.js文件,并在其中写入以下内容:

module.exports = {

  entry: 'babel-loader!vue-loader!./myComp.vue',

  output: {

    filename: 'bundle.js',

    path: 'dist'

  }

}

这将告诉 Webpack 从我们的myComp.vue文件开始编译,但在此之前,它将通过vue-loader处理它,将其转换为 js 文件,然后通过babel-loader将箭头函数转换为更简单和更兼容的形式。

我们可以使用不同且更标准的配置来实现相同的效果:

module.exports = {

 entry: './myComp.vue', 

  output: {

    filename: 'bundle.js'

  },

  module: {

    rules: [

      {

        test: /.vue$/,

        use: 'vue-loader'

      },

      {

        test: /.js$/,

        use: 'babel-loader'

      }

    ]

  }

}

这是一个更通用的配置,它表示每当我们遇到以.vue结尾的文件时,应该使用vue-loader进行解析和处理,以及以.js结尾的文件应该使用babel-loader进行处理。

要配置 Babel 加载器,有几个选项;我们将遵循推荐的方式。在项目文件夹中创建一个名为.babelrc的文件(注意初始点),并指定我们要应用es2015预设,写入以下代码:

{

  "presets": ["es2015"]

}

最后,我总是喜欢在package.json文件中添加一个新的脚本,以便更容易地启动命令。在文件末尾(但在最后一个大括号之前)添加以下行:

"scripts": {

  "build": "webpack"

}

然后运行npm run build。这将在dist目录中创建一个名为bundle.js的文件;打开它并搜索包含例如double的行。你应该会找到类似于这样的内容:

...

var double = function double(n) {

  return n * 2;

};

...

这是我们的var double = n => n * 2,从 ES6 转换为常规JavaScript。

它是如何工作的...

es2015 Babel 预设是一组 Babel 插件,旨在将 ECMAScript2015(ES6)语法转换为更简单的 JavaScript。例如,它包含了babel-plugin-transform-es2015-arrow-functions插件,它可以将箭头函数转换为:

var addOne = n => n + 1

将箭头函数转换为更简单的 JavaScript,如下所示:

var addOne = function addOne(n) {

  return n + 1

}

为了选择文件及其相应的加载器,我们在webpack.config.js中填写了测试字段,并为匹配的.vue文件编写了以下内容:

test: /\.vue$/

这个语法是一个正则表达式,它总是以一个正斜杠开始,以另一个正斜杠结束。它匹配的第一个字符是点,表示为\.,因为.字符已经被用于其他目的。点后面必须跟着vue字符串,字符串的结束字符表示为美元符号。如果把它们都放在一起,它将匹配所有以.vue结尾的字符串。对于.js文件也是类似的。

在开发过程中运行代码检查器

对代码进行 linting 可以大大减少在开发过程中累积的小错误和低效,它保证了团队或组织中的编码风格的一致性,并使您的代码更易读。与偶尔运行 linter 不同,将其始终运行是有用的。本教程将教您如何在 Webpack 中实现此功能。

准备工作

在本教程中,我们将再次使用 Webpack。您将使用webpack-dev-server构建一个紧密循环,该循环在使用热重载进行连续反馈开发教程中有所涵盖。

如何实现...

在一个新的文件夹中,创建一个新的 npm 项目(可以使用npm init -yyarn init -y)。

在文件夹中,创建一个名为src的新目录,并在其中放置一个名为MyComp.vue的文件。让文件包含以下代码:

<template>

  <div>

    Hello {{name}}!

  </div>

</template>

<script>

export default {

  data () {

    return {

      name: 'John',

      name: 'Jane'

    }

  }

}

</script>

我们已经发现了一个问题-John的 name 属性将被后面的具有相同键的属性Jane覆盖。让我们假装没有注意到这一点,并将组件放在一个网页中。为此,我们需要另一个文件,名为index.js,在src目录中。在其中写入以下代码:

import Vue from 'vue'

import MyComp from './MyComp.vue'

new Vue({

  el: '#app',

  render: h => h(MyComp)

})

在根目录中,放置一个名为index.html的文件,其中包含以下代码:

<!DOCTYPE html>

<html>

  <head>

    <title>Hello</title>

  </head>

  <body>

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

      <script src="bundle.js"></script>

    </body>

</html>

现在我们需要一个webpack.config.js文件来告诉 Webpack 如何编译我们的文件;在其中写入以下内容:

module.exports = {

  entry: './src/index.js',

  module: {

    rules: [

      { 

        test: /.vue$/,

        use: 'vue-loader'

      }

    ]

  }

}

这只是告诉 Webpack 从index.js文件开始编译,并且每当它找到一个.vue文件时,将其转换为 JavaScript 与vue-loader。除此之外,我们还希望使用一个 linter 扫描所有的文件,以确保我们的代码中没有愚蠢的错误。

将以下 loader 添加到rules数组中:

{

  test: /.(vue|js)$/,

  use: 'eslint-loader',

  enforce: 'pre'

}

enforce: 'pre'属性将在其他 loader 之前运行此 loader,因此它将应用于您编写的代码而不是其转换。

我们最后需要做的是配置 ESLint。在根目录中创建一个名为.eslintrc.js的新文件,并在其中添加以下内容:

module.exports = {

  "extends": "eslint:recommended",

  "parser": "babel-eslint",

  plugins: [

    'html'

  ]

}

我们在这里说了几件事。首先是我们想要应用于我们的代码的一组规则;换句话说,我们的规则集(现在为空)正在扩展推荐的规则集。其次,我们使用babel-eslint解析器而不是默认的解析器。最后,我们使用 HTML ESLint 插件,它将帮助我们处理.vue文件并提取其中的 JavaScript 代码。

现在我们已经准备好启动我们的开发机器了,但首先我们需要使用以下命令安装依赖项:

npm install --save vue

npm install --save-dev babel-eslint eslint eslint-loader eslint-plugin-html vue-loader vue-template-compiler webpack webpack-dev-server

我们可以直接启动 Webpack 开发服务器,但我强烈建议将以下代码添加到package.json文件中:

"scripts": {

  "dev": "webpack-dev-server --entry ./src/index.js --inline --hot --open"

}

现在,如果我们启动npm run dev,浏览器应该打开并显示以下错误的组件:

<q>Hello Jane!</q>

您还应该能够在控制台中看到问题:

11:7 error Duplicate key 'name' no-dupe-keys

这意味着我们有两个具有相同“name”的键。通过删除该属性来纠正错误:

data () {

  return {

    name: 'John'

  }

}

在控制台中,当您保存 Vue 组件后,您应该注意到 Webpack 已经再次执行了编译,这次没有错误。

工作原理是...

基本上,在此处发生的是,linter 加载器在其他编译步骤之前处理文件并将错误写入控制台。这样,您就可以在不断开发的过程中看到代码中的不完美之处。

ESLint 和 Webpack 在 Vue 官方模板中可用。现在您知道,如果出于某种原因,您想要修改 ESLint 规则,可以从.eslintrc.js文件中进行修改,并且如果您想要使用其他 linter,可以在 Webpack 配置文件中使用另一个加载器。

只使用一个命令来构建一个压缩和一个开发的.js 文件

在开发组件时,您可能需要一个可靠的流程来发布构建的文件。一个常见的操作是发布库/组件的两个版本:一个用于开发目的,一个用于在生产代码中使用,通常是经过压缩的。在这个配方中,您将调整官方模板,同时发布一个经过压缩和一个开发的 JavaScript 文件。

准备就绪

如果您已经在构建和分发自己的组件,那么这个配方就有意义。如果您想了解更多信息,我建议您参考“使用 Webpack 打包您的组件”配方。

如何操作...

我们将从官方的 Webpack 模板开始一个项目。您可以使用自己的模板,或者使用vue init webpack创建一个新项目,并使用“npm install”安装依赖项。

进入build目录。当您运行npm run build命令时,实际上是在该目录中运行build.js文件。

如果您检查文件,您会在末尾找到类似以下内容的内容:

webpack(webpackConfig, function (err, stats) {

...

})

这相当于使用相同配置在命令行中启动 Webpack,指定在第一个参数webpackConfig中指定的配置。为了获得一个经过压缩和一个非经过压缩的文件,我们必须将webpackConfig带到一个公共的基准,然后只指定开发和生产版本之间的差异。

为此,请进入同一目录中的webpack.prod.conf.js。在这里,您可以看到我们正在传递的配置;特别是,您会发现UglifyJsPlugin,它负责压缩文件(如果您查看插件数组)。删除该插件,因为它代表了两个版本之间的主要区别。

现在,在 Webpack 命令之前,在build.js中写入以下内容:

const configs = [

  {

    plugins: [

      new webpack.optimize.UglifyJsPlugin({

        compress: {

          warnings: false

        },

        sourceMap: true

      })

    ]

  },

  {

    plugins: []

  }

]

现在你有了一个包含两个不同配置的数组,一个带有压缩文件所需的插件,一个没有。如果你将它们与webpack.prod.conf.js中的配置合并,你将得到不同的结果。

为了合并这两个配置,我们将使用webpack-merge包。在文件顶部添加以下行:

var merge = require('webpack-merge')

然后,将 Webpack 命令的第一行修改为以下内容:

configs.

map(c =>

 webpack(merge(webpackConfig, c)

, function (err, stats) {

...

这将启动与我们在配置数组中指定的配置数量一样多的不同合并配置。

现在你可以运行npm run build命令了,但问题是文件将具有相同的名称。从webpack.prod.conf.js中剪切输出属性,并将其粘贴到config数组中,现在它应该是这样的:

const configs = [

  {

    output: {

 path: <whatever is your path>,

 filename: 'myFilename.min.js'),

 <other options you may have>

 },

    plugins: [

      new webpack.optimize.UglifyJsPlugin({

        compress: {

          warnings: false

        }

      })

    ]

  },

  {

    output: {

 path: <whatever is your path>,

 filename: 'myFilename.js'),

 <other options you may have>

 },

    plugins: []

  }

]

如果你现在构建你的项目,你将得到一个压缩和一个开发文件。当然,你可以根据需要个性化你的配置,使它们变得非常不同。例如,你可以在一个配置中添加源映射,而将另一个配置保持不变。

它是如何工作的...

我们首先创建了一个表示 Webpack 配置差异的对象数组。然后,我们使用webpack-merge的帮助将每个配置片段映射到一个更大的公共配置中。当我们现在调用npm run build命令时,两个配置将依次运行。

将文件名的后缀命名为min是一种常见的约定,表示该文件已经被压缩并准备好在生产环境中使用。

发布你的组件到公共领域

在某个时刻,当你想要回馈社区时,会有一个时刻。也许你建了一个“放屁按钮”,或者你建了一个自动化股票期权交易者;无论你建了什么,JavaScript 和 Vue 社区都会很高兴欢迎你。在市场营销和许可方面还有很多事情要做,但在这个教程中,你将集中关注更多的技术方面。

准备工作

这个教程是针对那些想要与 Vue 社区分享他们的工作的人。在使用 Webpack 打包你的组件教程中,你会找到如何调整官方 Webpack 模板以正确打包你的组件;这个教程可以看作是第二部分。不过我们不会使用官方模板。

如何操作...

我在这个教程中采用的方法是使用Guillaume Chau的优秀vue-share-components模板。我们将从这个起点构建一个笑话按钮。

在命令行中,创建一个新的目录并在其中输入以下命令:

vue init Akryum/vue-share-components

它会询问你一些问题;你可以从下面的图片中复制回答。需要注意的是,你(可悲地)不能使用joke-button作为你的项目名称,因为在编写这个教程时我已经注册了它。不过,你可以想出一个类似的名称(在继续之前,你可能需要检查该名称在npm注册表中是否可用):

项目创建完成后,你可以像控制台输出一样使用npm install安装依赖项。

在项目内部,让我们创建一个笑话按钮组件。在component文件夹内,你会找到一个Test.vue组件;将其重命名为JokeButton.vue并使其看起来像以下代码:

<template>

  <div class="test">

    <button @click="newJoke">New Joke</button>

    <p>{{joke}}</p>

  </div>

</template>

<script>

const jokes = [

 'Chuck Norris/'s keyboard has the Any key.',

 'Chuck Norris can win at solitaire with only 18 cards.',

 'Chuck Norris/' first job was as a paperboy. There were no survivors.',

 'When Chuck Norris break the build, you can/'t fix it.',

]

export default {

  name: 'joke-button',

  data () {

    return {

      joke: '...',

    }

  },

  methods: {

    newJoke () {

      this.joke = jokes[Math.floor(Math.random() * jokes.length)]

    },

  },

}

</script>

显然,你可以创建你喜欢的组件;这只是一个例子。

index.js文件中,你会看到导入和安装了Test组件;你需要安装JokeButton代替。需要更改的行已经被标出:

import JokeButton

 from './components/JokeButton

.vue'

// Install the components

export function install (Vue) {

  Vue.component('jokeButton

', JokeButton

)

  /* -- Add more components here -- */

}

// Expose the components

export {

  JokeButton

,

  /* -- Add more components here -- */

}

...

我们的组件已经准备好了!

现在你需要去 npm 网站注册一个账号(如果你还没有的话)。

前往npmjs.com

点击注册并输入你的详细信息,就像我在这里做的一样:

当然,如果你喜欢的话,你可以订阅 npm 每周的新闻通讯。

注册完成后,你就完成了,可以回到命令行了。你必须使用以下命令从终端登录到 npm 注册表:

npm adduser

你将看到类似于这样的内容:

你将需要输入刚刚为 npm 网站输入的密码。

下一个命令将会在公共仓库中发布你的库:

npm publish

现在你甚至可以查找你的包,确保你会在下面的截图中找到它:

要尝试它,你可以在你自己的 README 中找到说明,多酷啊!

它是如何工作的...

vue-share-components 比官方模板更简单,所以通过检查它是一个很好的学习方式。

我们可以首先看一下 package.json 文件。以下几行是相关的:

...

"main": "dist/joke-button.common.js",

"unpkg": "dist/joke-button.browser.js",

"module": "index.js",

"scripts": {

  "dev": "cross-env NODE_ENV=development webpack --config config/webpack.config.dev.js --progress --watch",

  "build": "npm run build:browser && npm run build:common",

  "build:browser": "cross-env NODE_ENV=production webpack --config config/webpack.config.browser.js --progress --hide-modules",

  "build:common": "cross-env NODE_ENV=production webpack --config config/webpack.config.common.js --progress --hide-modules",

  "prepublish": "npm run build"

},

...

main 属性是我们在程序中写入以下命令时实际得到的内容:

import JokeButton from 'JokeButton'

或者,我们在添加以下代码时获得它:

var JokeButton = require("JokeButton")

所以,JokeButton 变量实际上将包含在我们的 joke-button.common.js 中导出的内容。

你可以编辑 package.jsonmain 属性,直接指向一个 .vue 组件。这样,你就把编译组件的责任交给了用户。虽然这对用户来说更多了一些工作,但当你想要自由地编译最新版本的 Vue 时,这也是有帮助的。

在后一种情况下,如果你的组件的一些逻辑是在 external.js 文件中导出的(就像本章的第一个示例中一样),请始终记得在 Webpack 规则中添加目录,如下所示:

{

  test: /.js$/,

  loader: 'babel-loader',

  include: [resolve('src'), resolve('test'), resolve('node_modules/myComponent')]

},

unpkg 是 unpkg.com 的特定部分,它是一个 CDN。这非常好,因为一旦我们发布了项目,我们的脚本将会在 unpkg.com/joke-button 上发布,并且它将指向适用于浏览器的 joke-button.browser.js 文件。

prepublish脚本是一个特殊的脚本,在使用npm publish命令将项目发布到 npm 之前调用。这消除了在发布组件之前忘记构建文件的可能性(这在我身上发生过很多次,所以我被迫人为地增加软件版本,手动构建文件,然后再次发布)。

另一个有趣的事实是webpack.config.common.jswebpack.config.browser.js之间的区别,前者输出joke-button.common.js文件,后者输出joke-button.browser.js文件。

第一个文件的输出设置如下:

output: {

  path: './dist',

  filename: outputFile + '.common.js',

  libraryTarget: 'commonjs2',

},

target

:

 '

node' 

,

因此,它将输出一个公开 commonJS 接口的库;这是为非浏览器环境定制的,您需要使用 require 或 import 来使用该库。另一方面,用于浏览器的第二个文件具有以下输出:

output: {

  path: './dist',

  filename: outputFile + '.browser.js',

  library: globalName,

  libraryTarget: 'umd',

},

UMD 将在全局范围内公开自身,无需导入任何内容,因此它非常适合浏览器,因为我们可以将文件包含在 Vue 网页中并自由使用组件。这也是可能的,多亏了index.js的自动安装功能:

/* -- Plugin definition & Auto-install -- */

/* You shouldn't have to modify the code below */

// Plugin

const plugin = {

 /* eslint-disable no-undef */

 version: VERSION,

 install,

}

export default plugin

// Auto-install

let GlobalVue = null

if (typeof window !== 'undefined') {

 GlobalVue = window.Vue

} else if (typeof global !== 'undefined') {

 GlobalVue = global.Vue

}

if (GlobalVue) {

 GlobalVue.use(plugin)

}

这段代码的作用是将安装函数(用于在 Vue 中注册组件)封装在plugin常量中,并同时导出它。然后,它检查是否定义了windowglobal,如果是这样,它获取代表 Vue 库的Vue变量,并使用插件 API 来安装组件。

第八章:高级 Vue.js - 指令、插件和渲染函数

在本章中,我们将讨论以下主题:

  • 创建一个新的指令

  • 在 Vue 中使用 WebSockets

  • 为 Vue 编写插件

  • 手动渲染一个简单的组件

  • 渲染带有子元素的组件

  • 使用 JSX 渲染组件

  • 创建一个功能性组件

  • 使用高阶组件构建响应式表格

介绍

指令和插件是以可重用的方式打包功能并使其在应用程序和团队之间易于共享的方法;在本章中,您将构建其中的一些。渲染函数是 Vue 在幕后实际工作的方式,将模板转换为 Vue 语言,然后再次转换为 HTML 和 JavaScript;如果您需要优化应用程序的性能并处理一些特殊情况,它们将非常有用。

通常情况下,应尽量避免在可能的情况下使用这些高级功能,因为它们在过去有点被滥用。通常,许多问题可以通过编写一个良好的组件并分发组件本身来解决;只有在这种情况不成立时,您才应该考虑使用高级功能。

本章面向稍有经验的开发者,您可能不会在其他示例中找到逐步详细说明的水平,但我努力使它们完整。

创建一个新的指令

指令类似于小型函数,您可以使用它们快速地插入到您的代码中,主要是为了改善用户体验,并向您的图形界面添加新的低级功能。

准备工作

尽管这个示例在高级章节中,但它非常容易完成。指令之所以被称为“高级”是因为通常应该优先选择组合来为应用程序添加功能和样式。当组件无法满足需求时,可以使用指令。

如何操作...

我们将构建一个v-pony指令,将任何元素转换为小马元素。小马元素具有粉色背景,并在单击时更改颜色。

小马元素的 HTML 代码如下所示:

<div id="app">

  <p v-pony>I'm a pony paragraph!</p>

  <code v-pony>Pony code</code>

  <blockquote>Normal quote</blockquote>

  <blockquote v-pony>I'm a pony quote</blockquote>

</div>

为了显示差异,我包含了一个普通的blockquote元素。在我们的 JavaScript 部分,写入以下内容:

Vue.directive('pony', {

  bind (el) {

    el.style.backgroundColor = 'hotpink'

  }

})

这是如何声明新指令的。当指令绑定到元素时,将调用bind钩子。现在我们只是设置背景颜色。我们还希望在每次点击后更改颜色。要做到这一点,您必须添加以下代码:

Vue.directive('pony', {

  bind (el) {

    el.style.backgroundColor = 'hotpink'

    el.onclick = () => {

 const colGen = () => 

 Math.round(Math.random()*255 + 25)

 const cols =

 [colGen() + 100, colGen(), colGen()]

 const randRGB =

 `rgb(${cols[0]}, ${cols[1]}, ${cols[2]})`

 el.style.backgroundColor = randRGB

 }

  }

})

在这里,我们正在创建一个onclick监听器,它将生成一个偏向红色的随机颜色,并将其分配为新的背景颜色。

在我们的 JavaScript 末尾,不要忘记创建一个Vue实例:

new Vue({

  el: '#app'

})

您可以启动应用程序以查看指令的效果:

不要忘记点击文本以更改背景颜色!

工作原理...

声明新指令的语法如下所示:

Vue.directive(<name: String>, {

  // hooks

})

这将注册一个新的全局指令。在 hooks 对象内部,您可以定义两个重要的函数:bind,您在本示例中使用的函数,以及update,它在其中包含的组件每次更新时触发。

每个钩子函数至少被调用三个参数:

  • el:HTML 元素

  • binding:指令可以接收一个参数;binding 是一个包含参数值的对象

  • vnode:此元素的 Vue 内部表示

我们使用el参数直接编辑元素的外观。

在 Vue 中使用 WebSockets

WebSockets 是一种新技术,它使用户和托管应用程序的服务器之间可以进行双向通信。在这项技术出现之前,只有浏览器可以发起请求和连接。如果页面上有更新,浏览器必须不断地轮询服务器。使用 WebSockets,这不再是必需的;在建立连接后,服务器只有在需要时才能发送更新。

准备工作

您不需要为此示例做任何准备,只需要了解 Vue 的基础知识。如果您不知道什么是 WebSockets,您实际上不需要知道,只需将其视为服务器和浏览器之间连续双向通信的通道。

如何操作...

对于这个示例,我们需要一个充当客户端的服务器和浏览器。我们不会构建一个服务器;相反,我们将使用一个已经存在的服务器,通过 WebSockets 将您发送的任何内容回显。因此,如果我们发送Hello消息,服务器将回复Hello

您将构建一个聊天应用程序,该应用程序将与此服务器进行通信。编写以下 HTML 代码:

<div id="app">

  <h1>Welcome</h1>

  <pre>{{chat}}</pre>

  <input v-model="message" @keyup.enter="send">

</div>

<pre>标签将帮助我们渲染聊天记录。由于我们不需要<br/>元素来换行,我们可以使用特殊字符n表示换行。

为了使我们的聊天工作,我们首先必须在 JavaScript 中声明我们的 WebSocket:

 const ws = new WebSocket('ws://echo.websocket.org')

之后,我们声明我们的Vue实例,其中包含一个chat字符串(用于保存到目前为止的聊天记录)和一个message字符串(用于保存我们当前正在编写的消息):

new Vue({

  el: '#app',

  data: {

    chat: '',

    message: ''

  }

})

我们仍然需要定义send方法,该方法在文本框中按下Enter时调用:

new Vue({

  el: '#app',

  data: {

    chat: '',

    message: ''

  },

  methods: {

 send () {

 this.appendToChat(this.message)

 ws.send(this.message)

 this.message = ''

 },

 appendToChat (text) {

 this.chat += text + 'n'

 }

 }

}

我们将appendToChat方法分解出来,因为我们将使用它来附加我们收到的所有消息。为此,我们必须等待组件被实例化。created钩子是一个安全的地方:

...

created () {

  ws.onmessage = event => {

    this.appendToChat(event.data)

  }

}

...

现在启动应用程序与您的个人回声室聊天:

工作原理...

要查看您构建的内容的内部,请打开 Chrome 开发者工具(! | 更多工具 | 开发者工具或Opt + Cmd + I):

转到网络选项卡并重新加载页面;您应该看到echo.websocket.orl WebSocket,如屏幕截图所示。输入一些内容,消息将出现在帧选项卡中,如下所示:

绿色消息是您发送的消息,而白色消息是您接收的消息。您还可以检查消息的长度(以字节为单位)以及发送或接收的确切时间。

为 Vue 编写一个插件

插件是我们想要在应用程序中拥有的一组实用工具或全局新行为。Vuex 和 vue-router 是 Vue 插件的两个著名例子。插件可以是任何东西,因为编写插件意味着在非常低的层次上进行操作。你可以编写不同类型的插件。对于这个示例,我们将专注于构建具有全局属性的指令。

准备工作

这个示例将基于创建一个新的指令,只是我们将添加一些用于全局协调的功能。

如何做...

对于这个示例,我们将为一个袋鼠欣赏俱乐部建立一个网站。主页 HTML 的布局如下:

<div id="app">

  <h1>Welcome to the Kangaroo club</h1>

  <img src="https://goo.gl/FVDU1I" width="300px" height="200px">

  <img src="https://goo.gl/U1Hvez" width="300px" height="200px">

  <img src="https://goo.gl/YxggEB" width="300px" height="200px">

  <p>We love kangaroos</p>

</div>

你可以将袋鼠图片的链接更改为你喜欢的链接。

在 JavaScript 部分,我们暂时实例化一个空的Vue实例:

new Vue({

  el: '#app'

})

如果我们现在打开页面,会得到这个结果:

现在,我们想在网站上添加一个有趣的注释。我们希望页面上的元素(除了标题)以随机的时间间隔跳动。

为了实现这个目标,你将实现的策略是将所有需要跳动的元素注册到一个数组中,然后定期选择一个随机元素并使其跳动。

我们首先需要定义 CSS 中的跳动动画:

@keyframes generateJump {

  20%{transform: translateY(0);}

  40%{transform: translateY(-30px);}

  50%{transform: translateY(0);}

  60%{transform: translateY(-15px);}

  80%{transform: translateY(0);}

}

.kangaroo {

  animation: generateJump 1.5s ease 0s 2 normal;

}

这样做的效果是创建一个名为kangaroo的类,当应用于一个元素时,它会沿着 y 轴跳动两次。

接下来,编写一个函数,在 JavaScript 中将这个类添加到指定的元素上:

const jump = el => {

  el.classList.add('kangaroo')

  el.addEventListener('animationend', () => {

    el.classList.remove('kangaroo')

  })

}

jump函数会添加kangaroo类,并在动画完成后将其移除。

我们希望在注册的元素中随机选择一个执行此操作:

const doOnRandomElement = (action, collection) => {

  if (collection.length === 0) {

    return

  }

  const el = 

    collection[Math.floor(Math.random()*collection.length)]

  action(el)

}

doOnRandomElement函数接受一个动作和一个集合,并将该动作应用于一个随机选择的元素。然后,我们需要在随机的时间间隔内调度它:

const atRandomIntervals = action => {

  setTimeout(() => {

    action()

    atRandomIntervals(action)

  }, Math.round(Math.random() * 6000))

}

atRandomIntervals函数接受指定的函数,并在小于 6 秒的随机时间间隔内调用它。

现在我们已经拥有了构建使元素跳跃的插件所需的所有函数:

const Kangaroo = {

  install (vueInstance) {

    vueInstance.kangaroos = []

    vueInstance.directive('kangaroo', {

      bind (el) {

       vueInstance.kangaroos.push(el)

      }

    })

    atRandomIntervals(() => 

      doOnRandomElement(jump, vueInstance.kangaroos))

  }

}

Kangaroo 插件在安装时创建一个空数组;它声明了一个新的指令kangaroo,该指令将所有包含在其中的元素注册到数组中。

然后在随机的时间间隔内,从数组中随机选择一个元素,并调用跳跃函数。

要激活插件,在声明Vue实例之前(但在声明Kangaroo之后)需要添加一行代码:

Vue.use(Kangaroo)

new Vue({

  el: '#app'

})

我们必须选择跳动的元素,也就是除了标题以外的所有元素:

 <div id="app">

   <h1>Welcome to the Kangaroo club</h1>

   <img v-kangaroo

 src="https://goo.gl/FVDU1I" width="300px" height="200px">

   <img v-kangaroo

 src="https://goo.gl/U1Hvez" width="300px" height="200px">

   <img v-kangaroo

 src="https://goo.gl/YxggEB" width="300px" height="200px">

   <p v-kangaroo

>We love kangaroos</p>

 </div>

如果现在运行您的应用程序,您将看到图像或文本每隔几秒钟像袋鼠一样跳动。

工作原理...

本质上,Vue 插件只是一种将一些功能组合在一起的方式。没有太多限制,创建插件的唯一要做的就是声明一个安装函数。一般的语法如下所示:

MyPlugin.install = (vueInstance, option) => {

  // ...

}

要使用您刚刚创建的插件,编写以下代码:

Vue.use(MyPlugin, { 

/* any option you need */

 }) 

这里,第二个参数是传递给install函数的可选对象。

由于插件是全局实体,您应该尽量少使用它们,只用于您预见会影响整个应用程序的功能。

手动渲染一个简单的组件

Vue 将您的 HTML 模板转换为渲染函数。通常,您应该坚持使用模板,因为它们更简单。有几种情况下,渲染函数变得非常有用。在这里,我们展示了一个简单的例子,其中渲染函数很有用。

准备工作

这是有关渲染函数的第一个示例。如果您已经了解 Vue 的基础知识,您将理解其中的一切。

如何实现...

渲染函数的第一个用例是当您只想要一个显示另一个组件的Vue实例时。

编写一个空的 HTML 布局,如下所示:

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

我们有一个名为 Greeter 的组件,我们希望将其显示为主要的Vue实例。在 JavaScript 部分,添加以下代码:

const Greeter = {

  template: '<p>Hello World</p>'

}

在这里,我们必须想象我们从其他地方获取了Greeter组件,并且由于组件已经很好地打包,我们不想修改它。相反,我们将它传递给Vue主实例:

const Greeter = {

  template: '<p>Hello World</p>'

}

new Vue({

 el: '#app',

 render: h => h(Greeter)

})

如果我们现在启动应用程序,我们只会看到Greeter组件。主Vue实例只充当包装器。

工作原理...

渲染函数替换了Vue实例中的模板。当调用渲染函数时,传递的参数是所谓的createElement函数。为了简洁起见,我们将其命名为h。这个函数接受三个参数,但现在只需要注意我们传递的第一个参数(也是唯一一个参数)是Greeter组件。

理论上,您可以在h函数内联编写组件。在实际项目中,这并不总是可能的,这取决于运行时是否存在 Vue 模板编译器。当您使用官方的 Webpack 模板时,您会被问到是否要在分发软件时包含 Vue 模板编译器。

createElement函数的参数在这里列出:

  1. 作为第一个参数,唯一必需的参数,您可以选择传递三种不同的内容:
  • Vue 组件的选项,就像我们的示例中一样

  • 表示 HTML 标签的字符串(例如divh1p

  • 一个返回 Vue 组件的选项对象或表示 HTML 标签的字符串的函数

  1. 第二个参数必须是一个名为Data Object的对象。这个对象将在下一个示例中解释。

  2. 第三个参数是一个数组或字符串:

  • 数组表示要放在组件内部的元素、文本或组件的列表

  • 您可以编写一个将被渲染为文本的字符串

渲染带有子元素的组件

在这个示例中,您将使用渲染函数完全构建一个简单的网页,其中包含一些元素和组件。这将让您近距离观察 Vue 如何编译您的模板和组件。如果您想构建一个高级组件,并且想要一个完整的示例来启动,这可能会很有用。

准备工作

这是一个完整的构建组件的渲染函数的示例。通常情况下,您不需要在实践中这样做;这只推荐给高级读者。

如何做到这一点...

您将为一个水管工俱乐部构建一个页面。页面将如下所示:

每当我们在名称文本框中写入一个名称时,它将被写入问候中,就像v-model指令一样。

对于这个示例,我们从末尾开始而不是从开头开始,因为通常当您不得不使用render函数时,您对您想要的结果有一个非常清晰的想法。

在我们应用程序的 HTML 部分,让我们从一个空标签开始:

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

在 JavaScript 中,在render函数中写入一个空的<div>元素:

new Vue({

  el: '#app',

  render: h => h('div')

})

我们将首先放入的是标题,如下所示:

new Vue({

  el: '#app',

  render: h => h(

    'div',

    [

 h('h1', 'The plumber club page')

 ]

  )

})

所有其他的元素和组件都将适应我们刚刚为标题创建的数组。

我们需要一个<input>元素,它将接收值并显示问候语。为此,我们可以构建一个Vue组件。

在下面的代码中,我们使用的是一个常规的 JavaScript 函数,而不是箭头函数;这是因为我们想要一个对组件本身的引用。箭头函数不允许您修改this的作用域,而this取决于函数的调用方式,并且可以选择地绑定到常规函数中的任何变量。在我们的情况下,它将绑定到实例组件。

在页面标题之后,我们在同一个数组中添加以下组件:

h(

  {

    render: function (h) {

      const self = this

      return h('div', [

        'Your name is ',

        h('input', {

          domProps: {

            value: self.name

          },

          on: {

            input (event) {

              self.name = event.target.value

            }

          }

        }),

        h(

          'p',

          'Hello ' + self.name + 

            (self.exclamation ? '!' : ''))

      ])

    },

    data () { return { name: '' } },

    props: ['exclamation']

  },

  {

    props: {

      exclamation: true

    }

  }

)

该组件有三个选项:renderdataprops函数。

createElement函数的第二个参数是为我们的 props 实际分配值:

{

  props: {

    exclamation: true

  }

}

这将等同于在声明组件时写:exclamation="true"

您可以轻松理解组件的dataprops选项。让我们来看看我们在render函数中写了什么。

在函数的第一行,我们将self = this设置为一种方便的方式,以便在添加任何嵌套函数时引用组件。然后,我们返回一个createElement函数(h)的结果,该函数在一个 div 标签内将三个元素放置在 DOM 中。第一个是原始文本Your name is,然后是两个元素:一个输入框和一个段落。

在使用渲染函数时,我们没有v-model指令的直接等价物。相反,我们手动实现它。我们将值绑定到名称,然后添加一个监听器到输入事件,该事件将把状态变量name的值设置为文本框中的内容。

然后,我们插入一个段落元素,根据exclamation属性的值添加一个感叹号,组成问候语。

在组件之后,我们可以在同一个数组中添加以下内容,如图所示:

 'Here you will find ', h('i', 'a flood '), 'of plumbers.'

如果您做得正确,您应该能够运行应用程序并看到整个页面。

它是如何工作的...

在这个例子中,我们看到了 Vue 在编译我们的模板时发生的一瞥;再次强调,您不建议在常规组件中这样做。大多数情况下,结果将更冗长,几乎没有收益。另一方面,有几种情况下编写渲染函数实际上可能会产生更好或更健壮的代码,并涵盖一些难以用模板表达的功能。

使用 JSX 渲染组件

JSX 在 React 社区非常流行。在 Vue 中,您不必使用 JSX 来构建组件的模板;您可以使用更熟悉的 HTML。然而,如果您被迫编写大量的渲染函数,JSX 是您可以做的下一件最好的事情。

准备工作

在尝试这个示例之前,最好先玩一下渲染函数。之前的示例提供了一些练习。

如何做...

JSX 需要一个 Babel 插件才能工作。在这个示例中,我假设你是在 webpack 模板中工作。

要安装 Babel 插件,可以运行以下命令:

npm install

 babel-plugin-syntax-jsx

 babel-plugin-transform-vue-jsx

 babel-helper-vue-jsx-merge-props

 --save-dev

.babelrc文件中,在plugins数组中添加以下内容:

 "

plugins" 

: [

 ...

  "

transform-vue-jsx" 

]

像往常一样运行npm install来安装所有依赖项。

现在,打开main.js并删除其中的所有内容。用以下代码替换它:

import Vue from 'vue'

/* eslint-disable no-new */

new Vue({

  el: '#app',

  render (h) {

    return <div>{this.msg}</div>

  },

  data: {

    msg: 'Hello World'

  }

})

如果你从未见过 JSX,那么这一行是有点奇怪的。只要注意我们在前面的代码中没有在render选项中使用箭头函数。这是因为我们在内部使用了this,我们希望它绑定到组件上。

你可以使用npm run dev命令看到你的页面已经工作了。

它是如何工作的...

Babel 插件将把 JSX 代码转换为 JavaScript 的render函数。

我不建议在 Vue 中使用 JSX。我唯一能想到它有用的时候是当你需要将render函数与 JavaScript 混合使用,并且需要一种快速和可读的方式来定义模板。除此之外,使用 JSX 没有太多优势。

更多内容...

让我们稍微复杂一点的代码,至少让我们了解 JSX 如何与 props 配合使用。

在主Vue实例之前定义一个新的组件:

const myComp = {

  render (h) {

    return <p>{this.myProp}</p>

  },

  props: ['myProp']

}

让我们在我们的Vue实例中使用这个组件,并通过 props 传递msg变量:

new Vue({

  el: '#app',

  render (h) {

    return <div>

      <myComp myProp={this.msg}/>

    </div>

  },

  data: {

    msg: 'Hello World'

  },

  components: {

    myComp

  }

})

语法与 HTML 模板略有不同。特别要注意如何传递 props 以及如何使用驼峰命名和自闭合标签。

创建一个功能组件

组件的一个轻量级版本是功能组件。功能组件没有实例变量(因此没有this)并且没有状态。在这个示例中,我们将编写一个简单的功能组件,它通过 HTML 接收一些指令并将它们转换为绘图。

准备就绪

在尝试这个示例之前,你至少应该熟悉 Vue 中的渲染函数。你可以使用之前的示例来做到这一点。

如何实现...

当你编写一个<svg>元素时,通常需要将数据放在其中的元素的属性中才能真正绘制形状。例如,如果你想绘制一个三角形,你需要写以下内容:

<svg>

  <path d="M 100 30 L 200 30 L 150 120 z"/>

</svg>

d属性内的文本是一系列指令,用于移动虚拟光标进行绘制:M将光标移动到<svg>内的坐标(100, 30),然后L绘制一条线直到(200, 30),然后再次绘制到坐标(150, 120)。最后,z关闭我们正在绘制的路径,结果始终是一个三角形。

我们想要用一个组件来绘制一个三角形,但我们不喜欢属性,而且我们想用我们自己的语言来编写,所以我们会写以下内容以获得相同的结果:

<orange-line>

  moveTo 100 30 traceLine 200 30 traceLine 150 120 closePath

</orange-line>

这是一个完美的功能组件的工作,因为没有需要管理的状态,只是从一个组件到一个元素的转换。

你的 HTML 布局将简单地如下所示:

<div id="app">

  <orange-line>

    moveTo 100 30 traceLine 200 30 traceLine 150 120 closePath

  </orange-line>

</div>

然后,在 JavaScript 中布局你的功能性组件:

const OrangeLine = {

  functional: true,

  render (h, context) {

    return h('svg',

      []

    )

  }

}

你必须指定组件将是功能性的,使用functional: true;然后渲染函数与通常略有不同。第一个参数仍然是createElement函数,但传递的第二个参数是我们组件的上下文。

我们可以通过context.children访问组件 HTML 中写的文本(绘制命令)。

你可以看到我已经添加了一个空的<svg>元素。在其中,有一个空的子元素数组;我们将只把<path>元素放在那里,如下所示:

render (h, context) {

  return h('svg',

    [

      h('path', {

 attrs: {

 d: context.children.map(c => {

 return c.text

 .replace(/moveTo/g, 'M')

 .replace(/traceLine/g, 'L')

 .replace(/closePath/g, 'z')

 }).join(' ').trim(),

 fill: 'black',

 stroke: 'orange',

 'stroke-width': '4'

 }

 })

    ]

  )

}

这段代码创建了一个路径元素,然后设置了一些属性,比如fillstroked属性从组件内部获取文本,进行一些替换,然后返回它。

我们只需要在 JavaScript 中创建Vue实例:

new Vue({

  el: '#app',

  components: {

    OrangeLine

  }

})

现在,加载应用程序,我们应该看到一个三角形,如下面的截图所示:

工作原理...

Vue 允许您创建非常轻量级的组件,因为它们没有任何内部状态。但是这也带来了一些限制,例如,我们可以将一些逻辑放在哪里来处理用户输入(以元素的子元素或 props 的形式)只能在渲染函数中。

我们传递的上下文包含以下属性:

  • props:这是由用户传递的。

  • children:这实际上是一个虚拟节点数组,是我们组件在模板中的子元素。在这里我们没有实际的 HTML 元素,只有 Vue 的表示。

  • slots:这是一个返回插槽的函数(在某些情况下可以替代 children)。

  • data:这是传递给组件的整个数据对象。

  • parent:这是对父组件的引用。

在我们的代码中,我们通过以下方式提取了组件内部的文本:

context.children.map(c => {

  return c.text

    .replace(/moveTo/g, 'M')

    .replace(/traceLine/g, 'L')

    .replace(/closePath/g, 'z')

}).join(' ').trim()

我们正在获取包含在 children 中的虚拟节点数组,并将每个节点映射到其文本。由于我们只在 HTML 中放置了一些文本,节点数组将是一个单例,只有一个节点:我们输入的文本。因此,在这种特殊情况下,执行var a = children.map(c => someFunction(c))等同于执行var a = [someFunction(children[0])]

我们不仅提取文本,还替换了一些我发明的用于描述svg命令的术语,用真实的命令替换。join函数将把数组中的所有字符串(在我们的情况下只有一个)拼接在一起,trim函数将删除所有的空格和换行符。

使用高阶组件构建响应式表格

当我们需要决定要实际包装哪个组件时,功能组件是非常好的包装器。在这个示例中,您将编写一个响应式表格,根据浏览器宽度显示不同的列。

准备工作

这个示例是关于功能组件的。如果您想热身一下,可以尝试完成前一个示例。

如何实现...

对于这个示例,我们将使用优秀的语义 UI CSS 框架。要使用它,您必须将 CSS 库作为依赖项或<link>标签包含进来。例如,您可以将以下代码放在 HTML 的<head>中:

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/semantic-ui/2.2.7/semantic.css" />

如果您使用的是 JSFiddle,内部的链接就足够了。

您还需要在页面中添加另一个标签,以便在移动设备上显示良好:

<meta name="viewport" content="width=device-width">

这告诉移动浏览器页面的宽度等于设备的宽度。如果您不添加这个,移动设备可能会认为页面比手机大得多,并试图显示全部内容,从而显示您的应用的缩小版本。

我们将设计一个猫品种的表格。您可以在 Vue 实例状态中看到所有的数据。在您的 JavaScript 中编写如下代码:

new Vue({

  el: '#app',

  data: {

    width: document.body.clientWidth,

  breeds: [

    { name: 'Persian', colour: 'orange', affection: 3, shedding: 5 },

    { name: 'Siberian', colour: 'blue', affection: 5, shedding: 4 },

    { name: 'Bombay', colour: 'black', affection: 4, shedding: 2 }

  ]

  },

  created() {

    window.onresize = event => {

      this.width = document.body.clientWidth

    }

  },

  components: {

    BreedTable

  }

})

我们声明了width变量来改变页面的布局,由于页面的宽度本质上不是响应式的,我们还在window.onresize上安装了一个监听器。对于一个真实的项目,您可能需要更复杂的东西,但对于这个示例,这就足够了。

另外,请注意我们如何使用BreedTable组件,代码如下:

const BreedTable = {

  functional: true,

  render(h, context) {

    if (context.parent.width > 400) {

      return h(DesktopTable, context.data, context.children)

    } else {

      return h(MobileTable, context.data, context.children)

    }

  }

}

我们的组件所做的就是将所有的context.datacontext.children传递给另一个组件,这个组件将是DesktopTableMobileTable,具体取决于分辨率。

我们的 HTML 布局如下:

<div id="app">

  <h1>Breeds</h1>

  <breed-table :breeds="breeds"></breed-table>

</div>

breeds属性将传递给context.data中的选定组件。

我们的桌面表格看起来很普通:

const DesktopTable = {

  template: `

    <table class="ui celled table unstackable">

      <thead>

        <tr>

          <th>Breed</th>

          <th>Coat Colour</th>

          <th>Level of Affection</th>

          <th>Level of Shedding</th>

        </tr>

      </thead>

      <tbody>

        <tr v-for="breed in breeds">

          <td>{{breed.name}}</td>

          <td>{{breed.colour}}</td>

          <td>{{breed.affection}}</td>

          <td>{{breed.shedding}}</td>

        </tr>

      </tbody>

    </table>

  `,

  props: ['breeds']

}

顶部的类是语义 UI 的一部分,它们将使我们的表格看起来更好。特别是unstackable类,它禁用了 CSS 执行的自动堆叠。我们将在下一节中详细介绍这个。

对于移动端的表格,我们不仅希望编辑样式,还希望对列进行分组。品种将与颜色一起显示,情感与脱毛程度一起显示。此外,我们希望以紧凑的样式来表达它们。表头将如下所示:

const MobileTable = {

  template: `

    <table class="ui celled table unstackable">

      <thead>

       <tr>

         <th>Breed</th>

         <th>Affection & Shedding</th>

       </tr>

     </thead>

   ...

我们不仅仅是拼写外套的颜色,还会画一个小圆圈来表示颜色:

...

<tbody>

  <tr v-for="breed in breeds">

    <td>{{breed.name}}

      <div 

        class="ui mini circular image"

        :style="'height:35px;background-color:'+breed.colour"

      ></div>

    </td>

  ...

此外,我们在移动端表格中使用心形和星级评分代替了情感和脱毛程度的数字:

      ...

      <td>

        <div class="ui heart rating">

          <i 

            v-for="n in 5"

            class="icon"

            :class="{ active: n <= breed.affection }"

          ></i>

        </div>

        <div class="ui star rating">

          <i 

            v-for="n in 5"

            class="icon"

            :class="{ active: n <= breed.shedding }"

          ></i>

        </div>

      </td>

    </tr>

  </tbody>

</table>

同时,不要忘记像DesktopTable组件中那样声明breeds属性。

现在在浏览器中启动你的应用程序。你可以看到当表格被压缩到足够小的时候,它会将列进行分组:

下面的截图显示了数字被心形和星级评分所替代:

它的工作原理是...

响应式页面根据浏览器的宽度来改变布局,在用户使用平板电脑或智能手机浏览网站时非常重要。

大多数组件只需要开发一次,响应式页面只需要根据不同的尺寸多次进行样式设计。与为移动端优化的独立网站相比,这样可以节省很多开发时间。

通常,在响应式页面中,表格从列式布局变为堆叠式布局,如下图所示:

我从来不喜欢这种方法,但它的一个明显缺点是,如果你让表格在一侧看起来很好,那么在另一侧看起来就不那么好。这是因为你必须以相同的方式设计单元格,而响应式布局会将它们堆叠起来。

我们的BreedTable组件会动态地在两个组件之间切换,而不仅仅依赖于 CSS。由于它是一个功能性组件,与完整组件相比非常轻量级。

在实际应用中,使用onresize事件是有问题的,主要是因为性能受损。在生产系统中,通过 JavaScript 实现响应性的解决方案需要更加结构化。例如,考虑使用定时器或使用matchMedia

最后要注意的是,Vue 实例从未注册这两个子组件;这是因为它们从未出现在模板中,而是直接在代码中作为对象引用。

第九章:使用 Vuex 的大型应用程序模式

在本章中,我们将介绍以下配方:

  • 在 vue-router 中动态加载页面

  • 构建一个简单的应用程序状态存储

  • 了解 Vuex 的 mutations

  • 在 Vuex 中列出您的操作

  • 使用模块分离关注点

  • 构建 getter 以帮助检索数据

  • 测试您的存储

介绍

在本章中,您将学习 Vuex 的工作原理以及如何使用它来支持可扩展的应用程序。Vuex 实现了一种在前端框架中流行的模式,它将不同的关注点分开管理一个大型全局应用程序状态。只有 mutations 可以改变状态,所以您只需要在一个地方查找。大部分逻辑以及所有异步逻辑都包含在 actions 中;最后,getters 和 modules 进一步帮助在计算派生状态和将代码拆分为不同文件时分散认知负荷。

除了配方之外,您还会发现在开发实际的大型应用程序时我发现有用的一些智慧之粒;有些与命名约定有关,有些则是为了避免错误的小技巧。

如果您完成了所有的配方,您将准备好开发具有较少错误和无缝协作的大型前端应用程序。

在 vue-router 中动态加载页面

很快,您将构建具有大量组件的大型 Vue 网站。加载大量 JavaScript 可能会产生浪费和无用的前期延迟。在第四章的关于组件的一切中的异步加载组件配方中,我们已经看到了如何远程检索组件的提示。在这里,我们将使用类似的技术来加载由 vue-router 路由加载的组件。

准备就绪

这个配方需要了解 vue-router。如果您愿意,您可以通过在第四章的关于组件的一切中的异步加载组件来更好地了解发生了什么。

如何操作...

通过创建一个新目录并运行以下命令,使用vue-cli创建一个新项目:

vue init webpack

你可以根据自己的喜好回答问题,只要在要求时将vue-router添加到模板中即可。

我们将创建两个组件:一个将是我们的主页,它将是小而轻巧的,另一个组件将非常大且加载速度非常慢。我们想要实现的是立即加载主页,而不必等待浏览器下载巨大的组件。

components文件夹中打开Hello.vue文件。删除所有内容,只留下以下内容:

<template>

  <div>

    Lightweight hello

  </div>

</template>

在同一文件夹中,创建另一个名为Massive.vue的文件,并在其中写入以下内容:

<template>

  <div>

   Massive hello

  </div>

</template>

<script>

/* eslint-disable no-unused-vars */

const a = `

在最后一行留下一个打开的反引号,因为我们必须用大量无用的数据膨胀文件。保存并关闭Massive.vue

在控制台中,进入与文件存储在同一目录的位置,并使用以下文件将大量垃圾数据放入其中:

yes "XXX" | head -n $((10**6)) >> Massive.vue

这个命令的作用是将XXX行重复附加到文件中 10⁶次;这将使文件增加 400 万字节,使其对于快速浏览体验来说太大了。

现在我们需要关闭我们打开的反引号。现在不要尝试打开文件,因为你的文本编辑器可能无法打开这么大的文件;相反,使用以下命令:

echo '`</script>' >> Massive.vue

我们的Massive组件现在已经完成。

打开router文件夹中的index.js文件,并添加组件及其路由:

import Massive from '@/components/Massive'

...

export default new Router({

  routes: [

    {

      path: '/',

      name: 'Hello',

      component: Hello

    },

 {

 path: '/massive',

 name: 'Massive',

 component: Massive

 }

  ]

})

使用npm install安装所有依赖项后,我们现在可以使用npm run dev命令启动我们非常大的应用程序了。

应用程序将加载得非常快,但这是因为它直接从本地存储加载;为了模拟更真实的情况,打开开发者工具的网络选项卡,并选择网络限制。选择一些慢速的网络,比如 GPRS 或者较好的 3G,这是我们大多数人可能拥有的:

现在右键单击刷新按钮,选择强制刷新以绕过缓存(或按Shift + Cmd + R):

您会注意到页面在几分钟内不会加载。您可以通过在刷新按钮变成 X 时再次点击来停止页面的加载。

要修复这个问题,请返回到router文件夹中的index.js文件。删除以下行,其中导入Massive组件:

import Massive from '@/components/Massive'

前面的行告诉 Webpack 将Massive组件中包含的所有代码包含在一个 js 包中。相反,我们希望告诉 Webpack 将Massive组件保持为一个单独的包,并且只在必要时加载它。

不要直接导入组件,使用以下代码声明Massive

const Massive = resolve =>

 require(['../components/Massive.vue'], resolve)

Webpack 将把这个特殊的语法转换成一个单独的文件,它将被懒加载。保存并进行另一个硬刷新,同时将限制速度设置为较慢的速度(如 GPRS 到良好的 3G)。几秒钟后,您应该能够看到 hello 页面。如果您想加载Massive组件,只需将massive添加到 URL 中,但您将需要等待一段时间。

它是如何工作的...

现在显然在真实应用程序中不会有这么大的组件,但您可以很容易地看到,如果Massive组件代表应用程序的所有其他组件,它们可能很快达到如此大的大小。

这里的技巧是异步加载它们;Webpack 将帮助您将它们分成较小的包,以便只在需要时加载。

还有更多...

有一种替代的语法可以懒加载导入组件。它可能成为未来的 ECMA 标准,所以您应该知道它。打开router目录中的index.js文件,并完全删除对Massive组件的导入或我们在这个示例中添加的Massive常量行。

在路由中,尝试在指定/massive路由的组件时使用以下代码:

routes:

 [ 

 { 

path:

'/'

, 

name:

'Hello'

, 

component:

Hello 

 }, 

 {

 path:

'/massive'

, 

name:

'Massive'

, 

 component: import('@/components/Massive') 

 } 

] 

这将等同于我们之前所做的,因为 Webpack 将获取该行,并且不会直接导入 Massive 组件的代码,而是创建一个不同的 js 文件,进行懒加载加载。

为应用程序状态构建一个简单的存储

在这个示例中,您将了解在构建大型应用程序时使用 Vuex 的基本原理。这个示例有点不正规,因为为了理解 Vuex 存储的工作原理,我们将直接操作它;在真实的应用程序中,您永远不应该这样做。

准备就绪

在尝试这个示例之前,您应该完成第四章中的“使用 Vuex 使两个组件通信”。

操作步骤如下...

在一个新目录中运行以下命令,基于 Webpack 模板创建一个新项目:

vue init webpack

您如何回答这个问题并不重要。运行npm intall并使用npm install vuex --saveyarn add vuex(如果使用 yarn)安装 Vuex。

打开src文件夹中的main.js文件,并添加以下突出显示的行以完成安装 Vuex:

import Vue from 'vue'

import App from './App'

import router from './router'

import store from './store'

/* eslint-disable no-new */

new Vue({

 el: '#app',

 router,

 store,

 template: '<App/>',

 components: { App }

})

当然,现在没有store模块,所以您需要创建一个。为此,请在src文件夹下创建一个文件夹,并将其命名为store。在其中创建一个名为index.js的文件。在main.js文件中,我们没有指定使用index.js文件,但当没有指定文件而只有文件夹时,这是默认行为。

我们将实现一个简化的股票市场。我们有三种资产:星星(STAR),灯(LAMP)和钻石(DIAM)。我们将定义两个路线:一个用于 STAR/LAMP 市场,另一个用于 LAMP/DIAM 市场。

在 store 文件夹中的index.js文件中编写以下内容:

import Vue from 'vue'

import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({

  state: {

    STAR: 100,

    LAMP: 100,

    DIAM: 100,

    rate: {

      STAR: {

        LAMP: 2

      },

      LAMP: {

        DIAM: 0.5

      }

    }

  }

})

export default store

我们正在创建一个新的Vuex存储,用于保存我们的余额。最初,我们每种资产有 100 个;在存储中,星星和灯之间以及灯和钻石之间的汇率也是固定的。

components目录下创建一个名为Market.vue的新组件。它将具有以下模板:

<template>

  <div class="market">

    <h2>{{symbol1}}/{{symbol2}} Stock Exchange</h2>

    <div class="buy-sell">

      <input v-model.number="amount">{{symbol1}}

      <button @click="buy">

        Buy for {{rate*amount}} {{symbol2}}

      </button>

      <button @click="sell">

        Sell for {{rate*amount}} {{symbol2}}

      </button>

    </div>

  </div>

</template>

symbol1symbol2代表两个交易的资产。在这个组件的 JavaScript 中,我们定义了sellbuy方法,直接在全局的Vuex存储中进行操作:

<script>

export default {

  name: 'market',

  data () {

    return {

      amount: 0

    }

  },

  computed: {

    rate () {

      return this.$store.state.rate[this.symbol1][this.symbol2]

    }

  },

  props: ['symbol1', 'symbol2'],

  methods: {

    buy () {

      this.$store.state[this.symbol1] += this.amount

      this.$store.state[this.symbol2] -= this.amount * this.rate

    },

    sell () {

      this.$store.state[this.symbol1] -= this.amount

      this.$store.state[this.symbol2] += this.amount * this.rate

    }

  }

}

</script>

你永远不应该像我在这里所做的那样直接操作状态。你应该始终使用 mutations。在这里,我们跳过了中间人,以保持这个示例的简洁性。在下一个示例中会更多地介绍 mutations。

你需要在index.js中的router文件夹中以以下方式使用这个组件:

import Vue from 'vue'

import Router from 'vue-router'

import Market from '@/components/Market'

Vue.use(Router)

export default new Router({

  routes: [

    {

      path: '/',

      redirect: '/STAR/LAMP'

    },

    {

      path: '/:symbol1/:symbol2',

      component: Market,

      props: true

    }

  ]

})

在上面的代码中,我们使用Market组件来处理包含一对交易符号的任何路由。作为主页,我们使用了 STAR/LAMP 市场。

为了显示一些导航链接到不同的市场和我们当前的余额,我们可以使用以下模板编辑App.vue组件:

<template>

  <div id="app">

    <nav>

      <ul>

        <li>

          <router-link to="/STAR/LAMP">STAR/LAMP Market</router-link>

        </li><li>

          <router-link to="/LAMP/DIAM">LAMP/DIAM Market</router-link>

        </li>

      </ul>

    </nav>

    <router-view></router-view>

    <div class="balance">

      Your balance is:

      <ul>

        <li>{{$store.state.STAR}} stars</li>

        <li>{{$store.state.LAMP}} lamps</li>

        <li>{{$store.state.DIAM}} diamonds</li>

      </ul>

    </div>

  </div>

</template>

对于这个组件,我们不需要任何 JavaScript 代码,所以你可以删除<script>标签。

我们的应用现在已经准备好了;启动它并开始进行交易。下面的图片是我们完成的应用程序,不包含在App.vue中的样式:

它是如何工作的...

底部的余额就像是全局状态的一个总结。通过访问每个组件中由 Vuex 插件注入的$store变量,我们可以影响其他组件。当你想要将变量的作用域扩展到组件本身之外时,你可以很容易地想象如何使用这种策略在一个大型应用程序中。

一些状态可能是局部的,例如如果你需要一些动画或者你需要一些变量来显示组件的模态对话框;不把这些值放在存储中是完全可以的。否则,在一个地方拥有一个结构化的集中状态会有很大帮助。在接下来的示例中,你将使用更高级的技术来更好地利用 Vuex 的强大功能。

理解 Vuex 的 mutations

在 Vuex 应用程序中,改变状态的正确方式是通过 mutations 的帮助。mutations 是一种非常有用的抽象,用于将状态变化分解为原子单位。在这个示例中,我们将探索这一点。

准备工作

虽然不需要对 Vuex 了解太多就可以完成这个示例,但建议先完成前一个示例。

如何操作...

将 Vuex 作为项目的依赖项添加进来(CDN 地址为https://unpkg.com/vuex)。我假设你正在使用 JSFiddle 进行跟随;否则,请记得在存储代码之前放置Vue.use(Vuex)

我们将构建的示例应用程序是向网站用户广播通知。

HTML 布局如下所示:

<div id="app">

  <div v-for="(message, index) in messages"> 

    <p style="cursor:pointer">{{message}}

      <span @click="close(index)">[x]</span>

    </p>

  </div>

  <input v-model="newMessage" @keyUp.enter="broadcast">

  <button @click="broadcast">Broadcast</button>

</div>

我们的想法是有一个文本框用于编写消息,广播的消息将显示在顶部,最新的消息将首先显示。可以通过点击小 x 来关闭消息。

首先,让我们构建一个存储库,用于保存广播消息的列表,并枚举我们可以对该列表进行的可能的变化:

const store = new Vuex.Store({

  state: {

    messages: []

  },

  mutations: {

    pushMessage (state, message) {

      state.messages.push(message)

    },

    removeMessage (state, index) {

      state.messages.splice(index, 1)

    }

  }

})

所以,我们有一个消息列表;我们可以将一个消息推送到列表的顶部,或者通过知道其索引来删除一个消息。

接下来,我们需要编写应用程序本身的逻辑:

new Vue({

  store,

  el: '#app',

  data: {

    newMessage: ''

  },

  computed: Vuex.mapState(['messages']),

  methods: {

    broadcast () {

      store.commit('pushMessage', this.newMessage)

      this.newMessage = ''

    },

    close (index) {

      store.commit('removeMessage', index)

    }

  }

})

现在,您可以启动应用程序并开始向我们的虚拟用户广播消息了:

工作原理...

我认为值得注意的是突变的名称;它们被称为pushMessageremoveMessage,但在这个应用程序中它们实际上是在屏幕上显示消息并(虚构地)向用户广播消息。将它们称为showMessagebroadcastMessagehideMessage会更好吗?不,因为突变本身和突变的特定效果之间必须有明确的意图分离。当我们决定让用户有能力忽略这些通知或在实际广播通知之前引入延迟时,问题就变得清晰了。然后我们将有一个showMessage突变,它实际上不显示消息。

我们使用的计算语法如下所示:

computed: Vuex.mapState(['messages'])

当您将 Vuex 作为 ES6 模块导入时,您不必在表达式中显式使用 Vuex。您只需要写

import { mapState } from 'Vuex'

然后,mapState函数将可用。

mapState方法接受一个字符串数组作为参数,在存储中查找与字符串同名的state变量,并创建一个同名的计算属性。您可以使用任意多个变量进行此操作。

还有更多...

如果您在本地 npm 项目上跟随,打开 Vue 开发者工具(不幸的是,在使用 JSFiddle 时无法使用 Vue 开发者工具),您将看到每个消息都会发出一个新的突变。考虑一下,您点击了小时钟:

实际上,您可以使用它来撤消突变,如下图所示:

注意当点击时间旅行选项时,状态没有改变;这是因为紫色丝带仍然在最后一个状态。要检查不同的状态,只需点击突变名称。

这个调试机制是可能的,因为突变总是同步的;这意味着可以在突变之前和之后对状态进行快照,并通过时间进行导航。在下一个示例中,您将学习如何使用 Vuex 执行异步操作。

在 Vuex 中列出您的操作

您的所有突变都必须是同步的,那么如何等待超时或使用 Axios 进行 AJAX 请求呢?操作是下一级的抽象层,将帮助您解决这个问题。在操作内部,您可以提交多个突变并进行异步操作。

准备就绪

突变是操作的构建块,因此强烈建议您在尝试此操作之前完成前面的示例。

我们将使用“构建应用程序状态的简单存储”示例中的设置;您也可以使用自己的设置,但无论如何,此示例都基于官方 Webpack 模板的轻微修改。

如何做...

您将构建一个流行的 Xkcd 网站的克隆。实际上,它将更像是一个包装器,而不是一个真正的克隆,因为我们将重用网站上的面板。

基于 Webpack 模板创建一个 Vue 项目,使用vue init webpack。我们将首先在config文件夹中的index.js中将 API 连接到 Xkcd 网站。将以下行放入proxyTable对象中:

module.exports = {

  ...

  dev: {

    proxyTable: {

      '/comic': {

        target: 'https://xkcd.com',

        changeOrigin: true,

        pathRewrite: (path, req) => {

          const num = path.split('/')[2]

          return `/${num}/info.0.json`

        }

      }

    },

  ...

这将将我们发出的所有请求重定向到/comic到 Xkcd 网站。

src中,创建一个新的store目录,并在其中创建一个index.js文件;在这里,开始构建应用程序存储:

import Vue from 'vue'

import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({

  state: {

    currentPanel: undefined,

    currentImg: undefined,

    errorStack: []

  },

  actions: {},

  mutations: {}

}

export default store

您应该像在之前的示例中那样在main.js中导入它。我们想要跟踪当前面板编号、面板图像的链接和可能的错误。修改状态的唯一方法是通过突变,而操作可以执行异步工作。

当应用程序加载时,我们计划显示最新的漫画。为此,我们创建一个操作:

actions: {

  goToLastPanel ({ commit }) {

    axios.get(endpoint)

      .then(({ data }) => {

        commit('setPanel', data.num)

        commit('setImg', data.img)

      }).catch(error => {

        commit('pushError', error)

      })

  }

 ...

为了使此代码工作,我们需要声明端点并安装 Axios:

...

import axios from 'axios'

...

const endpoint = '/comic/'

编写相应的变异应该很容易:

mutations: {

  setPanel (state, num) {

    state.currentPanel = num

  },

  setImg (state, img) {

    state.currentImg = img

  },

  pushError (state, error) {

    state.errorStack.push(error)

  }

}

我们将重用Hello.vue组件,并将以下模板放在其中:

<template>

  <div class="hello">

    <h1>XKCD</h1>

    <img :src="currentImg">

  </div>

</template>

要在加载时显示最后一个面板,可以在组件中使用以下 JavaScript 代码:

<script>

import { mapState } from 'vuex'

export default {

  name: 'hello',

  computed: mapState(['currentImg']),

  created () {

    this.$store.dispatch('goToLastPanel')

  }

}

</script>

此外,您可以删除大部分App.vue模板,只保留以下内容:

<template>

  <div id="app">

    <router-view></router-view>

  </div>

</template>

它是如何工作的...

proxyTable对象将配置http-proxy-middleware。每当我们开发一个较大的 Web 应用程序的 UI,并在localhost上启动我们的开发服务器时,这非常有用,但我们的 API 响应到另一个 Web 服务器。当我们想要使用 CORS 并且不允许其他网站使用我们的 API 时,这一点尤为重要。Xkcd API 不允许localhost使用 Web 服务。这就是为什么,即使我们尝试直接使用 Xkcd API,我们的浏览器也不会让我们这样做。changeOrigin选项将使用 Xkcd 作为主机发送请求,从而使 CORS 变得不必要。

要从组件中调用一个动作,我们使用了dispatch函数。还可以传递第二个参数,第一个参数是动作本身的名称。然后,在定义动作时,将第二个参数作为第二个参数传递。

关于命名的最后一点说明——在我的观点中,动作是异步的,而变异是同步的,因此没有必要在动作的名称中显式地表明异步性。

使用模块分离关注点

在构建大型应用程序时,Vuex 存储可能会变得拥挤。幸运的是,可以使用模块将应用程序的不同关注点分成单独的部分。

准备工作

如果您想使用模块,可以将此示例作为参考。您应该已经了解足够多的 Vuex 知识。

对于这个示例,您需要对 Webpack 有一定的了解。

如何操作...

在这个示例中,我们将以稍微简化的方式对一个完全功能的人体进行建模。每个器官都将有一个单独的模块。

使用 vue init webpack 创建一个新的 Webpack 模板,并安装 npm install vuex。创建一个包含 src/store/index.js 文件的新目录。在其中,写下以下内容:

import Vue from 'vue'

import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({

  modules: {

    brain,

    heart

  }

})

export default store

heart 模块是这样的;在 store 声明之前放置它:

const heart = {

  state: { loves: undefined },

  mutations: {

    love (state, target) {

      state.loves = target

    },

    unlove (state) {

      state.loves = undefined

    }

  }

}

注意,在 mutations 中传递的状态不是根状态,而是模块的本地状态。

然后是大脑,它被分为左右两个脑叶;在 store 之前写下以下内容:

const brain = {

  modules: {

    left: leftLobe,

    right: rightLobe

  }

}

你可以将它们实现为简单的布尔状态(在它们所依赖的大脑之前写下它们):

const leftLobe = {

  namespaced: true,

  state: { reason: true },

  mutations: {

    toggle (state) { state.reason = !state.reason }

  }

}

const rightLobe = {

  namespaced: true,

  state: { fantasy: true },

  mutations: {

   toggle (state) { state.fantasy = !state.fantasy }

  }

}

namespaced 设置为 true 会修改你调用 mutator 的方式。由于它们都被称为 toggle,现在你可以指定是哪个脑叶,例如,对于左脑,突变字符串变为 left/toggle,其中 left 表示它是大脑中用于引用左脑的键。

要查看你的 store 在运行中的情况,你可以创建一个使用所有 mutations 的组件。对于大脑,我们可以有两个脑叶的图片,如下所示:

<img 

 :class="{ off: !$store.state.brain.left.reason }"

 src="http://i.imgur.com/n8B6wuY.png"

 @click="left"><img

 :class="{ off: !$store.state.brain.right.fantasy }"

 src="http://i.imgur.com/4BbfVur.png"

 @click="right">

这将创建两个红铅笔绘制的脑叶图;注意嵌套方式中模块名称的使用。以下的 off CSS 规则将使脑叶变灰:

.off {

  filter: grayscale(100%)

}

为了调用 mutations,我们在正确的方法中使用上述字符串:

methods: {

  left () {

    this.$store.commit('left/toggle')

  },

  right () {

    this.$store.commit('right/toggle')

  }

}

你还可以创建一个输入文本框并调用其他两个 mutations,如下所示:

...

love () {

  this.$store.commit('love', this.partner)

},

clear () {

  this.$store.commit('unlove')

  this.partner = undefined

}

...

这很简单,但是如何获取所爱的名称呢?你可以在模板中使用这些胡子:

<p>

 loves: {{$store.state.heart.loves}}</p>

<input v-model="partner" @input="love">

<button @click="clear">Clear</button>

显然,你必须在 Vue 实例上声明 partner 变量:

它是如何工作的...

我们已经看到如何使用模块将应用程序的关注点分割成不同的单元。随着项目规模的增长,这种能力可能变得重要。

通常的模式是,在一个 mutation 中,你可以直接访问本地状态:

const leftLobe = {

  namespaced: true,

  state: { reason: true },

  mutations: {

    toggle (state) {

      // here state is the left lobe state

      state.reason = !state.reason

    }

  }

}

在 mutation 中,只能访问本地状态是有意义的。例如,大脑不能改变心脏,反之亦然,但是动作呢?如果我们在模块内声明一个动作,我们会传递一个名为 context 的对象,它看起来像这样:

{

  "getters":{},

  "state":{

    "reason":true

  },

  "rootGetters":{},

  "rootState":{

    "brain":{

      "left":{

        "reason":true

      },

      "right":{

        "fantasy":false

      }

    },

    "heart":{

      "loves": "Johnny Toast"

    }

  }

}

所以,如果我们想在左侧叶子中声明一个动作,并且我们想要影响心脏,我们必须做以下操作:

actions: {

  beNerd ({ rootState }) {

    rootState.heart.loves = 'Math & Physics'

  }

}

构建 getter 来帮助检索数据

你不想在状态中保留太多数据。保留重复或派生数据尤其危险,因为它很容易使数据不同步。Getter 可以帮助你解决这个问题,而不会将负担转移到组件上,因为所有逻辑都集中在一个地方。

准备工作

如果你已经具备一些 Vuex 知识并且想要扩展你的视野,那么这个教程适合你。

如何做到这一点...

想象一下,你正在构建一个比特币钱包。你想给用户一个关于他们余额的概览,并且你希望他们看到对应的欧元数量。

使用vue init webpacknpm install vuex创建一个新的 Webpack 模板。创建一个新的src/store/index.js文件,并在其中写入以下内容:

import Vue from 'vue'

import Vuex from 'vuex'

Vue.use(Vuex)

const store = new Vuex.Store({

  state: {

    bitcoin: 600,

    rate: 1000,

    euro: 600000

  }

})

export default store

这段代码容易出错。第一个错误可能是欧元金额的计算错误,如果我们没有正确进行乘法运算。第二种错误可能是在交易过程中告诉用户bitcoineuro余额,导致其中一个金额过时和错误。

为了解决这些问题,我们使用getters

const store = new Vuex.Store({

  state: {

    bitcoin: 600,

    rate: 1000

  },

  getters: {

    euro: state => state.bitcoin * state.rate

  }

})

这样,euro金额永远不会在状态中,而是始终计算得出。此外,它集中在存储中,所以我们不需要在组件中添加任何内容。

现在,从模板中轻松检索两个金额:

<template>

  <div>

    <h1>Balance</h1>

    <ul>

      <li>{{$store.state.bitcoin}}฿

</li>

      <li>{{$store.getters.euro}}&euro;</li>

    </ul>

  </div>

</template>

这里,&#3647 ;是比特币符号的 HTML 实体。

它是如何工作的...

如果我们不谈论输入数据,那么为派生数据设置一个getter总是一个好主意。我们还没有讨论过 getter 的一个显著特点是它们能够与其他 getter 进行交互并接受一个参数。

访问其他 getter

当调用 getter 时传递的第二个参数是包含其他getter的对象:

getters

:

{ 

 ...

  getCatPictures: state => state.pictures.filter(pic => isCat(pic)) 

 getKittens

:

(

state

,

 getters

)

=

>

{ 

 return

 getters

.

getCatPictures()

.

filter

(cat

=

>

 !isAdult(cat)

) 

 } 

} 

在我们的示例中,我们可以调用euro getter 来获得一些派生数据,例如我们可以用平均价格为 150,000 欧元的比特币购买多少房屋:

const store = new Vuex.Store({

  state: {

    bitcoin: 600,

    rate: 1000

  },

  getters: {

    euro: state => state.bitcoin * state.rate,

    houses: (state, getters) => 

getters.euro() / 150000

})

传递参数

如果一个 getter 返回一个带有参数的函数,那么该参数将是 getter 的参数:

getters: {

  ...

  getWorldWonder: state => nth => state.worldWonders[nth]

}

在我们的示例中,一个实际的例子可以在前一段中的 getter 中指定房屋的平均成本:

const store = new Vuex.Store({

  state: {

    bitcoin: 600,

    rate: 1000

  },

  getters: {

    euro: state => state.bitcoin * state.rate,

    houses: (state, getters) => averageHousePrice => {

 return getters.euro() / averageHousePrice

 }

})

测试你的商店

正如你从第七章中所了解的,测试是专业软件中最重要的部分。由于商店通常定义应用程序的业务逻辑,因此对其进行测试对于应用程序可能是至关重要的。在这个示例中,你将为 Vuex 商店编写测试。

准备工作

这个示例需要来自第七章的知识,即单元测试和端到端测试以及对 Vuex 的熟悉;你可以从本章的早期示例中获得它。

如何做...

首先,我将定义一些我们的商店必须实现的功能;然后你将编写测试来证明这些功能是否存在并且正常工作。

软件要求

我们的商店由待办事项列表中的项目组成,如下所示:

state: {

  todo: [

    { id: 43, text: 'Buy iPhone', done: false },

    ...

  ],

  archived: [

    { id: 2, text: 'Buy gramophone', done: true },

    ...

  ]

}

我们有两个要求:

  • 我们必须有一个MARK_ITEM_AS_DONE变异,将done字段从 false 更改为 true

  • 我们必须有一个downloadNew操作,从服务器下载最新的项目并将它们添加到列表中

测试变异

要能够测试你的变异,你必须使它们在测试文件中可用。为此,你必须从商店中提取变异对象。考虑类似于这样的东西:

import Vuex from 'vuex'

import Vue from 'vue'

Vue.use(Vuex)

const store = new Vuex.Store({

  ...

  mutations: {

    ...

    MARK_ITEM_AS_DONE (state, itemId) {

      state.todo.filter(item => {

        return item.id === itemId

      }).forEach(item => {

        item.done = true

      })

      state.archived.filter(item => {

        return item.id === itemId

      }).forEach(item => {

        item.done = true

      })

    }

  }

}) 

export default store

您必须将其提取到类似以下的内容中:

export const mutations = { ... }

const store = new Vuex.Store({ ... })

export default store

这样,您可以在测试文件中使用以下代码导入 mutations:

import { mutations } from '@/store'

满足需求 1 的测试可以编写如下:

describe('mutations', () => {

  it(`MARK_ITEM_AS_DONE mutation must change the

        done field from false to true for a todo`, () => {

    const state = {

      todo: [

        { id: 43, text: 'Buy iPhone', done: false }

      ],

      archived: [

        { id: 40, text: 'Buy cat', done: false }

      ]

    }

    mutations.MARK_ITEM_AS_DONE(state, 43)

    expect(state.todo[0].done).to.be.true

  })

})

如果您使用官方的 Webpack 模板,可以使用npm run unit运行测试。默认情况下,这将使用 PhantomJS,它不实现某些功能。您可以使用 Babel polyfills,或者只需进入karma.conf.js并在browsers数组中将PhantomJS替换为Chrome。记得使用npm install karma-chrome-launcher --save-dev安装 Chrome launcher。

测试 actions

测试 actions意味着测试操作是否提交了预期的 mutations。我们对 mutations 本身不感兴趣(至少在单元测试中不感兴趣),因为它们已经单独测试过了。但是,我们可能需要模拟一些依赖项。

为了避免依赖于 Vue 或 Vuex(因为我们不需要它们,而且它们可能会污染测试),我们在 store 目录中创建一个新的actions.js文件。使用npm install axios安装 Axios。actions.js文件可以如下所示:

import axios from 'axios'

export const actions = {

  downloadNew ({ commit }) {

    axios.get('/myNewPosts')

      .then(({ data }) => {

        commit('ADD_ITEMS', data)

      })

  }

}

为了测试需求 2,我们首先模拟应该下载新的待办事项的服务器调用:

describe('actions', () => {

const actionsInjector = 

  require('inject-loader!@/store/actions')

const buyHouseTodo = {

  id: 84,

  text: 'Buy house',

  done: true

}

const actions = actionsInjector({

  'axios': {

    get () {

      return new Promise(resolve => {

        resolve({

          data: [buyHouseTodo]

        })

      })

    }

  }

}).default

}

这将确保对axios的 get 方法的任何调用都将始终返回一个新的待办事项。

然后,我们希望确保在调度时调用ADD_ITEMS mutation:

describe('actions', () => {

  const actionsInjector = 

    require('inject-loader!@/store/actions')

    const buyHouseTodo = {

      id: 84,

      text: 'Buy house',

      done: true

    }

    const actions = actionsInjector({

      'axios': {

        get () {

          return new Promise(resolve => {

            resolve({ data: [buyHouseTodo] })

          })

        }

      }

    }).default

    it(`downloadNew should commit ADD_ITEMS

    with the 'Buy house' todo when successful`, done => {

    const commit = (type, payload) => {

      try {

        expect(type).to.equal('ADD_ITEMS')

        expect(payload).to.deep.equal([buyHouseTodo])

        done()

      } catch (error) {

        done(error)

      }

    }

  actions.downloadNew({ commit })

  })

})

工作原理如下...

虽然对 mutations 的测试非常简单,但我认为对 actions 的测试值得更多解释。

由于我们不想依赖外部服务来执行操作,我们不得不模拟axios服务。我们使用了inject-loader,它接受原始库并使用任意代码模拟我们指定的部分(@符号是src的简写);在我们的情况下,我们模拟了axios库,特别是get方法。我们必须使用 CommonJS 语法(使用require)因为这是告诉 Webpack 在导入时使用加载器的唯一方式。

在测试中,我们所做的是模拟commit函数。通常,这个函数调用一个修改状态的 mutation。我们只想知道是否调用了正确的 mutation,并且传入了正确的参数。此外,我们不得不将所有内容包装在一个try块中;如果没有它,测试将超时失败,我们将丢失错误信息。相反,现在我们立即失败,并且可以从控制台中读取导致测试失败的错误。

第十章:与其他框架集成

在本章中,我们将探讨以下主题:

  • 使用 Electron 构建通用应用程序

  • 使用 Vue 和 Firebase

  • 使用 Feathers 创建实时应用程序

  • 使用 Horizon 创建响应式应用程序

介绍

Vue 很强大,但如果你需要一个后端,它就不能独自完成太多事情;至少你需要一个服务器来部署你的软件。在本节中,您将实际上使用流行的框架构建小而完整、可工作的应用程序。Electron 用于将 Vue 应用程序带到桌面。Firebase 是一个现代化的云后端,最后,FeatherJS 是一个简约但功能齐全的 JavaScript 后端。当您完成这些后,您将拥有与它们交互并快速构建专业应用程序所需的所有工具。

使用 Electron 构建通用应用程序

Electron 是一个用于创建在 Mac、Linux 和 Windows 上运行的通用应用程序的框架。它的核心是一个简化版本的 Web 浏览器。它已被用于创建广泛使用的应用程序,如 Slack 和 Visual Studio Code 等。在这个示例中,您将使用 Electron 构建一个简单的应用程序。

准备工作

要构建这个应用程序,我们将只使用基本的 Vue 功能。Electron 超出了本书的范围,但对于这个示例,不需要了解 Electron 的知识;事实上,这是学习更多关于 Electron 的好起点。

如何做到这一点...

在这个示例中,我们将构建一个小而完整的应用程序——一个番茄钟应用程序。番茄钟是一个大约 25 个时间单位的间隔,在这个间隔中,您应该专注于工作。它被称为这个名字,因为通常使用一个番茄形状的厨房计时器来测量时间。这个应用程序将跟踪时间,这样您就不必购买一个昂贵的厨房计时器了。

使用 Electron-Vue 脚手架是启动一个 Vue 项目与 Electron 的最佳方式(你不说!)。可以通过以下命令轻松实现:

vue init simulatedgreg/electron-vue pomodoro

您可以使用默认值进行回答,但当被问及要安装哪个插件时,只需选择vue-electron。使用npm intall安装所有依赖项,如果愿意,您可以在进行必要的修改时保持应用程序处于热重载状态,使用npm run dev。您可以通过点击角落的x来隐藏开发工具:

首先,我们希望我们的应用程序尽可能小。让我们转到app/src/main/index.js文件;此文件控制我们应用程序的生命周期。将窗口大小更改为以下内容:

mainWindow = new BrowserWindow({

  height: 200,

  width: 300

})

然后,我们实际上不希望在app/src/render/components文件夹中有样板组件,所以您可以删除所有内容。相反,创建一个Pomodoro.vue文件并将此模板放入其中:

<template>

  <div class="pomodoro">

    <p>Time remaining: {{formattedTime}}</p>

    <button v-if="remainingTime === 1500" @click="start">Start</button>

    <button v-else @click="stop">Stop</button>

  </div>

</template>

为了使其工作,我们还需要编写 JavaScript 部分,如下所示:

<script>

export default {

  data () {

    return {

      remainingTime: 1500,

      timer: undefined

    }

  },

  methods: {

    start () {

      this.remainingTime -= 1

      this.timer = setInterval(() => {

        this.remainingTime -= 1

        if (this.remainingTime === 0) {

          clearInterval(this.timer)

        }

      }, 1000)

    },

    stop () {

      clearInterval(this.timer)

      this.remainingTime = 1500

    }

  }

}

</script>

这样,点击程序中的开始按钮将每秒减少 1 秒。点击停止按钮将清除计时器并将剩余时间重置为 1500 秒(25 分钟)。计时器对象基本上是setInterval操作的结果,而clearInterval只是停止计时器正在执行的任何操作。

在我们的模板中,我们希望有一个formattedTime方法,以便以mm:ss格式显示时间,这比仅剩余秒数(即使这更加极客)更易读,因此我们需要添加计算函数:

computed: {

  formattedTime () {

    const pad = num => ('0' + num).substr(-2)

    const minutes = Math.floor(this.remainingTime / 60)

    const seconds = this.remainingTime - minutes * 60

    return `${minutes}:${pad(seconds)}`

  }

}

要将此组件添加到应用程序中,请转到App.vue文件并编辑以下行,替换landingPage占位符元素:

<template>

  <div id="#app">

 <pomodoro></pomodoro>

  </div>

</template>

<script>

 import Pomodoro from 'components/Pomodoro'

  export default {

    components: {

 Pomodoro

    }

  }

</script>

使用npm run dev启动应用程序,您现在应该能够在工作或学习时跟踪时间:

您甚至可以使用npm run build命令构建应用程序的可分发版本。

它是如何工作的...

我们实现的计时器对于时间跟踪来说并不特别准确。让我们来审查一下代码:

this.timer = setInterval(() => {

  this.remainingTime -= 1

  if (this.remainingTime === 0) {

    clearInterval(this.timer)

  }

}, 1000)

这意味着我们每秒减少剩余时间。问题在于setInterval函数本身不是 100%准确的,可能会在 1000 毫秒之前或之后触发函数,这取决于机器的计算负载;这样,误差的边界可以累积并成为相当大的数量。更好的方法是在每次调用函数时检查时钟并在每个循环中调整误差,尽管我们不会在这里涵盖这个问题。

使用 Vue 和 Firebase

使用 Vue 和 Firebase 作为后端非常容易,这要归功于 VueFire - 一个包含 Firebase 绑定的插件。在这个示例中,您将开发一个完全功能的气味数据库。

准备工作

Firebase 超出了本书的范围,但是我假设您对基本概念有所了解。除此之外,您真的没有太多需要了解的,因为我们将在其上构建一个非常基本的 Vue 应用程序。

如何做...

在开始编写代码之前,我们需要创建一个新的 Firebase 应用程序。要做到这一点,您必须登录firebase.google.com/并创建一个新的应用程序。在我们的例子中,它将被称为smell-diary。您还需要记下您的 API 密钥,该密钥可以在项目设置中找到:

此外,您需要禁用身份验证;转到数据库部分,在规则选项卡中将读取和写入都设置为 true:

{

  "rules": {

    ".read": true,

    ".write": true

  }

}

我们已经完成了 Firebase 的配置。

打开一个干净的 HTML5 样板或 JSFiddle,使用Vue作为库。我们将需要以下依赖项,以脚本标签的形式放在文件的头部:

 <script src="https://unpkg.com/vue/dist/vue.js"></script>

 <script src="https://www.gstatic.com/firebasejs/3.6.9/firebase.js"></script>

 <script src="https://unpkg.com/vuefire/dist/vuefire.js"></script>

VueFire 将自动检测到 Vue(因此顺序很重要)并将其安装为插件。我们将构建一个非常简单的数据库来跟踪我们周围事物的气味。以下是我们应用程序的 HTML 布局:

<div id="app">

  <ul>

    <li v-for="item in items">

      {{item.name}}: {{item.smell}}

    <button @click="removeItem(item['.key'])">X</button>

    </li>

  </ul>

  <form @submit.prevent="addItem">

    <input v-model="newItem" />

    smells like

    <input v-model="newSmell" />

    <button>Add #{{items.length}}</button>

  </form>

</div>

在我们应用的 JavaScript 部分,我们需要指定 API 密钥以与 Firebase 进行身份验证,写入以下内容:

const config = {

  databaseURL: 'https://smell-diary.firebaseio.com/'

}

然后,我们将配置提供给 Firebase 并获取数据库的控制权:

const firebaseApp = firebase.initializeApp(config)

 const db = firebaseApp.database()

这可以在Vue实例之外完成。VueFire 插件在Vue实例中安装了一个名为firebase的新选项;我们必须指定我们想要使用item变量访问 Firebase 应用中的/items

new Vue({

  el: '#app',

  firebase: {

    items: db.ref('/items')

  }

})

newItemnewSmell变量将临时保存我们在输入框中输入的值;然后,addItemremoveItem方法将发布和删除我们数据库中的数据:

data: {

  newItem: '',

  newSmell: ''

},

methods: {

  addItem () {

    this.$firebaseRefs.items

      .push({

        name: this.newItem,

        smell: this.newSmell

      })

    this.newItem = ''

    this.newSmell = ''

  },

  removeItem (key) {

    this.$firebaseRefs.items

      .child(key).remove()

  }

}

如果你现在启动你的应用,你已经可以添加你最喜欢的香味和找到它们的方法了:

它是如何工作的...

Firebase 作为一个简单的键值存储。在我们的例子中,我们从来没有存储值,而是总是添加子项;你可以在 Firebase 控制台上查看你创建的内容。

键是自动创建的,它们包含空值和 32 层嵌套数据。我们使用一层嵌套来为每个对象插入名称和气味。

使用 Feathers 创建实时应用

大多数现代应用程序是实时的,不是传统意义上的实时,而是指它们不需要重新加载页面就可以更新。实现这一点的最常见方法是通过 WebSockets。在这个示例中,我们将利用 Feathers 和 Socket.io 来构建一个猫数据库。

准备工作

这个示例没有先决条件,但是如果你想要更多的上下文,你可以在开始这个示例之前完成*创建一个 REST 客户端(和服务器!)*示例。

如何操作...

要完成这个示例,你需要 Feathers 的命令行;使用以下命令安装它:

npm install -g feathers-cli

现在,运行feathers generate,它将为你创建所有的样板代码。当被询问到 API 时,选择 Socket.io:

其他问题可以保持默认值。在 Feather 控制台中,输入generate service创建一个新的服务。你可以将其命名为 cats,并将其他问题保持默认值。

public文件夹中,打开index.html并删除除了 HTML5 模板之外的所有内容。你需要在头部添加三个依赖项:

 <script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.1.10/vue.js"></script>

 <script src="//cdnjs.cloudflare.com/ajax/libs/socket.io/1.7.3/socket.io.js"></script>

 <script src="//unpkg.com/feathers-client@¹.0.0/dist/feathers.js"></script>

body标签中,按照以下方式编写 HTML 布局:

<div id="app">

  <div v-for="cat in cats" style="display:inline-block">

    <img width="100" height="100" :src="cat.url" />

    <p>{{cat.name}}</p>

  </div>

  <form @submit.prevent="addCat">

    <div>

      <label>Cat Name</label>

      <input v-model="newName" />

    </div>

    <div>

      <label>Cat Url</label>

      <input v-model="newUrl" />

    </div>

    <button>Add cat</button>

    <img width="30" height="30" :src="newUrl" />

  </form>

</div>

第一个<div>标签是一个猫的画廊。然后,构建一个表单来添加你收集到的猫的新图片。

body标签中,你可以使用以下代码配置 Feathers 服务:

<script>

  const socket = io('http://localhost:3030')

  const app = feathers()

    .configure(feathers.socketio(socket))

  const catService = app.service('cats')

这是为了配置将连接到 WebSockets 的浏览器的客户端。catService方法是对猫数据库的处理。接下来,我们编写Vue实例:

  new Vue({

    el: '#app',

    data: {

      cats: [],

      newName: '',

      newUrl: ''

    },

    methods: {

      addCat () {

        catService.create({

          name: this.newName,

          url: this.newUrl

        })

        this.newName = ''

        this.newUrl = ''

      }

    },

最后,在启动时我们需要请求数据库中的所有猫,并安装一个监听器以防其他用户创建新的猫:

    mounted () {

      catService.find()

        .then(page => {

          this.cats = page.data

        })

      catService.on('created', cat => {

        this.cats.push(cat)

      })

    }

 })

 </script>

如果你使用npm start运行应用程序,你可以在控制台中看到 URL,打开另一个浏览器窗口,观察它实时变化:

工作原理...

实时查看添加的猫显然是现代应用的发展方向。Feathers 让你可以轻松创建它们,并且只需少量的代码,这要归功于底层的 Socket.io,而 Socket.io 又使用了 WebSockets。

WebSockets 并不复杂,Feathers 在这种情况下只是监听通道中的消息,并将其与将某些内容添加到数据库的操作关联起来。

当你可以仅仅更换数据库和 WebSocket 提供程序,或者切换到 REST 而不需要修改 Vue 代码时,Feathers 的强大之处就显现出来了。

使用 Horizon 创建一个响应式应用

Horizon 是一个构建响应式、实时可扩展应用的平台。它在内部使用 RethinkDB,并且与 Vue 完全兼容。在这个示例中,你将构建一个自动个人日记。

准备工作

这个示例只需要一些 Vue 的基础知识,但真的没有其他太多的东西。

但在开始之前,请确保您安装了 RethinkDB。您可以在他们的网站上找到更多信息(www.rethinkdb.com/docs/install/)。如果您有 Homebrew,可以使用brew install rethinkdb来安装它。

此外,您将需要一个 Clarifai 令牌。要免费获取一个,请访问developer.clarifai.com/并注册。您将看到您应该在应用程序中编写的代码,如下图所示:

特别是,您将需要clientIdclientSecret,它们以以下方式显示:

var

 app = 

new

 Clarifai.App( 

 'your client id would be printed here'

, 

 'your client secret would be here' 

);

记下这段代码,或者准备好将其复制粘贴到你的应用程序中。

如何做...

写日志是一项困难的任务,您每天都要写很多东西。在这个示例中,我们将构建一个自动日志,它将根据我们在一天中拍摄的照片来为我们写作。

Horizon 将帮助我们记住一切,并在我们的设备之间同步日记。在安装了 RethinkDB 之后,使用以下命令安装 Horizon:

npm install -g horizon

现在,您将拥有新的命令hz。通过输入hz -h来检查它;您应该会看到类似以下的内容:

要创建将托管我们新应用程序的目录,请输入以下内容:

hz init vue_app

然后,进入新创建的vue_app目录,查看dist文件夹中的index.html。这个文件将是我们服务器的入口点,用编辑器打开它。

您可以清除所有内容,只留下一个空的 HTML5 样板,其中包含一个空的<head><body>。在头部部分,我们需要声明对 Vue、Horizon 和 Clarifai 的依赖关系,如下所示:

 <script src="https://unpkg.com/vue"></script>

 <script src="/horizon/horizon.js"></script>

 <script src="https://sdk.clarifai.com/js/clarifai-latest.js"></script>

请注意,Horizon 不是来自 CDN,而是来自本地依赖。

我们首先要为我们的日志制定一个模板。我们有两个部分。在第一个部分,我们将列出我们过去做过的事情。在 HTML 的主体中写入以下内容:

<div id="app">

  <div>

    <h3>Dear diary...</h3>

    <ul>

      <li v-for="entry in entries">

        {{ entry.datetime.toLocaleDateString() }}:

        {{ entry.text }}

      </li>

    </ul>

  </div>

...

在第二部分中,我们将输入新的条目:

  ...

  <h3>New Entry</h3>

  <img

    style="max-width:200px;max-height:200px"

    :src="data_uri"

  />

  <input type="file" @change="selectFile" ref="file">

  <p v-if="tentativeEntries.length">Choose an entry</p>

  <button v-for="tentativeEntry in tentativeEntries" @click="send(tentativeEntry)">

    {{tentativeEntry}}

  </button>

</div>

在此之后,打开一个<script>标签,我们将在其中编写以下所有的 JavaScript 代码。

首先,我们需要登录到 Clarifai:

var app = new Clarifai.App(

 '7CDIjv_VqEYfmFi_ygwKsKAaDe-LwEzc78CcW1sA',

 'XC0S9GHxS0iONFsAdiA2xOUuBsOhAT0jZWQTx4hl'

 )

显然,您需要输入您从 Clarifai 获取的clientIdclientSecret

然后,我们需要启动 Horizon 并获得我们将要创建的entries集合的句柄:

const horizon = new Horizon()

const entries = horizon('entries')

现在,我们终于可以编写我们的Vue实例,其中包含三个状态变量:

new Vue({

  el: '#app',

  data: {

    tentativeEntries: [],

    data_uri: undefined,

    entries: []

  },

  ...

tentativeEntries数组将包含我们可以从中选择的日记的可能条目列表;data_uri将包含我们要用作今天所做的事情的参考图像(base64 编码);entries是所有过去的条目。

当我们加载一张图片时,我们要求 Clarifai 提供可能的条目:

...

methods: {

  selectFile(e) {

  const file = e.target.files[0]

  const reader = new FileReader()

  if (file) {

    reader.addEventListener('load', () => {

      const data_uri = reader.result

      this.data_uri = data_uri

      const base64 = data_uri.split(',')[1]

      app.models.predict(Clarifai.GENERAL_MODEL, base64)

        .then(response => {

          this.tentativeEntries =

            response.outputs[0].data.concepts

            .map(c => c.name)

        })

      })

    reader.readAsDataURL(file)

  }

},

...

然后,当我们按下发送按钮时,我们告诉 Horizon 的 entries 集合存储这个新条目:

    ...

    send(concept) {

      entries.store({

        text: concept,

         datetime: new Date()

      }).subscribe(

        result => console.log(result),

        error => console.log(error)

      )

      this.tentativeEntries = []

      this.$refs.file.value = ''

      this.data_uri = undefined

    }

  }

})

最后,我们希望在页面加载时在屏幕上显示最后十个条目,并且每次添加新条目时,它都会实时弹出。在 Vue 实例中的方法之后,添加以下钩子:

created() {

  entries.order('datetime', 'descending').limit(10).watch()

    .subscribe(allEntries => {

      this.entries = [...allEntries].reverse()

  })

}

要运行 Horizon 服务器,请使用以下命令:

hz serve --dev

上述代码的输出如下:

前往指定地址(第一行,而不是管理员界面),您将看到以下内容:

您会注意到,如果您有其他浏览器窗口打开,它们将实时更新。现在,您终于可以每天写一篇日记而无需打字了!

它是如何工作的...

我们的应用程序使用了一种被称为响应式的模式。它的核心可以在创建的句柄中清楚地看到:

entries.order('datetime', 'descending').limit(10).watch()

  .subscribe(allEntries => {

    this.entries = [...allEntries].reverse()

  })

第一行返回的是所谓的响应式中的 observable。Observable 可以被视为事件的源。每当一个事件被触发时,订阅该源的订阅者将对其进行处理。在我们的例子中,我们正在获取整个 entries 集合,而被抛出的事件是对该集合的修改。每当我们接收到这种类型的事件时,我们会更新entries数组。

我不会在这里提供对响应式编程的深入解释,但我想强调这种模式非常有助于可扩展性,因为你可以很容易地实现对数据流的控制;limit(10)就是一个例子。

posted @ 2024-05-16 12:09  绝不原创的飞龙  阅读(7)  评论(0编辑  收藏  举报