VueJS2-学习指南-全-

VueJS2 学习指南(全)

原文:zh.annas-archive.org/md5/0B1D097C4A60D3760752681016F7F246

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

这本书是关于 Vue.js 的。我们将开始我们的旅程,试图理解 Vue.js 是什么,它与其他框架相比如何,以及它允许我们做什么。我们将在构建小型有趣的应用程序的同时学习 Vue.js 的不同方面,并将这些方面应用到实践中。最后,我们将回顾所学到的内容,并展望未来,看看我们还能学到什么并做些什么。因此,您将学到以下内容:

  • Vue.js 是什么以及它是如何工作的

  • Vue.js 的响应性和数据绑定

  • Vue.js 可重用组件

  • Vue.js 的插件

  • 测试和部署使用 Vue.js 编写的应用程序

本书中的所有示例都是基于最近发布的 Vue 2.0 版本构建的。该书还包含了对先前版本的引用,涉及框架的已弃用或已更改的方面。

我相信您会喜欢使用本书构建 Vue.js 应用程序的过程。

本书涵盖的内容

第一章《使用 Vue.js 购物》,包括对 Vue.js 的介绍,本书中使用的术语以及第一个基本示例。

第二章《基础知识-安装和使用》解释了 Vue.js 的幕后情况,提供了对架构模式的理论见解,涵盖了几乎所有主要的 Vue.js 概念,并引导了本书中将开发的应用程序。

第三章《组件-理解和使用》深入探讨了组件,并解释了如何使用简单的组件系统和单文件组件重写应用程序。

第四章《响应性-将数据绑定到您的应用程序》详细解释了 Vue.js 中数据绑定机制的用法。

第五章《Vuex-管理应用程序中的状态》包含了对 Vuex 的详细介绍,Vuex 是 Vue.js 的状态管理系统,并解释了如何在应用程序中使用它以实现良好的可维护架构。

第六章,“插件-用自己的砖头建造你的房子”,展示了如何在 Vue 应用程序中使用插件,并解释了如何在应用程序中使用现有插件,并解释了如何构建我们自己的插件然后使用它。

第七章,“测试-测试我们到目前为止所做的事情的时间!”,包含了 Vue 应用程序中可以使用的测试技术的介绍,以将它们带到所需的质量水平。我们通过展示如何编写单元测试以及如何为本书中的应用程序开发端到端测试来解决这个问题。

第八章,“部署-是时候上线了!”,展示了如何将您的 Vue 应用程序带到世界上,并通过持续集成工具保证其质量。它解释了如何将 GitHub 存储库连接到 Travis 持续集成系统和 Heroku 云部署平台。

第九章,“接下来是什么”,总结了到目前为止所做的一切,并留给读者后续步骤。

附录,“练习解决方案”,提供了前三章练习的解决方案。

这本书需要什么

这本书的要求如下:

  • 带有互联网连接的计算机

  • 文本编辑器/集成开发环境

  • Node.js

这本书适合谁

这本书适用于 Web 开发人员或想要成为 Web 开发人员的人。无论您刚开始使用 Web 技术还是已经是 Web 技术浩瀚海洋中框架和语言的大师,这本书可能会在响应式 Web 应用程序的世界中向您展示一些新东西。如果您是 Vue 开发人员并且已经使用过 Vue 1.0,这本书可能是您迁移到 Vue 2.0 的有用指南,因为本书的所有示例都基于 Vue 2.0。即使您已经在使用 Vue 2.0,这本书也可能是一个很好的练习,从头开始构建一个应用程序,应用所有 Vue 和软件工程概念,并将其推向部署阶段。

至少需要一些技术背景。如果你已经能够用 JavaScript 编写代码,那将是一个巨大的优势。

惯例

在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是这些样式的一些示例以及它们的含义解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名都会显示如下:“您的插件必须提供install方法。”

代码块设置如下:

export default {
  components: {
    ShoppingListComponent,
    ShoppingListTitleComponent
  },
  computed: mapGetters({
    shoppinglists: 'getLists'
  })
}

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

export default {
  components: {
    ShoppingListComponent,
    ShoppingListTitleComponent
  },
  computed: mapGetters({
    shoppinglists: 'getLists'
  }),
  **methods: mapActions(['populateShoppingLists']),**
  store,
  **mounted () {**
**    this.populateShoppingLists()**
**  }**
}

任何命令行输入或输出都会按照以下方式书写:

**cd shopping-list**
**npm install vue-resource --save-dev**

新术语重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会以这种方式出现在文本中:“勾选开发者模式复选框”。

注意

警告或重要说明会出现在这样的框中。

提示

提示和技巧会以这种方式出现。

第一章:使用 Vue.js 去购物

"Vue.js 是一个用于构建惊人的 Web 应用程序的 JavaScript 框架。Vue.js 是一个用于创建 Web 界面的 JavaScript 库。Vue.js 是一种利用 MVVM 架构的工具。"

简化的 JavaScript 术语建议 Vue.js 是一个基于底层数据模型创建用户界面(视图)的 JavaScript 库(jargon.js.org/_glossary/VUEJS.md)。

官方的 Vue.js 网站(vuejs.org/)在几个月前表示,Vue.js 是用于现代 Web 界面的反应式组件。

使用 Vue.js 去购物

现在它说明了 Vue.js 是一个渐进式的 JavaScript 框架:

使用 Vue.js 去购物

那么 Vue.js 到底是什么?框架?工具?库?它应该用于构建全栈 Web 应用程序,还是仅用于添加一些特殊功能?我应该从我喜欢的框架转到它吗?如果是的话,为什么?我可以在我的项目中同时使用它和其他工具吗?它可能带来什么优势?

在本章中,我们将尝试找到所有这些问题的答案。我们将稍微涉及 Vue.js,并在一些小而简单的示例中使用它。

更具体地说,我们将做以下事情:

  • 了解 Vue.js 是什么,它的重要部分和历史

  • 了解哪些项目使用了 Vue.js

  • 使用 Vue.js 构建一个简单的购物清单,并将其实现与相同应用程序的 jQuery 实现进行比较

  • 使用 Vue.js 构建一个简单的番茄工作法计时器

  • 享受一个小而简单的练习

流行词

在本书中将会有很多流行词、缩写和其他时髦的字母组合。请不要害怕它们。我可以告诉你更多,但是,对于使用 Vue.js 或任何其他框架需要做的大部分事情,你不需要全部都牢记在心!但是,无论如何,让我们把词汇表放在这里,这样你在书的任何地方都会对术语感到困惑,你可以回到这里看一看:

  • 应用状态:这是应用程序的全局集中状态。当应用程序启动时,此状态中的数据被初始化。任何应用程序组件都可以访问此数据;但是,它们不能轻易地更改它。状态的每个项目都有一个附加的变异,可以在应用程序组件内发生的特殊事件上分派。

  • Bootstrap:这是一个项目,提供了一组样式和 JavaScript 工具,用于开发响应式和美观的应用程序,而无需过多考虑 CSS。

  • 内容分发网络(CDN):这是一个特殊的服务器,其目的是以高可用性和高性能向用户传递数据。开发框架的人和公司喜欢通过 CDN 分发它们,因为它们只需在安装说明中指出 CDN 的 URL。Vue.js 托管在 npmcdn(npmcdn.com/),这是一个可靠的全球网络,用于发布到 npm 的内容。

  • 组件:这些是应用程序的部分,具有自己的数据和视图,可以在整个应用程序中重复使用,就像建造房子的砖块一样。

  • 层叠样式表(CSS):这是一组样式,应用于 HTML 文档,使其变得美观漂亮。

  • 声明式视图:这些是提供了一种直接数据绑定的视图,可以在普通的 JavaScript 数据模型和表示之间进行绑定。

  • 指令:这是 Vue.js 中的特殊 HTML 元素属性,允许以不同的方式进行数据绑定。

  • 文档对象模型(DOM):这是一种表示标记语言节点的约定,例如 HTML、XML 和 XHTML。文档的节点被组织成 DOM 树。当有人说与 DOM 交互时,这只是他们花哨地说与 HTML 元素交互。

  • npm:这是 JavaScript 的包管理器,允许搜索、安装和管理 JavaScript 包。

  • Markdown:这是一种人性化的语法,允许网络作者编写文本而不必担心样式和 HTML 标记。Markdown 文件的扩展名为.md

  • 模型视图视图模型(MVVM):这是一种架构模式,其核心是视图模型,充当视图和数据模型之间的桥梁,允许它们之间的数据流动。

  • 模型视图控制器(MVC):这是一种架构模式。它允许将视图与模型分离,以及信息从视图流向模型,反之亦然。

  • 单向数据绑定:这是一种数据绑定类型,其中数据模型中的更改会自动传播到视图层,但反之则不会。

  • 快速原型制作:在 Web 中,这是一种轻松快速地构建用户界面模型的技术,包括一些基本的用户交互。

  • 响应性:在 Web 中,这实际上是数据的任何更改立即传播到视图层。

  • 双向数据绑定:这是一种数据绑定类型,其中数据模型的更改会自动传播到视图层,而视图层中发生的更改会立即反映在数据模型中。

  • 用户界面(UI):这是一组视觉组件,允许用户与应用程序进行交互。

  • Vuex:这是 Vue 应用程序的架构,允许简单地管理应用程序状态。

Vue.js 历史

当 Vue.js 的创始人 Evan You(evanyou.me/)在 Google 创意实验室的一个项目上工作时,他们需要快速原型制作一个相当大的 UI 界面。编写大量重复的 HTML 显然是耗时和耗资源的,这就是为什么 Evan 开始寻找已经存在的工具来实现这个目的。令他惊讶的是,他发现没有工具、库或框架能够完全符合快速原型制作的目的!那时,Angular 被广泛使用,React.js 刚刚开始,Backbone.js 等框架被用于具有 MVC 架构的大型应用程序。对于需要非常灵活和轻量级的快速 UI 原型制作的项目来说,这些复杂的框架都不太合适。

当你意识到某个酷炫的东西不存在,而你又能够创造它时——就去做吧

注意

Vue.js 诞生于快速原型制作工具。现在它可以用来构建复杂可扩展的响应式 Web 应用程序。

这就是 Evan 所做的。这就是他想到创建一个库的想法,通过提供一种简单灵活的响应式数据绑定和可重用组件的方式来帮助快速原型制作。

像每个优秀的库一样,Vue.js 一直在不断成长和发展,因此提供了比最初承诺的更多功能。目前,它提供了一种简单的附加和创建插件、编写和使用混合物的方法,以及总体添加自定义行为。Vue 可以以如此灵活的方式使用,并且对应用程序结构没有明确的意见,以至于它绝对可以被视为一个能够支持端到端构建复杂 Web 应用程序的框架。

关于 Vue.js 最重要的一点

Vue.js 允许你简单地将你的数据模型绑定到表示层。它还允许你在整个应用程序中轻松重用组件。

你不需要创建特殊的模型或集合,并在其中注册事件对象。你不需要遵循某种特殊的语法。你不需要安装任何无休止的依赖。

你的模型是普通的 JavaScript 对象。它们被绑定到你在视图中想要的任何东西(文本、输入文本、类、属性等),它就能正常工作。

你可以简单地将vue.js文件添加到你的项目中并使用它。或者,你可以使用vue-cli与 Webpack 和 Browserify 系列,它不仅可以快速启动整个项目,还支持热重载并提供开发者工具。

你可以将视图层与样式和 JavaScript 逻辑分开,也可以将它们放在同一个 Vue 文件中,并在同一个地方构建你的组件结构和逻辑。所有现代和常用的 IDE 都支持插件。

你可以使用任何预处理器,并且你可以使用 ES2015。你可以将它与你一直在开发的喜爱框架一起使用,或者你可以单独使用它。你可以仅仅用它来添加一些小功能,或者你可以使用整个 Vue 生态系统来构建复杂的应用程序。

如果你想要比较它与其他框架,比如 Angular 或 React,那么请访问vuejs.org/guide/comparison.html

如果你想了解关于 Vue.js 的所有惊人之处,那么欢迎访问github.com/vuejs/awesome-vue

我们去购物吧!

我不知道为什么,但我能感觉到你的周末即将到来,你开始考虑去购物买下周所需的杂货。除非你是一个能够在脑海中维护整个清单的天才,或者你是一个不需要那么多的谦逊的人,你可能在去购物前会列一个购物清单。也许你甚至会使用一些应用程序来帮助。现在,我问你:为什么不使用你自己的应用程序呢?你对创建和设计它有什么感觉?让我们做吧!让我们创建我们自己的购物清单应用程序。让我们从创建一个快速原型开始。这是一个非常简单的任务——为购物清单构建一个交互式原型。

它应该显示列表并允许我们添加和删除项目。实际上,这与待办事项列表非常相似。让我们开始使用经典的 HTML + CSS + JS + jQuery 方法来做这件事。我们还将使用 Bootstrap 框架(getbootstrap.com/)来使事情看起来更美观,而无需编写大量的 CSS 代码。(是的,因为我们的书不是关于 CSS,因为使用 Bootstrap 制作东西是如此地简单!)

使用 jQuery 实现购物清单

可能,您的代码最终看起来会像以下内容:

以下是 HTML 代码:

<div class="container"> 
  <h2>My Shopping List</h2> 
  <div class="input-group"> 
    <input placeholder="add shopping list item"        
      type="text" class="js-new-item form-control"> 
    <span class="input-group-btn"> 
      <button @click="addItem" class="js-add btn btn-default"          
        type="button">Add!</button> 
    </span> 
  </div> 
  <ul> 
    <li> 
      <div class="checkbox"> 
        <label> 
          <input class="js-item" name="list"              
            type="checkbox"> Carrot 
        </label> 
      </div> 
    </li> 
    <li> 
      <div class="checkbox"> 
        <label> 
          <input class="js-item" name="list" type="checkbox"> Book 
        </label> 
      </div> 
    </li> 
    <li class="removed"> 
      <div class="checkbox"> 
        <label> 
          <input class="js-item" name="list" type="checkbox"              
            checked> Gift for aunt's birthday 
        </label> 
      </div> 
    </li> 
  </ul> 
</div> 

以下是 CSS 代码:

.container { 
  width: 40%; 
  margin: 20px auto 0px auto; 
} 

.removed { 
  color: gray; 
} 

.removed label { 
  text-decoration: line-through; 
} 

ul li { 
  list-style-type: none; 
} 

以下是 JavaScript/jQuery 代码:

$(document).ready(function () { 
  /** 
   * Add button click handler 
   */ 
  function onAdd() { 
    var $ul, li, $li, $label, $div, value; 

    value = $('.js-new-item').val(); 
    //validate against empty values 
    if (value === '') { 
      return; 
    } 
    $ul = $('ul'); 
    $li = $('<li>').appendTo($ul); 
    $div = $('<div>') 
        .addClass('checkbox') 
        .appendTo($li); 
    $label = $('<label>').appendTo($div); 
    $('<input>') 
        .attr('type', 'checkbox') 
        .addClass('item') 
        .attr('name', 'list') 
        .click(toggleRemoved) 
        .appendTo($label); 
    $label 
        .append(value); 
    $('.js-new-item').val(''); 
  } 

  /** 
   * Checkbox click handler - 
   * toggles class removed on li parent element 
   * @param ev 
   */ 
  function toggleRemoved(ev) { 
    var $el; 

    $el = $(ev.currentTarget); 
    $el.closest('li').toggleClass('removed'); 
  } 

  $('.js-add').click(onAdd); 
  $('.js-item').click(toggleRemoved); 
}); 

提示

下载示例代码 下载代码包的详细步骤在本书的前言中有提到。该书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/Learning-Vue.js-2。我们还有来自丰富书籍和视频目录的其他代码包,可在github.com/PacktPublishing/上找到。去看看吧!

如果您在浏览器中打开页面,您可能会看到类似以下内容:

使用 jQuery 实现购物清单

使用 HTML + CSS + jQuery 方法实现购物清单

请查看jsfiddle.net/chudaol/u5pcnLw9/2/上的 JSFiddle。

正如你所看到的,这是一个非常基本的 HTML 代码片段,其中包含一个无序元素列表,每个元素都用复选框和文本呈现 - 一个用于用户文本和Add!按钮的输入。每次单击Add!按钮时,文本输入的内容都会被转换为列表条目并附加到列表中。当单击任何项目的复选框时,条目的状态会从to buy(未选中)切换到bought(已选中)。

让我们还添加一个功能,允许我们更改列表的标题(如果我们最终在应用程序中实现多个购物清单,这可能会很有用)。

因此,我们将最终得到一些额外的标记和一些更多的 jQuery 事件监听器和处理程序:

<div class="container"> 
  <h2>My Shopping List</h2> 
  <!-- ... --> 
  <div class="footer"> 
    <hr/> 
    <em>Change the title of your shopping list here</em> 
    <input class="js-change-title" type="text"
      value="My Shopping List"/> 
  </div> 
</div> 

//And javascript code: 
function onChangeTitle() { 
  $('h2').text($('.js-change-title').val()); 
} 
$('.js-change-title').keyup(onChangeTitle); 

jsfiddle.net/chudaol/47u38fvh/3/上查看 JSFiddle。

使用 Vue.js 实现购物清单

这是一个非常简单的例子。让我们尝试使用 Vue.js 逐步实现它。有很多种方法可以将vue.js包含到您的项目中,但在本章中,我们将通过添加来自CDN的 JavaScript Vue 文件来包含它:

<script  src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js">  </script> 

所以,让我们从渲染元素列表开始。

创建 HTML 文件并添加以下标记:

<div id="app" class="container"> 
  <h2>{{ title }}</h2> 
  <ul> 
    <li>{{ items[0] }}</li> 
    <li>{{ items[1] }}</li> 
  </ul> 
</div> 

现在添加以下 JavaScript 代码:

var data = { 
  items: ['Bananas', 'Apples'], 
  title: 'My Shopping List' 
}; 

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

在浏览器中打开它。您会看到列表已经渲染出来了:

使用 Vue.js 实现购物清单

使用 Vue.js 实现的购物清单

让我们分析一下这个例子。Vue 应用程序代码以新的Vue关键字开始。我们如何将标记片段绑定到应用程序数据?我们将 DOM 元素传递给Vue实例,该元素必须与其绑定。页面中的任何其他标记都不会受到影响,也不会识别 Vue 的魔法。

正如你所看到的,我们的标记被包裹在#app元素中,并作为Vue选项映射中的第一个参数传递。data参数包含在标记中使用双大括号({{}})的对象。如果您熟悉模板预处理器(例如 handlebars),您可能会发现这种注释非常容易理解;有关更多信息,请访问handlebarsjs.com/

那又怎样?—你可能会惊叹。你要教我什么?如何使用模板预处理器?非常感谢,但我宁愿喝点啤酒,看看足球。

停下来,亲爱的读者,别走,拿起你的啤酒,让我们继续我们的例子。你会发现这将是非常有趣的!

使用开发者工具分析数据绑定

让我们看看数据绑定的实际操作。打开浏览器的开发者工具,找到您的 JavaScript 代码,并在脚本的开头添加一个断点。现在分析一下 Vue 应用程序初始化之前和之后数据对象的样子。你会发现,它变化很大。现在data对象已经准备好进行反应式数据绑定了:

使用开发者工具分析数据绑定

Vue 对象初始化之前和之后的数据对象

现在,如果我们从开发者工具控制台更改data对象的title属性(我们可以这样做,因为我们的data是一个全局对象),它将自动反映在页面上的标题中:

使用开发者工具分析数据绑定

数据绑定:更改对象属性会立即影响视图

通过双向绑定将用户输入带入数据

因此,在我们的示例中,我们能够将数据从普通的 JavaScript 数据模型带到页面上。我们为它提供了一种从应用程序代码到页面的飞行。你不觉得如果我们能为我们的数据提供双向飞行会很好吗?

现在让我们看看如何实现双向数据绑定,以及如何从页面更改data属性的值。

复制标题的 HTML 标记,更改第一个 jQuery 示例中的输入,并向input元素添加属性v-model="title"

提示

您已经听说过 Vue.js 中的指令了吗?恭喜,您刚刚使用了一个!实际上,v-model属性是 Vue.js 的一个指令,提供了双向数据绑定。您可以在官方 Vue 页面上阅读更多关于它的信息:vuejs.org/api/#v-model

现在,我们的购物清单应用程序代码的 HTML 代码如下:

<div id="app" class="container"> 
  <h2>{{ title }}</h2> 
  <ul> 
    <li>{{ items[0] }}</li> 
    <li>{{ items[1] }}</li> 
  </ul> 
  <div class="footer"> 
    <hr/> 
    <em>Change the title of your shopping list here</em> 
    <input v-model="title"/> 
  </div> 
</div> 

就是这样!

现在刷新页面并修改输入。您会看到标题在您输入时自动更新:

通过双向绑定将用户输入带入数据

数据绑定:更改绑定到模型属性的文本会立即影响绑定到同一属性的文本。

因此,一切都很好;然而,这个例子只是抓取了两个项目元素,并将它们呈现为列表项。我们希望它能够独立于列表大小呈现项目列表。

使用 v-for 指令渲染项目列表

因此,我们需要一些机制来遍历items数组,并在我们的<ul>元素中呈现每个项目。

幸运的是,Vue.js 为我们提供了一个很好的指令,用于遍历迭代的 JavaScript 数据结构。它被称为v-for。我们将在列表项<li>元素中使用它。修改列表的标记,使其看起来像下面这样:

  <ul> 
    <li v-for="item in items">{{ item }}</li> 
  </ul> 

注意

在本书中,您将学习到其他很好的指令,如v-ifv-elsev-showv-onv-bind等等,所以请继续阅读。

刷新页面并查看。页面保持不变。现在,尝试从开发者工具控制台将项目推入items数组中。也尝试弹出它们。您会不会惊讶地看到items数组的操作立即反映在页面上:

使用 v-for 指令渲染项目列表

数据绑定:更改数组会立即影响基于它的列表

所以,现在我们有一个项目列表,只需一行标记就可以在页面上呈现出来。然而,我们仍然需要这些项目有一个复选框,允许我们在需要时勾选已购买的项目或取消勾选它们。

勾选和取消勾选购物清单项目

为了实现这种行为,让我们稍微修改我们的items数组,将我们的字符串项目更改为具有两个属性textchecked(以反映状态)的对象,并修改标记以为每个项目添加复选框。

因此,我们的数据声明的 JavaScript 代码将如下所示:

var data = { 
  items: [{ text: 'Bananas', checked: true },    
          { text: 'Apples',  checked: false }], 
  title: 'My Shopping List', 
  newItem: '' 
}; 

我们的列表标记将如下所示:

<ul> 
  <li v-for="item in items" v-bind:class="{ 'removed':      
    item.checked }"> 
    <div class="checkbox"> 
      <label> 
        <input type="checkbox" v-model="item.checked"> {{            
          item.text }} 
      </label> 
    </div> 
  </li> 
</ul>  

刷新页面并检查items复选框的checked属性,以及每个列表项<li>的移除类,是否与项目的checked布尔状态绑定。尝试点击复选框,看看会发生什么。仅仅用两个指令就能够传播项目的状态并改变相应的<li>HTML 元素的类,是不是很棒?

使用 v-on 指令添加新的购物清单项目

所以现在我们只需要对我们的代码进行一点小的修改,就能够真正地添加购物清单项目了。为了实现这一点,我们将在我们的数据中再添加一个对象,称之为newItem。我们还将添加一个小方法,将新项目推送到items数组中。我们将在标记页中使用v:on指令调用这个方法,该指令用于 HTML 输入元素和用于单击以添加新项目的按钮。

因此,我们的 JavaScript 代码将如下所示:

var data = { 
  items: [{ text: 'Bananas', checked: true },    
          { text: 'Apples', checked: false }], 
  title: 'My Shopping List', 
  **newItem: ''** 
}; 
new Vue({ 
  el: '#app', 
  data: data, 
  **methods: { 
    addItem: function () { 
      var text; 

      text = this.newItem.trim(); 
      if (text) { 
        this.items.push({ 
          text: text, 
          checked: false 
        }); 
        this.newItem = ''; 
      } 
    }** 
  } 
}); 

我们在data对象中添加了一个名为newItem的新属性。然后我们在 Vue 初始化options对象中添加了一个名为methods的新部分,并在该部分中添加了addItem方法。所有的数据属性都可以通过this关键字在methods部分中访问。因此,在这个方法中,我们只需获取this.newItem并将其推送到this.items数组中。现在我们必须将对这个方法的调用绑定到某个用户操作上。正如已经提到的,我们将使用v-on指令,并将其应用于新项目输入的enter键盘事件和Add!按钮的单击事件。

在我们的项目列表之前添加以下标记:

<div class="input-group"> 
  <input v-model="newItem" **v-on:keyup.enter="addItem"**      
    placeholder="add shopping list item" type="text" class="form-      
    control"> 
  <span class="input-group-btn"> 
    <button **v-on:click="addItem"** class="btn btn-default"            
      type="button">Add!</button> 
  </span> 
</div> 

注意

v-on指令将事件侦听器附加到元素。快捷方式是@符号。因此,你可以用@keyup="addItem"来代替v-on:keyup="addItem"。你可以在官方文档网站上阅读更多关于v-on指令的信息,网址是vuejs.org/api/#v-on

让我们完成。整个代码现在看起来像下面这样:

这是 HTML 代码:

<div id="app" class="container"> 
  <h2>{{ title }}</h2> 
  <div class="input-group"> 
    <input v-model="newItem" @keyup.enter="addItem"        
      placeholder="add shopping list item" type="text" 
      class="form-control"> 
  <span class="input-group-btn"> 
    <button @click="addItem" class="btn btn-default"        
      type="button">Add!</button> 
  </span> 
  </div> 
  <ul> 
    <li v-for="item in items" :class="{ 'removed': item.checked      
      }"> 
      <div class="checkbox"> 
        <label> 
          <input type="checkbox" v-model="item.checked"> {{              
            item.text }} 
        </label> 
      </div>     
    </li> 
  </ul> 
  <div class="footer hidden"> 
    <hr/> 
    <em>Change the title of your shopping list here</em> 
    <input v-model="title"/> 
  </div> 
</div> 

这是 JavaScript 代码:

var data = { 
  items: [{ text: 'Bananas', checked: true },    
          { text: 'Apples', checked: false }], 
  title: 'My Shopping List', 
  newItem: '' 
}; 

new Vue({ 
  el: '#app', 
  data: data, 
  methods: { 
    addItem: function () { 
      var text; 

      text = this.newItem.trim(); 
      if (text) { 
        this.items.push({ 
          text: text, 
          checked: false 
        }); 
        this.newItem = ''; 
      } 
    } 
  } 
}); 

这是 JSFiddle 的链接:jsfiddle.net/chudaol/vxfkxjzk/3/

在现有项目中使用 Vue.js

我现在可以感觉到你已经看到了将模型的属性绑定到表示层有多容易,你已经开始考虑如何在现有项目中使用它。但是然后你会想:天啊,不,我需要安装一些东西,运行npm install,改变项目的结构,添加指令,改变代码。

在这里我可以告诉你:不!不需要安装,不需要 npm,只需获取vue.js文件,将其插入到你的 HTML 页面中,然后使用它。就这样,不需要改变结构,不需要架构决策,也不需要讨论。只需使用它。我将向你展示我们在 EdEra(www.ed-era.com)中如何在 GitBook 章节的末尾包含一个小的“自我检查”功能。

EdEra 是一个总部位于乌克兰的在线教育项目,其目标是将整个教育系统转变为现代、在线、互动和有趣的东西。实际上,我是这个年轻的美好项目的联合创始人兼首席技术官,负责整个技术部分。因此,在 EdEra,我们有一些建立在开放的 EdX 平台(open.edx.org/)之上的在线课程,以及一些建立在伟大的 GitBook 框架(www.gitbook.org)之上的互动教育书籍。基本上,GitBook 是一个基于 Node.js 技术栈的平台。它允许具有对 Markdown 语言和基本 Git 命令的基本知识的人编写书籍并将它们托管在 GitBook 服务器上。EdEra 的书籍可以在ed-era.com/books找到(注意,它们都是乌克兰语)。

让我们看看我们在书中使用 Vue.js 做了什么。

在某个时候,我决定在教授英语的书籍中关于人称代词的章节末尾包含一个小测验。因此,我包含了vue.js JavaScript 文件,编辑了相应的.md文件,并包含了以下 HTML 代码:

<div id="pronouns"> 
    <p><strong>Check yourself :)</strong></p> 
    <textarea class="textarea" v-model="text" v-      
      on:keyup="checkText"> 
        {{ text }} 
    </textarea><i  v-bind:class="{ 'correct': correct,      
      'incorrect': !correct }"></i> 
</div> 

然后我添加了一个自定义的 JavaScript 文件,其中包含了以下代码:

$(document).ready(function() { 
  var initialText, correctText; 

  initialText = 'Me is sad because he is more clever than I.'; 
  correctText = 'I am sad because he is more clever than me.'; 

  new Vue({ 
    el: '#pronouns', 
    data: { 
      text: initialText, 
      correct: false 
    }, 
    methods: { 
      checkText: function () { 
        var text; 
        text = this.text.trim(); 
        this.correct = text === correctText; 
      } 
    } 
  }); 
}); 

注意

你可以在这个 GitHub 页面上查看这段代码:github.com/chudaol/ed-era-book-english。这是一个用 markdown 编写并插入 HTML 的页面的代码:github.com/chudaol/ed-era-book-english/blob/master/2/osobovi_zaimenniki.md。这是一个 JavaScript 代码:github.com/chudaol/ed-era-book-english/blob/master/custom/js/quiz-vue.js。你甚至可以克隆这个存储库,并使用gitbook-cli在本地尝试(github.com/GitbookIO/gitbook/blob/master/docs/setup.md)。

让我们来看看这段代码。你可能已经发现了你已经看过甚至尝试过的部分:

  • data对象包含两个属性:

  • 字符串属性 text

  • 布尔属性 correct

  • checkText方法只是获取text属性,将其与正确的文本进行比较,并将值分配给正确的值

  • v-on指令在键盘松开时调用checkText方法

  • v-bind指令将类correct绑定到correct属性

这是我的 IDE 中的代码样子:

在现有项目中使用 Vue.js

在驱动项目中使用 Vue

接下来是在浏览器中的样子:

在现有项目中使用 Vue.js

Vue.js 在 GitBook 页面内的实际应用

在现有项目中使用 Vue.js

Vue.js 在 GitBook 页面内的实际应用

english.ed-era.com/2/osobovi_zaimenniki.html查看它。

很棒,对吧?非常简单,非常响应!

Vue.js 2.0!

在撰写本文时,Vue.js 2.0 已经宣布(vuejs.org/2016/04/27/announcing-2.0/)。请查看相关链接:

Vue.js 的第二个版本与其前身相比有一些显著的区别,从处理数据绑定的方式开始,到其 API。它使用轻量级虚拟 DOM 实现进行渲染,支持服务器端渲染,并且更快、更精简。

在撰写本文时,Vue 2.0 处于早期 alpha 阶段。不过不用担心。本书中涵盖的所有示例都基于 Vue 2.0 的最新稳定版本,并且与两个版本都完全兼容。

使用 Vue.js 的项目

也许,此时你想知道有哪些项目是建立在 Vue.js 之上,或者将其作为其代码库的一部分。有很多不错的开源、实验性和企业项目在使用它。这些项目的完整和不断更新的列表可以在github.com/vuejs/awesome-vue#projects-using-vuejs找到。

让我们来看看其中一些。

Grammarly

Grammarly(www.grammarly.com/)是一个帮助您正确书写英语的服务。它有几个应用程序,其中一个是一个简单的 Chrome 扩展,只是检查您填写的任何文本输入。另一个是一个在线编辑器,您可以用它来检查大块的文本。这个编辑器是使用 Vue.js 构建的!以下是 Grammarly 在线编辑器中正在编辑的文本的截图:

Grammarly

Grammarly:一个建立在 Vue.js 之上的项目

Optimizely

Optimizely(www.optimizely.com/)是一个帮助您测试、优化和个性化您的网站的服务。我曾使用 Packt 网站创建了一个 Optimizely 实验,并在这个资源中查看了 Vue.js 的实际效果。它看起来像下面这样:

Optimizely

Optimizely:一个建立在 Vue.js 之上的项目

鼠标悬停可以打开上下文菜单,允许对页面数据进行不同的操作,包括最简单的文本编辑。让我们试试这个:

Optimizely

使用 Optimizely 并观看 Vue.js 的实际操作

文本框已打开。当我在其中输入时,标题中的文本会被动地更改。我们使用 Vue.js 看到并实现了它:

Optimizely

使用 Optimizely 并观看 Vue.js 的实际操作

FilterBlend

FilterBlend(github.com/ilyashubin/FilterBlend)是一个开源的 CSS 背景混合模式和滤镜属性的游乐场。

您可以加载您的图像并将混合与滤镜相结合。

如果您想尝试 FilterBlend,您可以在本地安装它:

  1. 克隆存储库:
**git clone https://github.com/ilyashubin/FilterBlend.git**

  1. 进入FilterBlend目录:
**cd FilterBlend**

  1. 安装依赖项:
**npm install**

  1. 运行项目:
**gulp**

localhost:8000上打开您的浏览器并进行操作。您会发现,一旦您在右侧菜单中更改了某些内容,它会立即传播到左侧的图像中。所有这些功能都是使用 Vue.js 实现的。在 GitHub 上查看代码。

FilterBlend

FilterBlend:一个基于 Vue.js 构建的项目

PushSilver

PushSilver(pushsilver.com)是一个为忙碌的人创建简单发票的良好而简单的服务。它允许创建发票,向客户发送和重新发送它们,并跟踪它们。它是由一位进行自由咨询的开发人员创建的,他厌倦了每次为每个小项目创建发票。这个工具运行良好,它是使用 Vue.js 构建的:

PushSilver

PushSilver:基于 Vue.js 构建的发票管理应用程序

PushSilver

PushSilver:基于 Vue.js 构建的发票管理应用程序

书籍路线图

这本书,像大多数技术书籍一样,是以这样一种方式组织的,您不需要从头到尾阅读它。您可以选择您最感兴趣的部分并跳过其余部分。

本书的组织如下:

  • 如果您正在阅读本书,就无需说明第一章中正在发生什么。

  • 第二章,“基础知识-安装和使用”,是非常理论性的,将解释 Vue.js 及其主要部分背后发生了什么。因此,如果你不喜欢理论,想要动手编码,可以跳过这部分。在这一部分,我们还将介绍安装和设置过程。

  • 从第三章到第八章,我们将在构建应用程序的同时探索 Vue.js 的主要特性。

  • 在第三章,“组件-理解和使用”,我们将介绍 Vue 组件,并将这些知识应用到我们的应用程序中。

  • 在第四章,“反应性-将数据绑定到您的应用程序”,我们将使用 Vue 提供的所有数据绑定机制。

  • 在第五章,“Vuex-管理应用程序中的状态”,我们将介绍 Vuex 状态管理系统,并解释如何在我们的应用程序中使用它。

  • 在第六章,“插件-用自己的砖建造你的房子”,我们将学习如何为 Vue 应用程序创建和使用插件,以丰富其功能。

  • 在第七章,“测试-是时候测试我们到目前为止所做的了!”,我们将涵盖并探索 Vue.js 的自定义指令,并在我们的应用程序中创建一些。

  • 在第八章,“部署-是时候上线了!”,我们将学习如何测试和部署使用 Vue.js 编写的 JavaScript 应用程序。

  • 在第九章,“接下来是什么?”,我们将总结我们所学到的内容,并看看接下来我们可以做些什么。

让我们管理好时间!

此时此刻,我已经知道你对这本书非常热情,想要一口气读到底。但这是不对的。我们应该管理好我们的时间,给自己一些工作时间和休息时间。让我们创建一个小应用程序,实现番茄工作法定时器,以帮助我们管理工作时间。

注意

Pomodoro技术是一种以厨房番茄计时器命名的时间管理技术(事实上,Pomodoro 在意大利语中意味着番茄)。这种技术包括将工作时间分解为短暂休息间隔。在官方网站上了解更多关于 Pomodoro 技术的信息:pomodorotechnique.com/

因此,我们的目标非常简单。我们只需要创建一个非常简单的时间计数器,直到工作间隔结束,然后重新开始并递减直到休息时间结束,依此类推。

让我们开始吧!

我们将引入两个 Vue 数据变量,minutesecond,它们将显示在我们的页面上。每秒钟的主要方法将递减second;当second变为0时,它将递减minute;当minutesecond变量都变为0时,应用程序应在工作和休息间隔之间切换:

我们的 JavaScript 代码将如下所示:

const POMODORO_STATES = { 
  WORK: 'work', 
  REST: 'rest' 
}; 
const WORKING_TIME_LENGTH_IN_MINUTES = 25; 
const RESTING_TIME_LENGTH_IN_MINUTES = 5; 

new Vue({ 
  el: '#app', 
  data: { 
    minute: WORKING_TIME_LENGTH_IN_MINUTES, 
    second: 0, 
    pomodoroState: POMODORO_STATES.WORK, 
    timestamp: 0 
  }, 
  methods: { 
    start: function () { 
      this._tick(); 
      this.interval = setInterval(this._tick, 1000); 
    }, 
    _tick: function () { 
      //if second is not 0, just decrement second 
      if (**this.second** !== 0) { 
        **this.second**--; 
        return; 
      } 
      //if second is 0 and minute is not 0,        
      //decrement minute and set second to 59 
      if (**this.minute** !== 0) { 
        **this.minute**--; 
        **this.second** = 59; 
        return; 
      } 
      //if second is 0 and minute is 0,        
      //toggle working/resting intervals 
      this.pomodoroState = this.pomodoroState ===        
      POMODORO_STATES.WORK ? POMODORO_STATES.REST :        
      POMODORO_STATES.WORK; 
      if (this.pomodoroState === POMODORO_STATES.WORK) { 
        **this.minute** = WORKING_TIME_LENGTH_IN_MINUTES; 
      } else { 
        **this.minute** = RESTING_TIME_LENGTH_IN_MINUTES; 
      } 
    } 
  } 
}); 

在我们的 HTML 代码中,让我们为minutesecond创建两个占位符,并为我们的 Pomodoro 计时器创建一个开始按钮:

<div id="app" class="container"> 
  <h2> 
    <span>Pomodoro</span> 
    <button  **@click="start()"**> 
      <i class="glyphicon glyphicon-play"></i> 
    </button> 
  </h2> 
  <div class="well"> 
    <div class="pomodoro-timer"> 
      <span>**{{ minute }}**</span>:<span>{{ second }}</span> 
    </div> 
  </div> 
</div> 

再次,我们使用 Bootstrap 进行样式设置,因此我们的 Pomodoro 计时器看起来像下面这样:

让我们管理时间!

使用 Vue.js 构建的倒计时器

我们的 Pomodoro 很好,但它也有一些问题:

  • 首先,我们不知道正在切换的状态是哪个州。我们不知道我们是应该工作还是休息。让我们引入一个标题,每次 Pomodoro 状态改变时都会改变。

  • 另一个问题是分钟和秒数的显示不一致。例如,对于 24 分钟 5 秒,我们希望看到 24:05 而不是 24:5。让我们通过在应用程序数据中引入计算值并显示它们而不是普通值来解决这个问题。

  • 还有另一个问题是我们的开始按钮可以一遍又一遍地点击,这会在每次点击时创建一个计时器。尝试多次点击它,看看你的计时器会变得多么疯狂。让我们通过引入开始、暂停和停止按钮,将应用程序状态应用到它们,并根据状态禁用按钮来解决这个问题。

使用计算属性切换标题

让我们首先通过创建计算属性标题来解决第一个问题,并在我们的标记中使用它。

注意

计算属性data对象中的属性,它们允许我们避免在模板中添加额外的逻辑。您可以在官方文档网站上找到有关计算属性的更多信息:vuejs.org/guide/computed.html

在 Vue 的options对象中添加computed部分,并在那里添加title属性:

data: { 
  //... 
}, 
computed: { 
  title: function () { 
    return this.pomodoroState === POMODORO_STATES.WORK ? 'Work!' :      
    'Rest!' 
  } 
}, 
methods: { 
//... 

现在只需在标记中将以下属性用作普通的 Vue data属性:

  <h2> 
    <span>Pomodoro</span> 
    <!--!> 
  </h2> 
  **<h3>{{ title }}</h3>** 
  <div class="well"> 

看!现在我们有一个标题,每当 Pomodoro 状态被切换时都会更改:

使用计算属性切换标题

基于计时器状态自动更改标题

不错,是吧?

使用计算属性进行左填充时间值

现在让我们对minutesecond数字应用相同的逻辑进行左填充。在我们的computed部分中的data选项中添加两个计算属性,minsec,并应用简单的算法在左侧填充数字为0。当然,我们可以使用著名的 left-pad 项目(github.com/stevemao/left-pad),但为了保持简单并且不破坏整个互联网(www.theregister.co.uk/2016/03/23/npm_left_pad_chaos/),让我们应用自己的简单逻辑:

computed: { 
  title: function () { 
    return this.pomodoroState === POMODORO_STATES.WORK ? 'Work!' :      
    'Rest!' 
  }, 
  **min**: function () { 
    if (this.minute < 10) { 
      return '0' + this.minute; 
    } 

    return this.minute; 
  }, 
  **sec**: function () { 
    if (this.second < 10) { 
      return '0' + this.second; 
    } 

    return this.second; 
  } 
} 

并且在我们的 HTML 代码中使用这些属性代替minutesecond

   <div class="pomodoro-timer"> 
    <span>**{{ min }}**</span>:<span>{{ sec }}</span> 
   </div> 

刷新页面并检查我们的数字现在有多美:

使用计算属性进行左填充时间值

在 Vue.js 中使用计算属性进行左填充

使用开始、暂停和停止按钮保持状态

因此,为了解决第三个问题,让我们引入三种应用状态,startedpausedstopped,并且让我们有三种方法可以允许我们在这些状态之间进行排列。我们已经有了启动应用程序的方法,所以我们只需在那里添加逻辑来将状态更改为started。我们还添加了另外两种方法,pausestop,它们将暂停计时器并更改为相应的应用程序状态:

**const POMODORO_STATES = { 
  WORK: 'work', 
  REST: 'rest' 
}; 
const STATES = { 
  STARTED: 'started', 
  STOPPED: 'stopped', 
  PAUSED: 'paused' 
};** 
//<...> 
new Vue({ 
  el: '#app', 
  data: { 
    **state: STATES.STOPPED**, 
    //<...> 
  }, 
  //<...> 
  methods: { 
    start: function () { 
      **this.state = STATES.STARTED**; 
      this._tick(); 
      this.interval = setInterval(this._tick, 1000); 
    }, 
    **pause**: function () { 
      **this.state = STATES.PAUSED;** 
      clearInterval(this.interval); 
    }, 
    **stop**: function () { 
      **this.state = STATES.STOPPED;** 
      clearInterval(this.interval);  
      this.pomodoroState = POMODORO_STATES.WORK; 
      this.minute = WORKING_TIME_LENGTH_IN_MINUTES; 
      this.second = 0; 
    }, 
    //<...> 
  } 
}); 

然后,在我们的 HTML 代码中添加两个按钮,并添加调用相应方法的click监听器:

    <button **:disabled="state==='started'"**
**@click="start()"**> 
      <i class="glyphicon glyphicon-play"></i> 
    </button> 
    <button **:disabled="state!=='started'"       
      @click="pause()"**> 
      <i class="glyphicon glyphicon-pause"></i> 
    </button> 
    <button **:disabled="state!=='started' && state !== 'paused'"      
       @click="stop()"**> 
      <i class="glyphicon glyphicon-stop"></i> 
    </button> 

现在我们的应用程序看起来很好,并且允许我们启动、暂停和停止计时器:

使用开始、暂停和停止按钮保持状态

根据应用程序状态切换开始、停止和暂停按钮

在 JSFiddle 中查看整个代码的样子:jsfiddle.net/chudaol/b6vmtzq1/1/

经过这么多的工作和新术语和知识,你肯定值得拥有一只小猫!我也喜欢小猫,所以这里有一只来自thecatapi.com/的随机小猫:

使用开始、暂停和停止按钮保持状态

练习

在本章结束时,我想提出一个小练习。我们在前几章中构建的番茄钟定时器无疑非常棒,但仍然缺少一些不错的功能。它可以提供的一个非常好的功能是在休息时间显示来自thecatapi.com/的随机小猫。你能实现这个吗?当然可以!但请不要把休息时间和工作时间搞混了!我几乎可以肯定,如果你盯着小猫而不是工作,你的项目经理是不会太喜欢的。

这个练习的解决方案可以在附录中找到,练习解答

摘要

我非常高兴你已经达到了这一点,这意味着你已经知道了 Vue.js 是什么,如果有人问你它是一个工具、一个库还是一个框架,你肯定会找到答案。你还知道如何使用 Vue.js 启动应用程序,以及如何在已有项目中使用 Vue 的功能。你已经玩过一些用 Vue.js 编写的非常棒的项目,并且开始开发一些属于自己的项目!现在你不仅仅是去购物,现在你是用 Vue.js 创建的购物清单去购物!现在你不需要从厨房偷一个番茄定时器来用作番茄钟定时器了;你可以使用自己用 Vue.js 制作的数字番茄钟定时器。最后但同样重要的是,现在你也可以在 JavaScript 应用程序中插入随机小猫,同样使用 Vue.js。

在下一章中,我们将介绍 Vue 的幕后工作原理,以及它是如何工作的,以及它使用的架构模式。每个概念都将以示例来加以说明。然后我们将准备深入代码,改进我们的应用程序,将它们提升到令人惊叹的状态。

第二章:基础知识-安装和使用

在上一章中,我们对 Vue.js 有了一些了解。我们能够在两个不同的应用程序中使用它,这两个应用程序是从头开始创建的。我们学会了如何将 Vue.js 集成到已经存在的项目中。我们能够看到 Vue 的响应式数据绑定是如何运作的。

现在,你可能会问自己:它是如何工作的?在数据模型发生变化时,它是如何实现快速 UI 变化的行为?也许,你决定在你的项目中使用 Vue.js,并且现在想知道它是否遵循某种架构模式或范式,以便你应该在你的项目中采用它。在本章中,我们将探讨 Vue.js 框架的关键概念,以了解其所有幕后功能。此外,在本章中,我们将分析安装 Vue.js 的所有可能方式。我们还将为我们的应用程序创建一个骨架,我们将通过接下来的章节来开发和增强它。我们还将学习调试和测试我们应用程序的方法。

因此,在本章中,我们将学习:

  • MVVM 架构范式是什么,以及它如何应用于 Vue.js

  • 什么是声明式视图

  • Vue.js 如何探索定义的属性、getter 和 setter

  • Vue.js 中响应性和数据绑定的工作原理

  • 什么是脏检查、DOM 和虚拟 DOM

  • Vue.js 1.0 和 Vue.js 2.0 之间的主要区别

  • 可重用组件是什么

  • Vue.js 中插件、指令、自定义插件和自定义指令的工作原理

  • 如何安装、启动、运行和调试 Vue 应用程序

MVVM 架构模式

你还记得我们在第一章中如何创建Vue实例吗?我们通过调用new Vue({...})来实例化它。你还记得在选项中,我们传递了页面上应该绑定这个Vue实例的元素,以及包含我们想要绑定到我们视图的属性的data对象。data对象是我们的模型,而Vue实例绑定的 DOM 元素是我们的视图:

MVVM 架构模式

经典的视图-模型表示,其中 Vue 实例将一个绑定到另一个

与此同时,我们的Vue实例是帮助将我们的模型绑定到视图以及反之的东西。因此,我们的应用程序遵循模型-视图-视图模型MVVM)模式,其中Vue实例是视图模型:

MVVM 架构模式

模型-视图-视图模型模式的简化图表

我们的Model包含数据和一些业务逻辑,我们的View负责其表示。ViewModel处理数据绑定,确保在Model中更改的数据立即影响View层,反之亦然。

因此,我们的视图完全是数据驱动的。ViewModel负责控制数据流,使数据绑定对我们来说完全是声明性的。

DefineProperty、getter 和 setter

那么,一旦数据传递给Vue实例,会发生什么?Vue对其应用了哪些转换,使其自动绑定到 View 层?

让我们分析一下,如果我们有一个字符串,每次它改变时,我们想对某个 DOM 元素应用一些转换,我们会怎么做。我们会如何应用字符串更改的监听函数?我们会将它附加到什么上?没有var stringVar='hello';stringVar.onChange(doSomething)这样的东西。

所以我们可能会将字符串的值设置和获取包装在某种函数中,该函数会做一些事情,例如每次字符串更新时更新 DOM。你会如何实现它?当你考虑这个问题时,我会准备一个有趣的快速演示。

打开您的购物清单应用程序的开发者工具。让我们写一点代码。创建一个obj变量和另一个text变量:

var obj = {}; 
var text = ''; 

让我们将 DOM 元素h2存储在一个变量中:

var h2 = document.getElementsByTagName('h2')[0]; 

如果我们将text分配给obj.text属性,如何才能在每次更改此属性时,h2innerHTML也会相应更改?

让我们使用Object.defineProperty方法(developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)。

该方法允许创建 getter 和 setter 函数,从而指定在访问或更改属性时必须发生什么:

Object.defineProperty(obj, 'text', { 
  get: function () { 
    return text; 
  }, 
  set: function (newVal) { 
    text = newVal;  
    **h2.innerHTML = text;** 
  } 
}); 

现在尝试从控制台更改obj.text属性。看看标题:

DefineProperty, getter 和 setter

每次属性更改时都会调用对象.defineProperty 的 set 方法

这个确切的机制被 Vue.js 使用过。一旦数据被传递给Vue实例,它的所有属性都会通过Object.defineProperty方法,为它们分配响应式的 getter 和 setter。对于页面上存在的每个指令,都会添加一个观察者,它会在set方法中被通知。在控制台中打开vue.js代码,并搜索一下说set: function reactiveSetter(newVal)的那一行。添加一个断点,尝试在输入框中更改购物清单的标题。现在一步步执行,直到达到这个函数中的最后一个调用,它说dep.notify()

DefineProperty, getters, and setters

在调用观察者通知方法的 setter 函数内部设置断点

进入这个函数。你会看到这个函数正在遍历属性的观察者并更新它们。如果你跳过这个调用,你会发现 DOM 没有被更新。这是因为在同一个事件循环中执行的更新被放入了定期刷新的队列中。

找到runBatcherQueue函数并在其中设置一个断点。再次尝试更改标题。你会看到这个函数遍历了所有等待在队列中的观察者,并在每个观察者上调用run方法。如果你进入这个方法,你会看到它将新值与先前的值进行比较:

if (value !== this.value ||... 

然后它调用了回调的执行:

this.cb.call(this.vm, value, oldValue); 

如果你进入这个回调函数,你会看到最终它会更新 DOM 的值:

    update: function update(value) { 
      **this.el[this.attr] = _toString(value);** 
    } 

这不是很简单吗?

注意:

在这个调试中使用的是 Vue 版本 1.0。

所以 Vue.js 响应式数据绑定背后的机制非常简单。观察者被分配给所有的指令和数据属性。然后,在Object.definePropertyset方法中,观察者被通知,然后它们更新相应的 DOM 或数据:

DefineProperty, getters, and setters

从数据对象到 DOM 的数据流

具有指令的 DOM 元素附加了监听器,监听它们的更新并调用相应的数据属性 setter,然后唤醒它们的观察者。

与其他框架相比

当你尝试一个新的工具时,你想知道它与其他工具或框架相比如何。你可以在 Vue.js 的官方页面上找到关于这方面的深入分析:vuejs.org/guide/comparison.html。我只会指出一些我认为对于大多数使用的框架很重要的主题。

React

React 和 Vue 非常相似。它们都使用虚拟 DOM,具有可重用的组件,并且都是关于反应性数据。然而,值得一提的是,Vue 只从其第二个主要版本开始使用虚拟 DOM。在 Vue 2.0 之前,它使用真实的 DOM。Vue 2.0 发布不仅比 Vue 1.0 更高效,而且比 React 更高效(vuejs.org/guide/comparison.html#Performance-Profiles)。

最显著的区别可能是两个框架中如何创建组件的方式。你可能已经知道在 React 中,一切都是 JavaScript。即使是模板,也是用 JavaScript 开发的,这实际上可能是好的,因此程序员总是在相同的范围内,渲染变得更加灵活。

然而,对于一些想要进行快速原型设计的设计师,或者对编程技能不是很强的开发人员,或者只是不想学习 JSX 的人来说,这可能会变得非常痛苦。在 Vue 组件中,你实际上也可以使用 JSX,但你仍然可以遵循常见的 Web 开发结构:在<style>标签内编写 CSS,在<template>标签内编写 HTML 代码,在<script>标签内编写组件的逻辑。例如,比较一下 React 中渲染函数内的模板和你可以在 Vue 组件内编写的模板。在这个例子中,我将展示如何渲染我们之前看到的购物清单的项目列表。因此,在 React 中,你最终会得到类似于这样的 JSX 代码:

render () { 
  return ( 
    <ul> 
    {items.map(item => 
    <li className={item.checked && 'removed'}> 
      <div className='checkbox'> 
        <input type='checkbox' checked={item.checked}>          
        { item.text } 
      </div> 
    </li> 
    )} 
    </ul> 
  ) 
}); 

使用 Vue,你只需在template标签内写入以下 HTML 代码:

<template> 
<ul> 
  <li v-for="item in items" :class="{ 'removed': item.checked }"> 
    <div class="checkbox"> 
    <label> 
    <input type="checkbox" v-model="item.checked"> {{ item.text }} 
  </label> 
  </div> 
  </li> 
</ul> 
</template>

我个人喜欢将这些东西分开,因此我觉得 Vue 提供了这种可能性很好。

Vue 的另一个好处是它允许使用scoped属性附加到style标签来在组件内部限定样式:

<style **scoped**> 
</style> 

在这种样式中,如果你使用预处理器,你仍然可以访问所有全局定义的变量,并且可以创建或重新定义只能由该组件访问的样式。

还值得一提的是,这两个框架的学习曲线。要开始使用 React 开发应用程序,您可能需要学习 JSX 和 ES2105 语法,因为官方 React 文档中的大多数示例都使用它。而对于 Vue,您可以从零开始。只需将其包含在页面中,就像您使用 jQuery 一样,您就可以使用相当简单和易于理解的语法来使用 Vue 模型和数据绑定,以及您喜欢使用的任何 JavaScript 版本。之后,您可以在学习和应用程序风格上进行扩展。

如果您想对这两个框架进行更深入的分析,请查看文档,尝试阐述类似的例子,并检查哪个更适合您的需求。

Angular

Angular 1 和 Angular 2 之间有很大的区别。我们都知道,Angular 的第二个版本与其前身完全不同。它提供了更高的性能,API 也不同,并且底层实现已经被重写。

这两个版本的区别如此之大,以至于在 Vue 的官方文档中,您会发现对比两个 Angular 版本的比较,就像对比两个不同的框架一样。然而,学习曲线以及每个框架强制您构建应用程序的方式对于两个 Angular 版本都是横跨的。事实证明,Vue 比 Angular 1 和 Angular 2 都不那么武断。只需比较一下 Angular 的快速入门指南和 Vue 的 hello world 应用程序,就可以看出这一点。angular.io/docs/js/latest/quickstart.htmlvuejs.org/guide/index.html#Hello-World

"即使没有 TypeScript,Angular 的快速入门指南也从一个使用 ES2015 JavaScript、NPM 的应用程序开始,有 18 个依赖项,4 个文件,超过 3000 个字来解释这一切 - 只是为了说 Hello World。"
--http://vuejs.org/guide/comparison.html#Learning-Curve

如果您仍在使用 Angular 1,值得一提的是,这个框架与 Vue 之间的重大区别在于,在这个版本的 Angular 中,每次作用域发生变化时,都会重新评估所有的观察者,从而执行脏检查,因此当观察者的数量变得相当高时,性能会降低。因此,在 Vue 中,当作用域中的某些内容发生变化时,只有这个属性的观察者会被重新评估。其他观察者都会保持空闲,等待它们各自的调用。

Vue

不,这不是打字错误。值得一提的是,Vue 也值得与 Vue 进行比较。Vue 最近推出了它的第二个版本,比起前身更快更干净。如果你仍在使用 Vue 1.0,值得升级。如果你对 Vue 的版本一无所知,值得了解它的发展以及新版本允许做什么。查看 Vue 在 2016 年 4 月发布 Vue 2.0 的博客文章vuejs.org/2016/04/27/announcing-2.0/

Vue.js 基础知识

在将我们的手放入代码并开始用组件、插件、混合、模板和其他东西增强我们的应用程序之前,让我们回顾一下主要的 Vue 特性。让我们分析可重用组件是什么,以及如何管理应用程序状态,还谈论插件、过滤器和混合。在本节中,我们将对这些特性进行简要概述。我们将在接下来的章节中深入学习它们。

可重用组件

现在你不仅知道 Vue.js 中的数据绑定是什么以及如何使用它,还知道它是如何工作的,现在是时候介绍 Vue.js 的另一个强大特性了。使用 Vue.js 创建的组件可以在应用程序中被使用和重复使用,就像你用砖块建造房子一样。每个组件都有自己的样式和绑定范围,与其他组件完全隔离。

组件创建语法与我们已经了解的Vue实例创建非常相似,你只需要使用Vue.extend而不是Vue

var CustomComponent = Vue.extend({...}) 

可重用组件

Vue.js 中的自定义组件

例如,让我们尝试将我们的购物清单代码分成组件。你记得,我们的购物清单基本上由三部分组成:包含购物清单项目的部分,包含添加新项目的输入的另一部分,以及允许更改购物清单标题的第三部分。

可重用组件

购物清单应用程序的三个基本部分

让我们修改应用程序的代码,使其使用三个组件,每个部分一个组件。

我们的代码看起来像下面这样:

var data = { 
  items: [{ text: 'Bananas', checked: true },    
          { text: 'Apples', checked: false }], 
  title: 'My Shopping List', 
  newItem: '' 
}; 

new Vue({ 
  el: '#app', 
  data: data, 
  methods: { 
    addItem: function () { 
      var text; 

      text = this.newItem.trim(); 
      if (text) { 
        this.items.push({ 
          text: text, 
          checked: false 
        }); 
        this.newItem = ''; 
      } 
    } 
  } 
}); 

现在我们将创建三个组件:ItemsComponentChangeTitleComponentAddItemComponent。它们都将具有带有 data 对象的 data 属性。addItem 方法将从主 Vue 实例跳转到 ChangeTitleComponent。所有必要的 HTML 将从我们的 index.html 文件转移到每个组件中。因此,最终,我们的主脚本将如下所示:

var data = { 
  items: [{ text: 'Bananas', checked: true },
          { text: 'Apples', checked: false }], 
  title: 'My Shopping List', 
  newItem: '' 
}; 

/** 
 * Declaring components 
 */ 
var **ItemsComponent** = Vue.extend({ 
  data: function () { 
    return data; 
  }, 
  template: '<ul>' + 
  '           <li v-for="item in items"
              :class="{ 'removed': item.checked }">' + 
  '             <div class="checkbox">' + 
  '               <label>' + 
  '                 <input type="checkbox"                       
                    v-model="item.checked"> {{ item.text }}' + 
  '               </label>' + 
  '             </div>' + 
  '           </li>' + 
  '         </ul>' 
}); 
var **ChangeTitleComponent** = Vue.extend({ 
  data: function () { 
    return data; 
  }, 
  template: '<input v-model="title"/>' 
}); 
var **AddItemComponent** = Vue.extend({ 
  data: function () { 
    return data; 
  }, 
  methods: { 
    addItem: function () { 
      var text; 

      text = this.newItem.trim(); 
      if (text) { 
        this.items.push({ 
          text: text, 
          checked: false 
        }); 
        this.newItem = ""; 
      } 
    } 
  }, 
  template: 
  '<div class="input-group">' + 
    '<input v-model="newItem" @keyup.enter="addItem"        
     placeholder="add shopping list item" type="text"       
     class="form-control">'  + 
    '<span class="input-group-btn">'  + 
    '  <button @click="addItem" class="btn btn-default"           
       type="button">Add!</button>'  + 
    '</span>'  + 
  '</div>' 
}); 

/** 
 * Registering components 
 */ 
**Vue.component('items-component', ItemsComponent); 
Vue.component('change-title-component', ChangeTitleComponent); 
Vue.component('add-item-component', AddItemComponent);** 
/** 
 * Instantiating a Vue instance 
 */ 
new Vue({ 
  el: '#app', 
  data: data 
}); 

我们如何在视图中使用这些组件?我们只需用注册组件的标签替换相应的标记。我们的标记看起来像下面这样:

可重用组件

具有定义组件的购物清单应用程序标记

因此,我们将用 <add-item-component></add-item-component> 标签替换第一个高亮区域,用 <items-component></items-component> 标签替换第二个高亮区域,用 <change-title-component></change-title-component> 标签替换第三个高亮区域。因此,我们之前庞大的标记现在看起来像下面这样:

<div id="app" class="container"> 
  <h2>{{ title }}</h2> 
  **<add-item-component></add-item-component> 
  <items-component></items-component>** 
  <div class="footer"> 
    <hr/> 
    <em>Change the title of your shopping list here</em> 
    **<change-title-component></change-title-component>** 
  </div> 
</div> 

我们将在下一章深入研究组件,并学习一种更好的组织方式。敬请关注!

Vue.js 指令

在上一章中,您已经学习了指令是什么,以及它们如何用于增强应用程序的行为。

您已经使用了一些指令,这些指令以不同的方式允许数据绑定到视图层(v-modelv-ifv-show等)。除了这些默认指令之外,Vue.js 还允许您创建自定义指令。自定义指令提供了一种机制,可以实现 DOM 到数据的自定义映射行为。

在注册自定义指令时,您可以提供三个函数:bindupdateunbind。在 bind 函数中,您可以将事件侦听器附加到元素,并在那里执行任何需要执行的操作。在接收旧值和新值作为参数的 update 函数中,您可以定义数据更改时应该发生的自定义行为。unbind 方法提供了所有所需的清理操作(例如,分离事件侦听器)。

提示

在 Vue 2.0 中,指令显著减少了责任范围,现在它们只用于应用低级别的直接 DOM 操作。Vue 的变更指南建议优先使用组件而不是自定义指令(github.com/vuejs/vue/issues/2873)。

因此,自定义指令的完整版本将如下所示:

Vue.directive('my-directive', { 
  bind: function () { 
    // do the preparation work on element binding 
  }, 
  update: function (newValue, oldValue) { 
    // do something based on the updated value 
  }, 
  unbind: function () { 
    // do the clean-up work 
  } 
}) 

简化版本,如果您只需要在值更新时执行某些操作,则只能具有update方法,该方法可以直接作为指令函数的第二个参数传递:

Vue.directive('my-directive', function (el, binding) { 
  // do something with binding.value 
}) 

理论很好,但没有一个小例子,它就会变得无聊。所以让我们看一个非常简单的例子,每次更新其值时都会显示数字的平方。

我们的自定义指令将如下所示:

Vue.directive('square', function (el, binding) { 
  el.innerHTML = Math.pow(binding.value, 2); 
}); 

在模板文件中使用v-前缀使用此指令:

<div v-square="item"></div> 

在其数据中实例化Vue实例,并尝试更改item的值。您会看到div元素内的值将立即显示更改后的值的平方数。此自定义指令的完整代码可以在 JSFiddle 上找到jsfiddle.net/chudaol/we07oxbd/

在 Vue.js 中的插件

Vue 的核心功能,正如我们已经分析的那样,提供了声明性数据绑定和组件组合。这种核心行为通过提供丰富功能的插件得到增强。有几种类型的插件:

  • 添加一些全局属性或方法(vue-element)的插件

  • 添加一些全局资源(vue-touch)的插件

  • 添加Vue实例方法并将它们附加到 Vue 的原型上的插件

  • 提供一些外部功能或 API(vue-router)的插件

插件必须提供一个install方法,该方法可以访问全局Vue对象,以增强和修改它。为了使用此插件,Vue 提供了use方法,该方法接收插件实例(Vue.use(SomePlugin))。

提示

您还可以编写自己的 Vue 插件,以启用对Vue实例的自定义行为。

让我们使用先前的自定义指令示例,并创建一个实现数学平方和平方根指令的简约插件。创建一个名为VueMathPlugin.js的文件,并添加以下代码:

export default { 
  **install**: function (Vue) { 
    Vue.directive('square', function (el, binding) { 
      el.innerHTML = Math.pow(binding.value, 2); 
    }); 
    Vue.directive('sqrt', function (el, binding) { 
      el.innerHTML = Math.sqrt(binding.value); 
    }); 
  } 
}; 

现在创建一个名为script.js的文件。让我们将主要脚本添加到此文件中。在此脚本中,我们将同时导入VueVueMathPlugin,并将调用 Vue 的use方法,以告诉它使用插件并调用插件的install方法。然后我们将像往常一样初始化一个Vue实例:

import Vue from 'vue/dist/vue.js'; 
import VueMathPlugin from './VueMathPlugin.js'; 

**Vue.use(VueMathPlugin);** 

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

现在创建一个包含main.js文件的index.html文件(我们将使用 Browserify 和 Babelify 构建它)。在这个文件中,让我们使用v-model指令添加一个输入,用于更改项目的值。使用v-squarev-sqrt指令创建两个 span:

<body> 
  <div id="app"> 
    <input v-model="item"/> 
    <hr> 
    <div>Square: <span **v-square="item"**></span></div> 
    <div>Root: <span **v-sqrt="item"**></span></div> 
  </div> 
  <script src="main.js"></script> 
</body> 

创建一个package.json文件,包括构建项目所需的依赖项,并添加一个构建main.js文件的脚本:

{ 
  "name": "vue-custom-plugin", 
  "scripts": { 
    "build": **"browserify script.js -o main.js -t
       [ babelify --presets [ es2015 ] ]"** 
  }, 
  "version": "0.0.1", 
  "devDependencies": { 
    "babel-preset-es2015": "⁶.9.0", 
    "babelify": "⁷.3.0", 
    "browserify": "¹³.0.1", 
    "vue": "².0.3" 
  } 
} 

现在使用以下命令安装依赖并构建项目:

**npm install**
**npm run build**

在浏览器中打开index.html。尝试更改输入框中的数字。正方形和平方根的值都会立即改变:

Vue.js 中的插件

数据的更改立即应用于作为自定义插件的一部分创建的指令

练习

使用三角函数(正弦、余弦和正切)增强MathPlugin

此练习的可能解决方案可以在附录中找到。

应用状态和 Vuex

当应用程序达到相当大的规模时,可能需要我们以某种方式管理全局应用程序状态。受 Flux(facebook.github.io/flux/)的启发,有一个 Vuex 模块,允许我们在 Vue 组件之间管理和共享全局应用程序状态。

提示

不要将应用程序状态视为复杂和难以理解的东西。实际上,它只不过是数据。每个组件都有自己的数据,“应用程序状态”是可以在所有组件之间轻松共享的数据!

应用状态和 Vuex

Vuex 存储库如何管理应用程序状态更新

与其他插件一样,为了能够使用和实例化 Vuex 存储库,您需要指示 Vue 使用它:

import Vuex from 'vuex'; 
import Vue from 'vue'; 

Vue.use(Vuex); 

var store = new Vuex.Store({ 
  state: { <...> }, 
  mutations: { <...> } 
}); 

然后,在初始化主组件时,将存储实例分配给它:

new Vue({ 
  components: components, 
  store: store 
}); 

现在,主应用程序及其所有组件都知道存储库,可以访问其中的数据,并能够在应用程序的任何生命周期中触发操作。我们将在接下来的章节中深入挖掘应用程序状态。

vue-cli

是的,Vue 有自己的命令行界面。它允许我们使用任何配置初始化 Vue 应用程序。您可以使用 Webpack 样板初始化它,使用 Browserify 样板初始化它,或者只是使用一个简单的样板,只需创建一个 HTML 文件并为您准备好一切,以便开始使用 Vue.js 进行工作。

使用npm安装它:

**npm install -g vue-cli**

初始化应用程序的不同方式如下:

**vue init webpack**
**vue init webpack-simple**
**vue init browserify**
**vue init browserify-simple**
**vue init simple**

为了看到区别,让我们分别使用简单模板和 Webpack 模板运行vue init,并查看生成结构的差异。以下是两个命令的输出差异:

vue-cli

命令vue init webpackvue init simple的输出

以下是应用程序结构的不同之处:

vue-cli

使用vue init simplevue init webpack脚手架生成的应用程序结构的不同之处

简单配置中的index.html文件已经包含了来自 CDN 的 Vue.js,所以如果你只需要做一些非常简单的事情,比如快速原型设计,就可以使用这个。

但是,如果你要开始一个需要在开发过程中进行测试和热重载的复杂单页面应用程序SPA)项目,请使用 Webpack 或 Browserify 配置。

IDE 的 Vue 插件

有一些主要 IDE 的 Vue 语法高亮插件。我会给你留下最潮的链接:

IDE 链接到 Vue 插件
Sublime github.com/vuejs/vue-syntax-highlight
Webstorm github.com/postalservice14/vuejs-plugin
Atom github.com/hedefalk/atom-vue
Visual Studio Code github.com/LiuJi-Jim/vscode-vue
vim github.com/posva/vim-vue
Brackets github.com/pandao/brackets-vue

安装、使用和调试 Vue.js 应用程序

在本节中,我们将分析安装 Vue.js 的所有可能方式。我们还将为我们将在接下来的章节中开发和增强的应用程序创建一个骨架。我们还将学习调试和测试我们的应用程序的方法。

安装 Vue.js

有许多种安装 Vue.js 的方式。从经典的开始,包括将下载的脚本放入 HTML 的<script>标签中,使用诸如 bower、npm 或 Vue 的命令行接口(vue-cli)等工具,以启动整个应用程序。

让我们看看所有这些方法,并选择我们喜欢的。在所有这些示例中,我们只会在页面上显示一个标题,写着学习 Vue.js

独立使用

下载vue.js文件。有两个版本,压缩和开发者版本。开发版本在vuejs.org/js/vue.js。压缩版本在vuejs.org/js/vue.min.js

提示

如果您正在开发,请确保使用 Vue 的开发非压缩版本。您会喜欢控制台上的良好提示和警告。

然后只需在<script>标签中包含vue.js,如下所示:

<script src="vue.js"></script> 

Vue 已在全局变量中注册。您可以开始使用它。

我们的示例将如下所示:

  <div id="app"> 
    <h1>{{ message }}</h1> 
  </div> 
  **<script src="vue.js"></script>** 
  <script> 
     var data = { 
       message: 'Learning Vue.js' 
     }; 

     new Vue({ 
       el: '#app', 
       data: data 
     }); 
  </script> 

CDN

Vue.js 在以下 CDN 中可用:

只需将 URL 放在script标签中的源中,您就可以使用 Vue 了!

<script src="  https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script> 

提示

请注意 CDN 版本可能与 Vue 的最新可用版本不同步。

因此,示例看起来与独立版本完全相同,但是我们使用 CDN URL 而不是在<script>标签中使用下载的文件。

Bower

如果您已经在 Bower 中管理您的应用程序,并且不想使用其他工具,Vue 也有一个 Bower 分发版本。只需调用bower install

**# latest stable release**
**bower install vue**

我们的示例看起来与前两个示例完全相同,但它将包括来自bower文件夹的文件:

<script src="bower_components/vue/dist/vue.js"></script> 

符合 CSP

内容安全策略CSP)是一种安全标准,提供了一组规则,必须由应用程序遵守,以防止安全攻击。如果您正在为浏览器开发应用程序,您可能熟悉这个策略!

对于需要符合 CSP 的脚本的环境,Vue.js 有一个特殊版本,位于github.com/vuejs/vue/tree/csp/dist

让我们将我们的示例作为 Chrome 应用程序,看看符合 CSP 的 Vue.js 在其中的表现!

首先创建一个文件夹用于我们的应用程序示例。Chrome 应用程序中最重要的是manifest.json文件,它描述了您的应用程序。让我们创建它。它应该如下所示:

{ 
  "manifest_version": 2, 
  "name": "Learning Vue.js", 
  "version": "1.0", 
  "minimum_chrome_version": "23", 
  "icons": { 
    "16": "icon_16.png", 
    "128": "icon_128.png" 
  }, 
  "app": { 
    "background": { 
      "scripts": ["main.js"] 
    } 
  } 
} 

下一步是创建我们的main.js文件,这将是 Chrome 应用程序的入口点。该脚本应监听应用程序的启动并打开一个给定大小的新窗口。让我们创建一个大小为 500 x 300 的窗口,并使用index.html打开它:

chrome.app.runtime.onLaunched.addListener(function() { 
  // Center the window on the screen. 
  var screenWidth = screen.availWidth; 
  var screenHeight = screen.availHeight; 
  var width = 500; 
  var height = 300; 

  chrome.app.window.create(**"index.html"**, { 
    id: "learningVueID", 
    outerBounds: { 
      width: width, 
      height: height, 
      left: Math.round((screenWidth-width)/2), 
      top: Math.round((screenHeight-height)/2) 
    } 
  }); 
}); 

此时,Chrome 特定的应用程序魔法已经结束,现在我们只需创建一个index.html文件,该文件将执行与之前示例中相同的操作。它将包括vue.js文件和我们的脚本,我们将在其中初始化我们的 Vue 应用程序:

<html lang="en"> 
<head> 
    <meta charset="UTF-8"> 
    <title>Vue.js - CSP-compliant</title> 
</head> 
<body> 
<div id="app"> 
    <h1>{{ message }}</h1> 
</div> 
<script src="assets/vue.js"></script> 
<script src="assets/app.js"></script> 
</body> 
</html> 

下载符合 CSP 标准的 Vue.js 版本,并将其添加到assets文件夹中。

现在让我们创建app.js文件,并添加我们已经多次编写的代码:

var data = { 
  message: "Learning Vue.js" 
}; 

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

将其添加到assets文件夹中。

不要忘记创建两个 16 和 128 像素的图标,并分别将它们命名为icon_16.pngicon_128.png

最后,您的代码和结构应该看起来与以下内容差不多:

CSP-compliant

使用 vue.js 创建示例 Chrome 应用程序的结构和代码

现在最重要的事情。让我们检查它是否有效!这非常非常简单:

  1. 在 Chrome 浏览器中转到chrome://extensions/url

  2. 勾选“开发者模式”复选框。

  3. 单击“加载未打包的扩展程序...”,并检查我们刚刚创建的文件夹。

  4. 您的应用程序将出现在列表中!现在只需打开一个新标签,单击应用程序,然后检查您的应用程序是否存在。单击它!

CSP-compliant

在 Chrome 应用程序列表中使用 vue.js 的示例 Chrome 应用程序

恭喜!您刚刚创建了一个 Chrome 应用程序!

npm

建议对于大型应用程序使用npm安装方法。只需按照以下方式运行npm install vue

**# latest stable release**
**npm install vue**
**# latest stable CSP-compliant release**
**npm install vue@csp**

然后需要引入它:

**var Vue = require("vue");**

或者,对于 ES2015 爱好者,请运行以下命令:

**import Vue from "vue";**

我们的 HTML 将与之前的示例完全相同:

<html lang="en"> 
<head> 
  <meta charset="UTF-8"> 
  <title>Vue.js - NPM Installation</title> 
</head> 
<body> 
  <div id="app"> 
    <h1>{{ message }}</h1> 
  </div> 
  <script src=**"main.js"**></script> 
</body> 
</html> 

现在让我们创建一个script.js文件,它几乎与独立版本或 CDN 版本完全相同,唯一的区别是它将需要vue.js

**var Vue = require('vue/dist/vue.js');** 

var data = { 
  message: 'Learning Vue.js' 
}; 

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

让我们安装 Vue 和 Browserify,以便能够将我们的script.js文件编译成main.js文件:

**npm install vue --save-dev**
**npm install browserify --save-dev**

package.json文件中,添加一个构建脚本,该脚本将在script.js上执行 Browserify,将其转换为main.js。因此,我们的package.json文件将如下所示:

{ 
  "name": "learningVue", 
  "scripts": { 
    "build": "browserify script.js -o main.js" 
  }, 
  "version": "0.0.1", 
  "devDependencies": { 
    "browserify": "¹³.0.1", 
    "vue": "².0.3" 
  } 
} 

现在运行以下命令:

**npm run build**

然后在浏览器中打开index.html

我有一个朋友在这一点上会说类似的话:真的吗?这么多步骤,安装,命令,解释...只是为了输出一些标题?我不干了!

如果您也在思考这个问题,请等一下。是的,这是真的,现在我们以一种相当复杂的方式做了一些非常简单的事情,但是如果您和我一起坚持一会儿,您将看到如果我们使用适当的工具,复杂的事情变得容易实现。另外,不要忘记检查您的番茄钟,也许是休息的时候了!

vue-cli

正如我们在上一章中已经提到的,Vue 提供了自己的命令行界面,允许使用您想要的任何工作流来引导单页应用程序。它立即提供了热重载和测试驱动环境的结构。安装了vue-cli之后,只需运行vue init <desired boilerplate> <project-name>,然后只需安装和运行:

**# install vue-cli**
**$ npm install -g vue-cli**
**# create a new project**
**$ vue init webpack learn-vue**
**# install and run**
**$ cd learn-vue**
**$ npm install**
**$ npm run dev** 

现在在localhost:8080上打开您的浏览器。您刚刚使用vue-cli来搭建您的应用程序。让我们将其调整到我们的示例中。打开一个源文件夹。在src文件夹中,您将找到一个App.vue文件。您还记得我们谈到过 Vue 组件就像是您构建应用程序的砖块吗?您还记得我们是如何在主脚本文件中创建和注册它们的,并且我提到我们将学习以更优雅的方式构建组件吗?恭喜,您正在看一个以时髦方式构建的组件!

找到说import Hello from './components/Hello'的那一行。这正是组件在其他组件中被重用的方式。看一下组件文件顶部的模板。在某个地方,它包含<hello></hello>标记。这正是在我们的 HTML 文件中hello组件将出现的地方。看一下这个组件;它在src/components文件夹中。正如您所看到的,它包含一个带有{{ msg }}的模板和一个导出带有定义的msg数据的脚本。这与我们在之前的示例中使用组件时所做的完全相同。让我们稍微修改代码,使其与之前的示例相同。在Hello.vue文件中,更改data对象中的msg

<script> 
export default { 
  data () { 
    return { 
   msg: **"Learning Vue.js"** 
    } 
  } 
} 
</script> 

App.vue组件中,从模板中删除除了hello标记之外的所有内容,使模板看起来像下面这样:

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

现在,如果重新运行应用程序,您将看到我们的示例具有美丽的样式,而我们没有触及:

vue-cli

使用 vue-cli 引导的 Vue 应用程序

提示

除了 Webpack 样板模板,你可以使用以下配置与你的vue-cli一起使用:

  • webpack-simple:一个用于快速原型设计的简单 Webpack + vue-loader 设置

  • browserify:一个具有热重载、linting 和单元测试的全功能 Browserify + Vueify 设置

  • browserify-simple:一个用于快速原型设计的简单 Browserify + Vueify 设置

  • simple:一个在单个 HTML 文件中的最简单的 Vue 设置

Dev build

亲爱的读者,我能看到你闪亮的眼睛,我能读懂你的心思。现在你知道如何安装和使用 Vue.js 以及它的工作原理,你肯定想深入了解核心代码并做出贡献!

我理解你。为此,你需要使用 Vue.js 的开发版本,你需要从 GitHub 上下载并自行编译。

让我们用这个开发版本的 Vue 来构建我们的示例。创建一个新文件夹,例如dev-build,并将所有文件从 npm 示例复制到此文件夹中。

不要忘记复制node_modules文件夹。你应该cd进入它,并从 GitHub 下载文件到其中,然后运行npm installnpm run build

**cd <APP-PATH>/node_modules**
**rm -rf vue**
**git clone https://github.com/vuejs/vue.git**
**cd vue**
**npm install**
**npm run build**

现在构建我们的示例应用程序:

**cd <APP-PATH>**
**npm run build**

在浏览器中打开index.html,你会看到通常的学习 Vue.js标题。

现在让我们尝试更改vue.js源代码中的一些内容!转到node_modules/vue/src/compiler/parser文件夹,并打开text-parser.js文件。找到以下行:

const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g  

实际上,这个正则表达式定义了 HTML 模板中使用的默认定界符。这些定界符内的内容被识别为 Vue 数据或 JavaScript 代码。让我们改变它们!让我们用双百分号替换{}!继续编辑文件:

const defaultTagRE = /\%\%((?:.|\n)+?)\%\%/g  

现在重新构建 Vue 源代码和我们的应用程序,然后刷新浏览器。你看到了什么?

Dev build

更改 Vue 源代码并替换定界符后,{{}}定界符不再起作用!

{{}}中的消息不再被识别为我们传递给 Vue 的数据。实际上,它被呈现为 HTML 的一部分。

现在转到index.html文件,并用双百分号替换我们的花括号定界符,如下所示:

<div id="app"> 
  <h1>**%% message %%**</h1> 
</div> 

重新构建我们的应用程序并刷新浏览器!现在怎么样?你看到了改变框架代码并尝试你的改变是多么容易。我相信你有很多关于如何改进或添加一些功能到 Vue.js 的想法。所以改变它,重新构建,测试,部署!愉快的拉取请求!

调试您的 Vue 应用程序

您可以像调试任何其他 Web 应用程序一样调试您的 Vue 应用程序。使用开发者工具(firebug),断点,调试器语句等。如果您想深入了解 Chrome 调试工具,请查看 Chrome 的文档developer.chrome.com/devtools

Vue 还提供了Vue.js devtools,因此调试 Vue 应用程序变得更容易。您可以从 Chrome 网络商店下载并安装它chrome.google.com/webstore/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd

不幸的是,它不适用于本地打开的文件,因此请使用一些简单的 HTTP 服务器来将我们的示例作为 Web 页面提供(例如,www.npmjs.com/package/http-server)。

安装后,打开,例如,我们的购物清单应用程序。打开开发者工具。您将看到Vue选项卡已自动出现:

调试您的 Vue 应用程序

Vue devtools

在我们的情况下,我们只有一个组件—<Root>。可以想象,一旦我们开始使用组件并且有很多组件,它们都会出现在 Vue devtools 调色板的左侧。单击<Root>组件并对其进行检查。您将看到附加到此组件的所有数据。如果您尝试更改某些内容,例如添加购物清单项目,检查或取消复选框,更改标题等,所有这些更改都将立即传播到 Vue devtools 中的数据。您将立即在其右侧看到更改。例如,让我们尝试添加一个购物清单项目。一旦开始输入,您会看到newItem如何相应更改:

调试您的 Vue 应用程序

模型中的更改立即传播到 Vue devtools 数据

当我们开始添加更多组件并向我们的 Vue 应用程序引入复杂性时,调试肯定会变得更有趣!

搭建我们的应用程序

你还记得我们在第一章开始工作的两个应用程序,购物清单应用程序和番茄钟应用程序吗?在本节中,我们将使用vue-cli工具搭建这些应用程序,以便它们准备好包含可重用组件,进行测试和部署。一旦我们引导这些应用程序,我们将在本书的最后工作。所以让我们小心谨慎地做,并充满爱心!

搭建购物清单应用程序

我们将使用vue-cli Webpack 配置来搭建购物清单应用程序。

提示

如果你忽略了与vue-cli相关的所有先前的实际练习,请不要忘记在继续下一步之前安装它:npm install -g vue-cli

如果你已经安装了vue-cli,请转到要引导应用程序的目录并运行以下命令:

**vue init webpack shopping-list**

对所有问题回答 yes(只需点击回车键),voilà!你已经引导了应用程序:

搭建购物清单应用程序

使用 vue-cli 引导购物清单应用程序

切换到购物清单目录并运行npm installnpm run dev。在localhost:8080上打开你的浏览器。你会看到新创建的 Vue 应用程序的Hello World页面:

搭建购物清单应用程序

新引导应用程序的 Hello World 视图

让我们清理引导代码,使应用程序准备好填充我们的特定应用程序代码。转到App.vue文件并删除所有内容,只留下定义应用程序结构的标签:

  • <template>与主要的<div>内部

  • <script>标签

  • <style>标签

因此,最终,你的App.vue文件看起来像下面这样:

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

<script>
</script>

<style>
</style>

看看在浏览器中打开的页面。有趣的是,你什么都没做,但页面现在不再包含默认的Hello World。页面是空的!它自动改变了!

尝试在<template>标签内添加一些内容。查看页面;一旦你引入更改,它会自动重新加载。这是因为vue-hot-reload插件会检测你的 Vue 组件的更改,并自动重建项目并重新加载浏览器页面。尝试在<script>标签内写一些不符合 lint 标准的 JavaScript 代码,例如使用notDefinedVariable

<script> 
  **notDefinedVariable = 5;** 
</script> 

浏览器中的页面没有刷新。看看你的 shell 控制台。它显示了lint错误,并且“拒绝”构建你的应用程序:

搭建购物清单应用程序

每次应用程序更改时都会检查 lint 规则

这是因为 ESLint 插件会检查代码是否符合 lint 规则,每次应用程序更改时都会发生这种情况。

有了这个,我们可以确信我们的代码将遵循最佳的质量标准。

说到质量,我们还应该准备好我们的应用程序能够运行单元测试。幸运的是,vue-cli与 Webpack 已经为我们做好了准备。运行npm run unit来运行单元测试,运行npm run e2e来运行端到端的 nightwatch 测试。端到端测试不会与正在运行的应用程序并行运行,因为两者都使用相同的端口。因此,如果你想在开发过程中运行测试,你应该在config/index.js配置文件中更改端口,或者在运行测试之间简单地停止应用程序。运行测试后,你会看到端到端测试失败。这是因为它们正在检查我们已经删除的应用程序特定元素。打开test/e2e/specs/目录下的test.js文件,并清除所有我们不再需要的断言。现在它应该看起来像下面这样:

module.exports = { 
  'default e2e tests': function (browser) { 
    browser 
    .url('http://localhost:8080') 
      .waitForElementVisible('#app', 5000) 
      .end() 
  } 
} 

重新运行测试。现在它们应该通过了。从现在开始,当我们向我们的应用程序添加代码时,我们将添加单元测试和端到端测试。

启动你的番茄钟应用程序

对于番茄钟应用程序,做与购物清单应用程序相同的事情。运行vue init webpack pomodoro,并重复所有必要的步骤,以确保结构已准备好用于填充番茄钟应用程序的代码!

练习

将我们的番茄钟应用程序实现为 Chrome 应用程序!你只需要使用符合 CSP 的 Vue.js 版本,并添加一个manifest.json文件。

总结

在本章中,我们分析了 Vue.js 的幕后情况。你学会了如何实现数据的响应性。你看到了 Vue.js 如何利用Object.defineProperty的 getter 和 setter 来传播数据的变化。你看到了 Vue.js 概念的概述,比如可重用组件、插件系统和使用 Vuex 进行状态管理。我们已经启动了我们将在接下来的章节中开发的应用程序。

在下一章中,我们将更深入地了解 Vue 的组件系统。我们将在我们的应用程序中使用组件。

第三章:组件-理解和使用

在上一章中,你学习了 Vue.js 的工作原理。你了解了幕后情况,甚至对 Vue.js 核心代码进行了轻微的调试。你学习了一些 Vue 的关键概念。你还学习并尝试了不同的安装 Vue.js 的方式。我们已经启动了应用程序;从本章开始,我们将开发和增强它。我们还学会了如何调试和测试我们的应用程序。

在第一章中,我们谈论了组件,甚至创建了一些。在本章中,我们将在我们的应用程序中使用组件,并看到一些有趣的指令在其中的作用。也就是说,在本章中,我们将做以下事情:

  • 重新讨论组件主题并回顾组件的定义

  • 为我们的应用程序创建组件

  • 学习什么是单文件组件

  • 学习如何使用特殊属性实现响应式 CSS 过渡

重新讨论组件

正如你在之前的章节中肯定记得的,组件是 Vue 应用程序的特殊部分,具有自己的数据和方法范围。组件可以在整个应用程序中被使用和重复使用。在上一章中,你学到了组件是通过使用Vue.extend({...})方法创建的,并且使用Vue.component()语法进行注册。因此,为了创建和使用一个组件,我们需要编写以下 JavaScript 代码:

//creating component 
var HelloComponent = Vue.extend({ 
  template: '<h1>Hello</h1>' 
}); 
//registering component 
Vue.component('hello-component', HelloComponent); 

//initializing the Vue application 
new Vue({ 
  el: '#app' 
}); 

然后,我们将在 HTML 中使用hello-component

<div id='app'> 
  <hello-component></hello-component> 
</div> 

提示

初始化和注册都可以写成单个Vue.component调用,带有相应的选项:

Vue.component('hello-component', { template: '<h1>Hello</h1>' });

使用组件的好处

在深入了解组件并重写应用程序之前,有一些东西我们需要学习。在本节中,我们将涵盖处理组件内的datael属性、组件模板、作用域和预处理器等内容。

在 HTML 中声明模板

在我们之前的例子中,我们创建了一个 Vue 组件,模板是以字符串形式编写的。这实际上很容易和不错,因为我们在组件内有我们需要的一切。现在想象一下我们的组件具有更复杂的 HTML 结构。编写复杂的 HTML 字符串模板容易出错,丑陋,并且违反最佳实践。

提示

通过最佳实践,我指的是清晰和可维护的代码。将复杂的 HTML 写成字符串是不可维护的。

Vue 允许在特殊的<template>标签内声明模板!

因此,为了重写我们的示例,我们将声明一个带有相应标记的 HTML 标签模板:

<template id="hello"> 
  <h1>Hello</h1> 
</template> 

然后,在我们的组件内部,我们将只使用模板的 ID,而不是 HTML 字符串:

Vue.component('hello-component', { 
  template: '#hello' 
}); 

我们的整个代码将如下所示:

<body> 
  **<template id="hello">** 
    <h1>Hello</h1> 
  **</template>** 

  <div id="app"> 
    <hello-component></hello-component> 
  </div> 

  <script src="vue.js"></script> 
  <script> 
    Vue.component('hello-component', { 
      template: '**#hello**' 
    }); 

    new Vue({ 
      el: '#app' 
    }); 
  </script> 
</body> 

在前面的示例中,我们只使用了组件的template属性。让我们继续看看datael属性在组件内部应该如何处理。

处理组件内的数据和 el 属性

如前所述,组件的语法与 Vue 实例的语法相同,但必须扩展 Vue 而不是直接调用它。基于这一前提,创建一个组件似乎是正确的:

var HelloComponent = Vue.extend({ 
  el: '#hello', 
  data: { msg: 'Hello' } 
}); 

但这会导致作用域泄漏。每个HelloComponent实例将共享相同的datael。这并不是我们想要的。这就是为什么 Vue 明确要求将这些属性声明为函数的原因:

var HelloComponent = Vue.component('hello-component', { 
  el: **function ()** { 
    return '#hello'; 
  }, 
  data: **function ()** { 
    return { 
      msg: 'Hello' 
    } 
  } 
}); 

即使您犯了错误,并将datael属性声明为对象或元素,Vue 也会友好地警告您:

处理组件内的数据和 el 属性

在 Vue 组件内将数据作为对象而不是函数使用时,Vue 会友好地警告

组件的作用域

如前所述,所有组件都有自己的作用域,其他组件无法访问。然而,全局应用程序作用域可被所有注册的组件访问。您可以将组件的作用域视为局部作用域,将应用程序作用域视为全局作用域。它是一样的。然而,在组件内部使用父级的数据并不直观。您必须在组件内明确指示应该使用prop属性访问哪些父级数据属性,并使用v-bind语法将它们绑定到组件实例。让我们看看它在我们的HelloComponent示例中是如何工作的。

让我们从声明包含属性msg的数据的HelloComponent开始:

Vue.component('hello-component', { 
  data: function () { 
    return { 
      **msg: 'Hello'** 
    } 
  } 
}); 

现在,让我们创建一个带有一些数据的Vue实例:

new Vue({ 
  el: '#app', 
  **data: { 
    user: 'hero' 
  }** 
});  

在我们的 HTML 中,让我们创建一个模板并将其应用到具有模板 ID 的组件上:

//template declaration 
<template id="hello"> 
  <h1>**{{msg}} {{user}}**</h1> 
</template> 

//using template in component 
Vue.component('hello-component', { 
  **template: '#hello',** 
  data: function () { 
    return { 
      msg: 'Hello' 
    } 
  } 
}); 

为了在页面上看到组件,我们应该在app容器的 HTML 中调用它:

<div id="app"> 
  **<hello-component></hello-component>** 
</div> 

如果您在浏览器中打开页面,您只会看到Hellouser数据属性仍然没有绑定到组件:

组件的作用域

父级的数据属性尚未绑定到我们的 Vue 组件

为了将数据绑定到父 Vue 应用程序,我们必须做以下两件事:

  • 在组件的prop属性中指示此属性

  • 将其绑定到hello-component调用:

//calling parent's data attributes in the component 
Vue.component('hello-component', { 
  template: '#hello', 
  data: function () { 
    return { 
      msg: 'Hello' 
    } 
  }, 
  **props: ['user']** 
}); 

//binding a user data property to the component 
<div id="app"> 
  <hello-component **v-bind:user="user"**></hello-component> 
</div> 

刷新页面,您将看到它现在向您呈现问候语:

组件的范围

在正确绑定父级的data属性到组件之后,一切都按预期运行。

提示

实际上,v-bind:user语法可以通过以下方式进行快捷操作:

:user<hello-component **:user="user"**></hello-component>

组件内部的组件

组件的美妙之处在于它们可以像乐高积木和积木一样在其他组件内部被使用和重复使用!让我们构建另一个组件;让我们称之为greetings,它将由两个子组件组成:一个要求用户姓名的表单和我们的hello组件。

为了做到这一点,让我们声明表单的模板和我们已经熟悉的hello模板:

<!--template for the form--> 
<template id="form"> 
  <div> 
    <label for="name">What's your name?</label> 
    <input **v-model="user"** type="text" id="name"> 
  </div> 
</template> 

//template for saying hello 
<template id="hello"> 
  <h1>{{msg}} {{user}}</h1> 
</template> 

现在我们将基于这些模板注册两个 Vue 组件:

//register form component 
Vue.component('**form-component**', { 
  template: '**#form**', 
  props: ['user'] 
}); 
//register hello component 
Vue.component('**hello-component**', { 
  template: '**#hello**', 
  data: function () { 
    return { 
      msg: 'Hello' 
    } 
  }, 
  props: ['user'] 
}); 

最后,我们将创建我们的 greetings 模板,它将使用formhello组件。不要忘记我们必须在组件调用时绑定user属性:

<template id="greetings"> 
  <div> 
    <form-component **:user="user"**></form-component> 
    <hello-component **:user="user"**></hello-component> 
  </div> 
</template> 

在这一点上,我们可以创建我们的 greetings 组件,并在其中使用 greetings 模板。让我们初始化,使用此组件中用户的data函数:

//create greetings component based on the greetings template 
Vue.component('greetings-component', { 
  template: '**#greetings**', 
  data: function () { 
    return { 
      user: 'hero' 
    } 
  } 
}); 

在我们的主应用程序容器内,我们现在将调用 greetings 组件:

<div id="app"> 
  **<greetings-component></greetings-component>** 
</div> 

不要忘记初始化 Vue 应用程序:

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

在浏览器中打开页面。您应该看到类似以下的内容:

组件内部的组件

由各种 Vue 组件构建的页面

尝试在输入框中更改名称。您期望它在问候标题中也发生变化,因为我们将其绑定到了它。但奇怪的是,它并没有改变。嗯,这实际上是正常的行为。默认情况下,所有属性都遵循单向数据绑定。这意味着如果在父级范围内更改数据,这些更改会传播到子组件,但反之则不会。这样做是为了防止子组件意外地改变父状态。但是,可以通过调用事件来强制子组件与其父组件通信。请查看 Vue 文档vuejs.org/guide/components.html#Custom-Events

在我们的案例中,我们可以将用户模型绑定到我们的表单input组件,并在用户在输入框中输入时发出input事件。我们通过使用v-on:input修饰符来实现这一点,就像在vuejs.org/guide/components.html#Form-Input-Components-using-Custom-Events中描述的那样。

因此,我们必须将v-model:user传递给form-component

<form-component **v-model="user"**></form-component> 

然后,form-component应该接受value属性并发出input事件:

Vue.component('form-component', { 
  template: '#form', 
  props: [**'value'**], 
  methods: { 
    **onInput: function (event) { 
      this.$emit('input', event.target.value) 
    }** 
  } 
}); 

form-component模板中的输入框应将v-on:inputonInput方法绑定到v-on:input修饰符:

<input **v-bind:value="value"** type="text" id="name" **v-on:input="onInput"**> 

提示

实际上,在 Vue 2.0 之前,可以通过显式告知使用.sync修饰符绑定的属性来实现组件与其父级之间的双向同步:<form-component :user.sync="user"></form-component>

刷新页面。现在您可以更改输入框中的名称,并立即传播到父级范围,从而传播到依赖该属性的其他子组件:

组件内部的其他组件

使用.sync修饰符绑定属性允许父级和子级组件之间的双向数据绑定

您可以在 JSFiddle 中找到此示例的完整代码jsfiddle.net/chudaol/1mzzo8yn/

提示

在 Vue 2.0 发布之前,还有一个数据绑定修饰符.once。使用此修饰符,数据将仅绑定一次,任何其他更改都不会影响组件的状态。比较以下内容:

<form-component **:user="user"**></form-component>

<form-component **:user.sync="user"**></form-component>

<form-component **:user.once="user"**></form-component>

使用简单组件重写购物清单

既然我们已经对组件有了很多了解,让我们使用它们来重写我们的购物清单应用程序。

提示

在重写应用程序时,我们将使用这个版本的购物清单应用程序作为基础:jsfiddle.net/chudaol/vxfkxjzk/3/

我们之前已经做过了,当我们开始讨论组件时。但那时,我们在组件选项中使用了字符串模板。现在让我们使用刚学会的模板来做。让我们再看一下界面,并再次识别组件:

使用简单组件重写购物清单

我们的购物清单应用程序将有四个组件

因此,我建议我们的购物清单应用程序由以下四个组件组成:

  • AddItemComponent:负责向购物清单添加新项目的组件

  • ItemComponent:负责在购物清单中呈现新项目的组件

  • ItemsComponent:负责渲染和管理ItemComponent列表的组件

  • ChangeTitleComponent:负责更改列表标题的组件

为所有组件定义模板

假设这些组件已经定义并注册,让我们为这些组件创建模板。

注意

驼峰命名法 VS 短横线命名法 您可能已经注意到,当我们声明描述组件的变量时使用驼峰命名法(var HelloComponent=Vue.extend({...})),但我们在短横线命名法中命名它们:Vue.component('hello-component', {...})。我们这样做是因为 HTML 属性不区分大小写的特性。因此,我们购物清单应用程序的组件将被称为:

add-item-component

item-component

items-component

change-title-component

看一下我们之前的标记是怎样的(jsfiddle.net/chudaol/vxfkxjzk/3/)。

让我们使用模板和组件名称重写它。在这部分,我们只关心呈现层,将数据绑定和操作处理留给将来实现。我们只需复制粘贴应用程序的 HTML 部分,并将其分发到我们的组件上。我们的四个模板将看起来像下面这样:

<!--add new item template--> 
<template id="**add-item-template**"> 
  <div class="input-group"> 
    <input @keyup.enter="addItem" v-model="newItem" 
      placeholder="add shopping list item" type="text" 
      class="form-control"> 
    <span class="input-group-btn"> 
      <button @click="addItem" class="btn btn-default" 
        type="button">Add!</button> 
    </span> 
  </div> 
</template> 

<!--list item template--> 
<template id="**item-template**"> 
  <li :class="{ 'removed': item.checked }"> 
    <div class="checkbox"> 
      <label> 
        <input type="checkbox" v-model="item.checked"> {{ item.text }} 
      </label> 
    </div> 
  </li> 
</template> 

<!--items list template--> 
<template id="**items-template**"> 
  <ul> 
    <item-component v-for="item in items" :item="item">
    </item-component> 
  </ul> 
</template> 

<!--change title template--> 
<template id="**change-title-template**"> 
  <div> 
    <em>Change the title of your shopping list here</em> 
    <input **v-bind:value="value" v-on:input="onInput"**/> 
  </div> 
</template> 

因此,我们的主要组件标记将由一些组件组成:

<div id="app" class="container"> 
  <h2>{{ title }}</h2> 
  **<add-item-component></add-item-component> 
  <items-component :items="items"></items-component>** 
  <div class="footer"> 
    <hr/> 
    **<change-title-component v-model="title"</change-title-component>** 
  </div> 
</div> 

正如您所看到的,每个模板的大部分内容都是对应 HTML 代码的简单复制粘贴。

然而,也有一些显著的不同之处。例如,列表项模板略有改变。您已经学习并在以前使用了v-for指令。在以前的示例中,我们将此指令与<li>等 HTML 元素一起使用。现在您可以看到它也可以与 Vue 自定义组件一起使用。

您可能还注意到更改标题模板中的一个小差异。现在它有一个绑定的值,并发出onInput方法绑定到v-on:input修饰符。正如您在上一节中学到的,子组件不能直接影响父组件的数据,这就是为什么我们必须使用事件系统的原因。

定义和注册所有组件

看一下我们以前的购物清单应用程序中的 JavaScript 代码:jsfiddle.net/chudaol/c8LjyenL/。让我们添加创建 Vue 组件的代码。我们将使用已定义模板的 ID 作为它们的template属性。还要不要忘记props属性,以从父应用程序传递属性。因此,我们添加以下代码:

//add item component 
Vue.component('add-item-component', { 
  template: '**#add-item-template**', 
  data: function () { 
    return { 
      **newItem**: '' 
    } 
  } 
}); 
//item component 
Vue.component('item-component', { 
  template: '**#item-template**', 
  props: ['**item**'] 
}); 
//items component 
Vue.component('items-component', { 
  template: '**#items-template**', 
  props: ['**items**'] 
}); 
//change title component 
Vue.component('change-title-component', { 
  template: '**#change-title-template**', 
  props: ['**value**'], 
  methods: { 
    onInput: function (event) { 
      this.$emit('input', event.target.value) 
    } 
  } 
}); 

正如您所看到的,在每个组件的props中,我们传递了不同的数据属性,只涉及到该组件的属性。我们还将newItem属性移动到add-item-componentdata属性中。在change-title-component中,我们添加了onInput方法,该方法发出输入事件,因此父组件中的标题受到用户在输入框中输入的任何内容的影响。

在浏览器中打开 HTML 文件。界面与之前完全相同!我们在本节中所做的所有代码可以在 JSFiddle 中找到:jsfiddle.net/chudaol/xkhum2ck/1/

练习

尽管我们的应用程序看起来与之前一样,但其功能已经丢失。它不仅不添加项目,而且还在 devtools 控制台中显示了丑陋的错误。

请使用事件发射系统将添加项目的功能带回来。

此练习的一个可能解决方案可以在附录练习解决方案中找到。

单文件组件

从旧的最佳实践中,我们知道将 HTML 与 CSS 和 JavaScript 文件分开总是一个好主意。一些现代框架如 React 正在放松并逐渐取消这一规则。如今,看到包含自己标记、样式和应用代码的小文件或组件并不会让你感到震惊。实际上,对于小组件来说,我们甚至发现这样的架构更加方便。Vue 也允许在同一个文件中定义与同一组件相关的所有内容。这种组件被称为单文件组件。

注意:

单文件 Vue 组件是一个扩展名为.vue的文件。包含这些组件的应用程序可以使用webpack vue配置构建。使用这种配置搭建应用程序的最简单方法是使用vue-cligithub.com/vuejs-templates/webpack)。

Vue 组件可以包含最多三个部分:

  • <script>

  • <template>

  • <style>

这些部分中的每一个都负责你所想的确切内容。在<template>标签中放入 HTML 模板应该负责的内容,在<script>标签中放入 Vue 组件的 JavaScript 代码、方法、数据、props 等。<style>标签应该包含给定组件的 CSS 样式。

你还记得我们的hello-component吗?在jsfiddle.net/chudaol/mf82ts9a/2/的 JSFiddle 中看一下它。

首先使用vue-cli使用webpack-simple配置搭建应用程序:

**npm install -g vue-cli vue init webpack-simple hello**

要将其重写为 Vue 组件,我们创建我们的HelloComponent.vue文件并添加以下代码:

<template> 
  <h1>{{ msg }}</h1> 
</template> 

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

请注意,我们不需要在 JavaScript 组件定义中指定模板。作为单文件组件,隐含的是应该使用的模板是在此文件中定义的模板。您可能也注意到我们在这里使用了 ES6 风格。另外,不要忘记data属性应该是一个函数而不是一个对象。

在我们的主脚本中,我们必须创建 Vue 应用程序并指示它使用HelloComponent

import Vue from 'vue' 
**import HelloComponent** from './HelloComponent.vue' 

new Vue({ 
  el: '#app', 
  **components: { HelloComponent }** 
}); 

我们的index.html标记不会改变。它仍然会调用hello-component

<body> 
  <div id="app"> 
    **<hello-component></hello-component>** 
  </div> 
  <script src="./dist/build.js"></script> 
</body> 

现在我们只需要安装npm依赖项(如果你还没有这样做)并构建应用程序:

**npm install 
npm run dev**

一旦你这样做了,你的浏览器将自动打开localhost:8080页面!

chapter3/hello文件夹中查看完整的代码。

您还可以在www.webpackbin.com/N1LbBIsLb的 webpackbin 中测试、修改、重新测试和检查hello组件。

提示

Webpackbin 是一个很好的服务,可以运行和测试使用 Webpack 构建的应用程序。尽管它仍处于测试阶段,但它是一个非常好的工具。由于它还很年轻,所以仍然存在一些小问题。例如,如果您尝试下载整个项目的软件包,它将无法构建。

IDE 的插件

Vue 的创建者和贡献者考虑到了开发人员,并为一系列现代 IDE 开发了插件。您可以在github.com/vuejs/awesome-vue#syntax-highlighting找到它们。如果您像我一样使用的是 IntelliJ 的 WebStorm IDE,请按照以下说明安装 Vue 支持插件:

  1. 转到Preferences | P****lugins

  2. 点击浏览存储库

  3. 在搜索框中输入vue

  4. 选择Vue.js,然后点击Install按钮:

IDE 的插件

安装 WebStorm IDE 的 Vue 插件

样式和范围

很明显,模板和组件的脚本只属于它。然而,样式并不适用于相同的规则。例如,尝试向我们的hello组件添加style标签,并添加 CSS 规则,使<h1>标签变成红色:

<style> 
  h1 { 
    color: red; 
  } 
</style> 

现在,当页面刷新时,可以预期Hello!标题的颜色会变成红色。现在尝试将<h1>标签添加到主index.html文件中。你可能会感到惊讶,但它也会是红色的:

<div id="app"> 
  <h1>This is a single file component demo</h1> 
  <hello-component></hello-component> 
</div> 

样式和范围

所有的<h1>标签都具有我们在组件内定义的样式

为了使样式只附加到组件的范围内,我们需要在<style>标签中指定scoped属性:

<style **scoped**> 
  h1 { 
    color: red; 
  } 
</style> 

看看页面,你会发现只有Hello!文本是红色的,其他的h1都是默认样式。

热重载

您可能已经注意到,现在我不再要求您刷新页面,而是要求您查看页面。这是因为在使用vue-cliWebpack 脚手架方法引导应用程序时,每次更改时页面都会自动刷新。这个魔法是由vue-hot-reload API 实现的,它监视应用程序的文件,并告诉浏览器在每次有变化时自动重新加载!耶!

预处理器

如果您喜欢预处理器,欢迎在您的.vue组件中使用它们。这是由于vue-loader允许使用 Webpack 加载程序。

注意

您可以在教程中找到有关vue-loader和预处理器的更多信息vue-loader.vuejs.org/en/

HTML 预处理器

为了能够在单文件 Vue 组件中使用预处理器,只需在<template>标签中添加lang属性!不要忘记安装相应的节点模块:

**npm install jade --save-dev** 

例如,在我们的hello组件模板中使用jade将会非常简单:

<template lang="jade"> 
  h1 {{ msg }} 
</template> 

CSS 预处理器

相同的逻辑也适用于 CSS 预处理器。让我们看看如何使用,例如,sass 预处理器:

<style lang="sass" scoped> 
  $red: red; 
  h1 { 
    color: $red; 
  } 
</style> 

提示

就像在前面的例子中一样,不要忘记安装相应的加载程序才能使其工作:npm install sass-loader node-sass --save-dev

JavaScript 预处理器

也可以使用任何 JavaScript 预处理器。就像在前面的两个例子中一样,只需使用lang属性指定要使用的预处理器。并且不要忘记通过npm安装它!

**> npm install coffee-loader coffee-script --save-dev 
<script lang="coffee"> 
  exports.default = data: -> 
  { msg: 'Hello!' } 
</script>** 

使用单文件组件重写我们的购物清单应用程序

现在我们已经了解了组件以及如何使用它们,还知道了使我们的代码更容易编写的好技巧,让我们回到我们的购物清单,并将其重写为单文件组件的 Vue 应用程序。为了简单设置,我们可以使用带有 Webpack 配置的vue-cli。实际上,我们已经在第二章基础知识-安装和使用中完成了。所以,只需找到这个应用程序,并准备开始工作。如果找不到,您可以轻松创建它:

**#install vue-cli if you still hadn't installed it 
$ npm install vue-cli -g 
#bootstrap the application 
$ vue init webpack shopping-list 
$ cd shopping-list 
$ npm install 
$ npm run dev** 

确保您的index.html文件看起来像下面这样:

<!DOCTYPE html> 
<html> 
  <head> 
    <meta charset="utf-8"> 
    <title>shopping-list</title> 
    <link rel="stylesheet" 
      href="https://maxcdn.bootstrapcdn.com/bootstrap/
      3.3.6/css/bootstrap.min.css"> 
  </head> 
  <body> 
    **<app></app>** 
  </body> 
</html> 

您的main.js文件应该看起来像下面这样:

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

new Vue({ 
  **el: 'app'**, 
  components: { App } 
}) 

我们现在准备创建我们的组件并用它们填充我们的应用程序。当然,您记得我们的购物清单基本上有四个组件:

  • AddItemComponent:负责向购物清单添加新项目的组件

  • ItemComponent:负责在购物清单项目列表中呈现新项目的组件

  • ItemsComponent:负责渲染和管理ItemComponent列表的组件

  • ChangeTitleComponent:负责更改列表标题的组件

让我们在components文件夹中创建它们。首先,只需在每个组件中包含三个空的部分(<template><script><style>),并在主App.vue组件中的正确位置调用它们。请在模板中放入一些内容,以便我们可以在页面上清楚地识别不同的组件。因此,我们所有四个组件的代码将如下所示:

使用单文件组件重写我们的购物清单应用程序

购物清单应用程序的所有四个组件的代码

现在打开App.vue组件。这是我们的主要组件,将所有组件组装在一起。

<template><script><style>标签中删除所有内容。我们现在将开始构建我们的应用程序。

首先,我们必须导入App.vue将使用的组件(在本例中,全部)。

提示

不要忘记,由于我们在这个应用程序中使用了 ES2015,我们可以使用 import/export 和所有其他美丽的 ES2015 功能。

<script>标签内,让我们导入组件并导出包含导入组件和返回购物清单项目的数据函数的对象:

<script> 
  import **AddItemComponent** from './components/AddItemComponent' 
  import **ItemsComponent** from './components/ItemsComponent' 
  import **ChangeTitleComponent** from './components/ChangeTitleComponent' 

  export default { 
    components: { 
      **AddItemComponent, 
      ItemsComponent, 
      ChangeTitleComponent** 
    }, 
    data () { 
      return { 
        **items: [{ text: 'Bananas', checked: true }, 
                { text: 'Apples', checked: false }]** 
      } 
    }, 
    methods: { 
      addItem (text) { 
        this.items.push({ 
          text: text, 
          checked: false 
        }) 
      } 
    } 
  } 
</script> 

我们的模板基本上可以与使用简单组件构建的购物清单应用程序中的模板相同。让我们暂时删除有关模型和数据绑定的所有内容。首先,插入负责添加项目的组件,然后是包含所有项目的组件,然后在页脚中,负责更改标题的组件。

然后,我们的模板将如下所示:

<template> 
  <div id="app" class="container"> 
    <h2>{{ title }}</h2> 
    **<add-item-component></add-item-component> 
    <items-component></items-component>** 
    <div class="footer"> 
      <hr/> 
      **<change-title-component></change-title-component>** 
    </div> 
  </div> 
</template> 

你还记得组件变量的名称是驼峰式的,当它们在模板内部使用时,应该使用 kebab-case 进行调用,对吧?好的,让我们看看它在浏览器中的样子:

使用单文件组件重写我们的购物清单应用程序

使用单文件组件构建的购物清单应用程序

看起来不太美观,对吧?让我们为每个组件填充它们的模板。

提示

我们将继续在这个应用程序中使用 Bootstrap 的 CSS 样式。在index.html文件中全局包含它:<link rel="stylesheet" href=" https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">

添加 ItemComponent

打开AddItemComponent.vue。让我们填充它的<template>。它将如下所示:

<template> 
  <div> 
    <div class="input-group"> 
      <input type="text" class="input form-control" 
        placeholder="add shopping list item"> 
      <span class="input-group-btn"> 
        <button class="btn btn-default" type="button">Add!</button> 
      </span> 
    </div> 
  </div> 
</template> 

如果你在浏览器中查看页面,你会发现它已经改变,变得更加像我们的购物清单应用程序。

配置 ItemComponent 和 ItemsComponent

现在让我们转到ItemComponent。我们只需复制并粘贴简单组件示例中的 HTML:

//ItemComponent.vue 
<template> 
  <li :class="{ 'removed': item.checked }"> 
    <div class="checkbox"> 
      <label> 
        <input type="checkbox" v-model="item.checked"> {{ item.text }} 
      </label> 
    </div> 
  </li> 
</template> 

让我们还为这个组件添加一些scoped样式。这个组件的特定样式是与<li><span>和类.removed有关的样式。让我们将它们复制并粘贴到这个组件中:

//ItemComponent.vue 
<style scoped> 
  .removed { 
    color: gray; 
  } 
  .removed span { 
    text-decoration: line-through; 
  } 
  li { 
    list-style-type: none; 
  } 
  li span { 
    margin-left: 5px; 
  } 
</style> 

现在打开ItemsComponents。你记得,它是ItemComponent元素的列表。即使你不记得,我猜这个组件名称的复数特征就表明了这一点。为了能够使用ItemComponent,它必须导入并在components属性中注册。所以,让我们先修改脚本:

//ItemsComponent.vue 
<script> 
  import **ItemComponent** from './ItemComponent' 

  export default { 
    components: { 
      **ItemComponent** 
    } 
  } 
</script> 

现在你可以在<template>中使用item-component了!你还记得如何在vue.js中进行迭代吗?当然记得!这就是为什么你现在打开<template>标签并编写以下代码:

//temsComponent.vue 
<template> 
  <div> 
    <item-component v-for="item in items" :item="item">
    </item-component> 
  </div> 
</template> 

如果你现在检查页面,你会惊讶地发现事情实际上并没有工作。网页控制台充满了错误。你能想出原因吗?

你还记得当子组件想要访问父组件的数据时,它们必须在组件初始化时声明“props”吗?这正是我们在ItemsComponentItemComponent的声明中忘记的事情。

首先,在App.vue中,将 items 绑定到items-component调用:

//App.vue  
<items-component **:items="items"**></items-component> 

然后将props属性添加到ItemsComponent中:

//ItemsComponent.vue 
<script> 
  import ItemComponent from './ItemComponent' 

  export default { 
    components: { 
      ItemComponent 
    }, 
    **props: ['items']** 
  } 
</script> 

现在回到ItemComponent并添加props属性:

//temComponent.vue 
<script> 
  export default { 
    props: ['item'] 
  } 
</script>  

现在检查页面。现在它确实包含了物品列表,并且在我们第一次创建它时几乎具有相同的外观和感觉。在chapter3/shopping-list文件夹中检查此部分的完整代码。

练习

完成购物清单应用程序,使其具有与以前相同的功能。

剩下的不多了,我相信你会在不到半个小时内完成它。这个练习的可能解决方案可以在附录中找到,练习的解决方案

使用单文件组件重写番茄钟应用程序

我希望你还记得并可能甚至使用了我们在本书第一章开发的番茄钟应用程序。

我现在想重新审视它,并做与上一节相同的练习——定义应用程序的组件并使用这些组件重写它。

让我们来看看我们的番茄钟应用程序。现在我要给你一个惊喜:我将包含一个屏幕截图,其中包含在休息时间显示的小猫,使用thecatapi.com/api

使用单文件组件重写番茄钟应用程序

番茄钟应用程序处于休息状态

有一些容易识别的组件:

  • 控制组件(开始,暂停,结束)的组件,让我们称之为**ControlsComponent**

  • 倒计时组件,**CowntdownComponent**

  • 当前状态的标题组件(Work!/Rest!),**StateTitleComponent**

  • 取决于状态(工作或休息)的小猫渲染组件**KittensComponent**(这是我最喜欢的!)

现在,请停止盯着小猫,让我们开始使用单文件组件来实现我们的番茄钟应用程序!一些用于搭建应用程序的第一步如下:

  1. 首先打开前一章中的脚手架式番茄钟应用程序,或者基于 Webpack 模板创建一个新的应用程序。

  2. application文件夹中运行npm installnpm run dev

  3. 确保你的index.html看起来像下面这样:

      <!DOCTYPE html> 
      <html> 
        <head> 
          <meta charset="utf-8"> 
          <title>pomodoro</title> 
        </head> 
        <body> 
          <app></app> 
        </body> 
      </html> 

  1. 确保你的main.js文件看起来像下面这样:
      import Vue from 'vue' 
      import App from './App' 

      /* eslint-disable no-new */ 
      new Vue({ 
        el: 'app', 
        components: { App } 
      }) 

  1. 打开你的浏览器到页面localhost:8080

  2. 然后,就像在之前的例子中一样,转到components文件夹并创建所有必要的.vue组件。

  3. 转到App.vue,并导入和注册所有创建的组件。

  4. 在每个组件的<template>部分,放入一些可以唯一标识它的东西,这样我们在检查页面时可以轻松识别它。

你几乎肯定会看到结构和初始代码,看起来像下面这样:

使用单文件组件重写番茄钟应用程序

使用单文件组件实现的番茄钟应用程序的最初状态

现在,假设我们的组件已经准备好使用,让我们将它们放在应用程序的布局中应该放置的位置。

我只是稍微提醒一下整个应用程序的标记之前是什么样子的:

<div id="app" class="container"> 
  <h2> 
    <span>Pomodoro</span> 
    **// Looks like our ControlsComponent** 
    <button > 
      <i class="glyphicon glyphicon-play"></i> 
    </button> 
    <button > 
      <i class="glyphicon glyphicon-pause"></i> 
    </button> 
    <button > 
      <i class="glyphicon glyphicon-stop"></i> 
    </button> 
  </h2> 
  **// Looks like our StateTitleComponent** 
  <h3>{{ title }}</h3> 
  **// Looks like our CountdownComponent** 
  <div class="well"> 
    <div class="pomodoro-timer"> 
      <span>{{ min }}</span>:<span>{{ sec }}</span> 
    </div> 
  </div> 
  **// Looks like our KittensComponent** 
  <div class="well"> 
    <img :src="catImgSrc" /> 
  </div> 
</div> 

你可能已经注意到,我删除了一些负责类绑定或操作处理程序的部分。不要担心。还记得《飘》中的斯嘉丽·奥哈拉吗?她过去常说,

“我现在不能考虑那个。我明天再想。”

goo.gl/InYm8e)。斯嘉丽·奥哈拉是一个聪明的女人。像斯嘉丽·奥哈拉一样。目前,我们将只关注App.vue<template>标签。其他的东西以后再说。现在我们基本上可以复制并粘贴这个 HTML 片段,并替换我们识别出来的部分,比如组件和它们的 kebab-case 名称。因此,App.vue中的模板将如下所示:

//App.vue 
<template> 
  <div id="app" class="container"> 
    <h2> 
      <span>Pomodoro</span> 
      **<controls-component></controls-component>** 
    </h2> 
    **<state-title-component></state-title-component> 
    <countdown-component></countdown-component> 
    <kittens-component></kittens-component>** 
  </div> 
</template> 

有点小了,对吧?打开你的应用程序的浏览器检查一下。不是很漂亮,肯定与我们的番茄钟应用程序无关,但是...它起作用!

使用单文件组件重写番茄钟应用程序

番茄钟应用程序作为单文件组件应用程序引导

现在我们该怎么办?将相应的标记复制到它们组件的<template>部分。请自己进行这个小小的复制粘贴,让它成为一个小小的家庭练习。但是,如果你想检查一下自己,可以看一下chapter3/pomodoro文件夹。目前就是这样了!所有的数据绑定和有趣的东西将在下一章中出现。所以不要关闭这本书。但是,不要忘记休息一下。

CSS 过渡的响应式绑定

在*转到下一章之前,我们将详细讨论不同类型的数据绑定,我想给你一点有趣的味道,即可以绑定的一些有趣的东西。我知道你非常关注文字,亲爱的读者。所以,到目前为止,你已经发现了两次过渡这个词,你可能已经猜到我们实际上可以将 CSS 过渡绑定到数据变化上。

因此,想象一下,如果您有一个元素,只有在data属性showtrue时才应该显示。这很容易,对吧?您已经了解了v-if指令:

<div v-if="show">hello</div> 

因此,每当show属性发生变化时,这个<div>就会相应地行为。想象一下,在隐藏/显示时,您希望应用一些 CSS 过渡。使用 Vue,您可以使用特殊的transition包装组件来指定数据更改时要使用的过渡:

<transition name="fade">  
  <div v-if="show" transition="my">hello</div> 
</transition> 

之后,您只需为fade-enterfade-leavefade-enter-activefade-leave-active类定义 CSS 规则。查看有关这些类的官方 Vue 文档页面vuejs.org/v2/guide/transitions.html#Transition-Classes

让我们看看在我们的kittens组件示例中它是如何工作的。首先,让我们在App.vue中的kittens-component中添加v-if指令:

<template> 
  <...> 
  <kittens-component **v-if="kittens"**></kittens-component> 
  <...> 
</template> 

此外,我们应该在App.vue<script>标签中添加data函数(还要使其全局,以便我们可以从 devtools 控制台修改它):

<script> 
// ... // 
window.data = { 
  kittens: true 
}; 

export default { 
  //.....// 
  data () { 
    return window.data 
  } 
} 
</script> 

查看浏览器:一切似乎没有改变。打开 devtools 控制台,输入以下内容:

data.kittens = false 

您将看到kittens组件将从页面中消失。如果您输入以下内容,它将再次出现:

data.kittens = true 

提示

我希望您没有忘记在主index.html文件中包含 Bootstrap 的 CSS。如果没有,您将看不到任何出现/消失,因为我们的<div>标签既没有信息也没有应用任何类:<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">

但是,我们正在谈论CSS过渡,而不是简单地隐藏/显示东西。现在让我们将 CSSfade过渡应用到我们的kittens组件上。只需添加一个带有名称属性fade的包装组件transition

<template> 
  <...> 
  **<transition name="fade">** 
    <kittens-component v-if="kittens"></kittens-component> 
  **</transition>** 
  <...> 
</template> 

现在,如果我们为正确的类定义良好的规则,我们将看到一个漂亮的 CSS 过渡。让我们来做吧。在<style>标签内添加以下 CSS 规则:

<style scoped> 
  **.fade-enter-active, .fade-leave-active** { 
    transition: opacity .5s 
  } 
  **.fade-enter, .fade-leave-active** { 
    opacity: 0 
  } 
</style> 

再次查看页面。打开控制台,输入data.kittens = falsedata.kittens = true。现在您可以看到在每次数据更改时发生漂亮的fade过渡。在下一章中,我们将更多地讨论 Vue.js 中的过渡,并将其应用到我们的应用程序中。

摘要

在本章中,您了解了 Vue 组件以及如何使用它们。您看到了如何使用经典方法(使用 HTML、CSS 和 JavaScript 的应用程序)创建和注册它们,还看到了使用单文件组件方法创建和操作它们是多么容易。需要记住的事情:

  • 虽然变量是使用驼峰格式创建的,但为了能够在模板内部使用组件,你必须应用相应的短横线格式,例如,MyBeautifulComponent -> my-beautiful-component

  • 组件内部的属性datael必须是函数而不是对象:{data: function () {}}

  • 如果你不希望组件的样式泄漏到全局范围,请给它添加一个scoped属性:<style scoped></style>

我们还使用单文件组件重写了我们的应用程序,并稍微涉及了数据绑定到 CSS 过渡。

在下一章中,我们将深入探讨所有类型的数据绑定,包括 CSS 和 JavaScript 过渡。我们将使用数据绑定使我们的应用程序重焕生机。最后但并非最不重要的是,我们将看到更多的猫!

第四章:反应性-将数据绑定到您的应用程序

在上一章中,您学习了 Vue.js 中最重要的概念之一:组件。您看到了如何创建组件,如何注册,如何调用以及如何使用和重用它们。您还学习了单文件组件的概念,甚至在购物清单和番茄钟应用程序中使用了它们。

在本章中,我们将深入探讨数据绑定的概念。我们之前已经谈论过它,所以你已经很熟悉了。我们将以所有可能的方式在我们的组件中绑定数据。

总之,在本章中,我们将:

  • 重新审视数据绑定语法

  • 在我们的应用程序中应用数据绑定

  • 遍历元素数组,并使用相同的模板渲染每个元素

  • 重新审视并应用数据和事件绑定的速记方式在我们的应用程序中

重新审视数据绑定

我们从第一章开始就一直在谈论数据绑定和反应性。所以,你已经知道数据绑定是一种从数据到可见层以及反之的变化传播机制。在本章中,我们将仔细审视所有不同的数据绑定方式,并在我们的应用程序中应用它们。

插入数据

让我们想象一下以下的 HTML 代码:

<div id="hello"></div> 

还想象以下 JavaScript 对象:

var data = { 
  msg: 'Hello' 
}; 

我们如何在页面上呈现数据条目的值?我们如何访问它们,以便我们可以在 HTML 中使用它们?实际上,在过去的两章中,我们已经在 Vue.js 中大量做了这个。理解并一遍又一遍地做这件事并没有问题。

“重复是学习之母”

如果您已经是数据插值的专业人士,只需跳过本节,然后继续表达式和过滤器。

那么,我们应该怎么做才能用msg的值填充<div>?如果我们按照老式的 jQuery 方式,我们可能会做类似以下的事情:

$("#hello").text(data.msg); 

但是,在运行时,如果您更改msg的值,并且希望这种更改传播到 DOM,您必须手动执行。仅仅改变data.msg的值,什么也不会发生。

例如,让我们编写以下代码:

var data = { 
  msg: 'Hello' 
}; 
$('#hello').text(data.msg); 
data.msg = 'Bye'; 

然后出现在<div>中的文本将是Hello。在jsfiddle.net/chudaol/uevnd0e4/上检查这个 JSFiddle。

使用 Vue,最简单的插值是用{{ }}(句柄注释)完成的。在我们的示例中,我们将编写以下 HTML 代码:

<div id="hello">**{{ msg }}**</div> 

因此,<div>的内容将与msg数据绑定。每次msg更改时,div的内容都会自动更改其内容。请查看jsfiddle.net/chudaol/xuvqotmq/1/上的 jsfiddle 示例。Vue 实例化后,data.msg也会更改。屏幕上显示的值是新的值!

这仍然是单向绑定的插值。如果我们在 DOM 中更改值,数据将不会发生任何变化。但是,如果我们只需要数据的值出现在 DOM 中,并相应地更改,这是一种完美有效的方法。

此时,应该非常清楚,如果我们想在模板中使用data对象的值,我们应该用{{}}将它们括起来。

让我们向我们的番茄钟应用程序添加缺失的插值。请在chapter4/pomodoro文件夹中检查当前情况。如果您运行npm run dev并查看打开的页面,您将看到页面如下所示:

插入数据

我们番茄钟应用程序中缺少的插值

从对页面的第一眼扫视中,我们能够确定那里缺少什么。

页面缺少计时器、小猫、番茄钟状态的标题(显示工作!休息!)、以及根据番茄钟状态显示或隐藏小猫占位符的逻辑。让我们首先添加番茄钟状态的标题和番茄钟计时器的分钟和秒。

添加番茄钟状态的标题

首先,我们应该决定这个元素应该属于哪个组件。看看我们的四个组件。很明显,它应该属于StateTitleComponent。如果您查看以下代码,您将看到它实际上已经在其模板中插值了标题:

//StateTitleComponent.vue 
<template> 
  <h3>**{{ title }}**</h3> 
</template> 

<style scoped> 
</style> 

<script> 
</script> 

好!在上一章中,我们已经完成了大部分工作。现在我们只需要添加必须被插值的数据。在这个组件的<script>标签中,让我们添加带有title属性的data对象。现在,让我们将其硬编码为可能的值之一,然后决定如何更改它。你更喜欢什么?工作! 还是 休息!?我想我知道答案,所以让我们将以下代码添加到我们的script标签中:

//StateTitleComponent.vue 
<script> 
  export default { 
    data () { 
      return { 
        **title: 'Learning Vue.js!'** 
      } 
    } 
  } 
</script> 

现在就让它保持这样。我们将在以后的方法和事件处理部分回到这个问题。

练习

以与我们添加 Pomodoro 状态标题相同的方式,请将分钟和秒计时器计数器添加到CountDownComponent中。它们现在可以是硬编码的。

使用表达式和过滤器

在前面的例子中,我们在{{}}插值中使用了简单的属性键。实际上,Vue 在这些花括号中支持更多的内容。让我们看看在那里可能做些什么。

表达式

这可能听起来出乎意料,但 Vue 支持在数据绑定括号内使用完整的 JavaScript 表达式!让我们去 Pomodoro 应用程序的任何一个组件,并在模板中添加任何 JavaScript 表达式。你可以在chapter4/pomodoro2文件夹中进行一些实验。

例如,尝试打开StateTitleComponent.vue文件。让我们在其模板中添加一些 JavaScript 表达式插值,例如:

{{ Math.pow(5, 2) }} 

实际上,你只需要取消注释以下行:

//StateTitleComponent.vue 
<!--<p>--> 
  <!--{{ Math.pow(5, 2) }}--> 
<!--</p>--> 

你将在页面上看到数字25。很好,不是吗?让我们用 JavaScript 表达式替换 Pomodoro 应用程序中的一些数据绑定。例如,在CountdownComponent组件的模板中,每个用于minsec的指令可以被一个表达式替换。目前它看起来是这样的:

//CountdownComponent.vue 
<template> 
  <div class="well"> 
    <div class="pomodoro-timer"> 
      **<span>{{ min }}</span>:<span>{{ sec }}</span>** 
    </div> 
  </div> 
</template> 

我们可以用以下代码替换它:

//CountdownComponent.vue 
<template> 
  <div class="well"> 
    <div class="pomodoro-timer"> 
      <span>{{ min + ':' + sec }}</span> 
    </div> 
  </div> 
</template> 

还有哪些地方可以添加一些表达式呢?让我们看看StateTitleComponent。此刻,我们使用的是硬编码的标题。然而,我们知道它应该以某种方式依赖于番茄钟的状态。如果它处于“工作”状态,它应该显示Work!,否则应该显示Rest!。让我们创建这个属性并将其命名为isworking,然后将其分配给主App.vue组件,因为它似乎属于全局应用状态。然后我们将在StateTitleComponent组件的props属性中重用它。因此,打开App.vue,添加布尔属性isworking并将其设置为true

//App.vue 
<...> 
window.data = { 
  kittens: true, 
  **isworking: true** 
}; 

export default { 
  <...> 
  data () { 
    return window.data 
  } 
} 

现在让我们在StateTitleComponent中重用这个属性,在每个可能的标题中添加两个字符串属性,并最后在模板中添加表达式,根据当前状态有条件地渲染一个标题或另一个标题。因此,组件的脚本将如下所示:

//StateTitleComponent.vue 
<script> 
  export default { 
    data () { 
      return { 
        **workingtitle: 'Work!', 
        restingtitle: 'Rest!'** 
      } 
    }, 
    **props: ['isworking']** 
  } 
</script> 

现在我们可以根据isworking属性有条件地渲染一个标题或另一个标题。因此,StateTitleComponent的模板将如下所示:

<template> 
  <div> 
    <h3> 
      {{ isworking ? workingtitle : restingtitle }} 
    </h3> 
  </div> 
</template> 

看一下刷新后的页面。奇怪的是,它显示Rest!作为标题。如果App.vue中的isworking属性设置为true,那么这是怎么发生的?我们只是忘记在App.vue模板中的组件调用上绑定这个属性!打开App.vue组件,并在state-title-component调用上添加以下代码:

<state-title-component **v-bind:isworking="isworking"**></state-title-component> 

现在,如果你查看页面,正确的标题会显示为Work!。如果你打开开发工具控制台并输入data.isworking = false,你会看到标题改变。

如果isworking属性为false,标题为Rest!,如下截图所示:

Expressions

如果isworking属性为true,标题为Work!,如下截图所示:

Expressions

过滤器

除了花括号内的表达式之外,还可以使用应用于表达式结果的过滤器。过滤器只是函数。它们是由我们创建的,并且通过使用管道符号|应用。如果你创建一个将字母转换为大写的过滤器并将其命名为uppercase,那么要应用它,只需在双大括号内的管道符号后面使用它:

<h3> {{ title | lowercase }} </h3> 

你可以链接尽可能多的过滤器,例如,如果你有过滤器ABC,你可以做类似{{ key | A | B | C }}的事情。过滤器是使用Vue.filter语法创建的。让我们创建我们的lowercase过滤器:

//main.js 
Vue.filter('lowercase', (key) => { 
  return key.toLowerCase() 
}) 

让我们将其应用到主App.vue组件中的 Pomodoro 标题。为了能够使用过滤器,我们应该将'Pomodoro'字符串传递到句柄插值符号内。我们应该将它作为 JavaScript 字符串表达式传递,并使用管道符号应用过滤器:

<template> 
  <...> 
    <h2> 
      <span>**{{ 'Pomodoro' | lowercase }}**</span> 
      <controls-component></controls-component> 
    </h2> 
  <...> 
</template> 

检查页面;Pomodoro标题实际上将以小写语法显示。

让我们重新审视我们的CountdownTimer组件并查看计时器。目前,只有硬编码的值,对吧?但是当应用程序完全功能时,值将来自某些计算。值的范围将从 0 到 60。计时器显示20:40是可以的,但少于十的值是不可以的。例如,当只有 1 分钟和 5 秒时,它将是1:5,这是不好的。我们希望看到类似01:05的东西。所以,我们需要leftpad过滤器!让我们创建它。

转到main.js文件,并在大写过滤器定义之后添加一个leftpad过滤器:

//main.js 
Vue.filter(**'leftpad'**, (value) => { 
  if (value >= 10) { 
    return value 
  } 
  return '0' + value 
}) 

打开CountdownComponent组件,让我们再次将minsec拆分到不同的插值括号中,并为每个添加过滤器:

//CountdownComponent.vue 
<template> 
  <div class="well"> 
    <div class="pomodoro-timer"> 
      <span>**{{ min | leftpad }}:{{ sec | leftpad }}**</span> 
    </div> 
  </div> 
</template> 

用 1 和 5 替换数据中的minsec,然后查看。数字出现了前面的"0"!

练习

创建两个过滤器,大写addspace,并将它们应用到标题Pomodoro:

  • 大写过滤器必须做到它所说的那样

  • addspace过滤器必须在给定的字符串值右侧添加一个空格

不要忘记Pomodoro不是一个关键字,所以在插值括号内,它应该被视为一个字符串!在这个练习之前和之后的标题看起来应该是这样的:

练习

在应用过滤器大写和添加空格之前和之后的 Pomodoro 应用程序的标题

自己检查:查看chapter4/pomodoro3文件夹。

重新审视和应用指令

在上一节中,我们看到了如何插值应用程序的数据以及如何将其绑定到可视层。尽管语法非常强大,并且提供了高可能性的数据修改(使用过滤器和表达式),但它也有一些限制。例如,尝试使用 {{}} 符号来实现以下内容:

  • 在用户输入中使用插值数据,并在用户在输入中键入时将更改应用到相应的数据

  • 将特定元素的属性(例如 src)绑定到数据

  • 有条件地渲染一些元素

  • 遍历数组并渲染一些组件与数组的元素

  • 在元素上创建事件监听器

让我们至少尝试第一个。例如,打开购物清单应用程序(在 chapter4/shopping-list 文件夹中)。在 App.vue 模板中创建一个 input 元素,并将其值设置为 {{ title }}

<template> 
  <div id="app" class="container"> 
    <h2>{{ title }}</h2> 
    **<input type="text" value="{{ title }}">** 
    <add-item-component></add-item-component> 
    <...> 
  </div> 
</template> 

哦不!到处都是错误。已删除属性内的插值,它说。这是否意味着在 Vue 2.0 之前,您可以轻松地在属性内使用插值?是的,也不是。如果您在属性内使用插值,您将不会收到错误,但在输入框内更改标题将不会产生任何结果。在 Vue 2.0 中,以及在之前的版本中,为了实现这种行为,我们必须使用指令。

注意

指令是具有 v- 前缀的元素的特殊属性。为什么是 v-?因为 Vue!指令提供了一种微小的语法,比简单的文本插值提供了更丰富的可能性。它们有能力在每次数据更改时对可视层应用一些特殊行为。

使用 v-model 指令进行双向绑定

双向绑定是一种绑定类型,不仅数据更改会传播到 DOM 层,而且 DOM 中绑定数据发生的更改也会传播到数据中。要以这种方式将数据绑定到 DOM,我们可以使用 v-model 指令。

我相信您仍然记得第一章中使用 v-model 指令的方式:

<input type="text" **v-model="title"**> 

这样,标题的值将出现在输入框中,如果您在此输入框中输入内容,相应的更改将立即应用到数据,并反映在页面上所有插值的值中。

只需用 v-model 替换花括号符号,并打开页面。

尝试在输入框中输入一些内容。您将看到标题立即更改!

只记住,这个指令只能用于以下元素:

  • <input>

  • <select>

  • <textarea>

尝试所有这些然后删除这段代码。我们的主要目的是能够使用更改标题组件来更改标题。

组件之间的双向绑定

从上一章中记得,使用v-model指令不能轻松实现组件之间的双向绑定。由于架构原因,Vue 只是阻止子组件轻松地改变父级作用域。

这就是为什么我们在上一章中使用事件系统来能够从子组件更改购物清单的标题。

我们将在本章中再次进行。等到我们到达v-on指令的部分之前再等几段时间。

使用v-bind指令绑定属性

v-bind指令允许我们将元素的属性组件属性绑定到一个表达式。为了将其应用于特定属性,我们使用冒号分隔符:

v-bind:attribute 

例如:

  • v-bind:src="src"

  • v-bind:class="className"

任何表达式都可以写在""内。数据属性也可以像之前的例子一样使用。让我们在我们的 Pomodoro 应用程序中的KittenComponent中使用thecatapi作为来源添加小猫图片。从chapter4/pomodoro3文件夹打开我们的 Pomodoro 应用程序。

打开KittenComponent,将catimgsrc添加到组件的数据中,并使用v-bind语法将其绑定到图像模板的src属性:

<template> 
  <div class="well"> 
    <img **v-bind:src="catImgSrc"** /> 
  </div> 
</template> 

<style scoped> 
</style> 

<script> 
  export default { 
    data () { 
      return { 
        **catimgsrc: "http://thecatapi.com/api/images/get?size=med"** 
      } 
    } 
  } 
</script> 

打开页面。享受小猫!

使用 v-bind 指令绑定属性

应用了源属性的 Pomodoro KittenComponent

使用 v-if 和 v-show 指令进行条件渲染

如果您在前面的部分中已经付出了足够的注意,并且如果我要求您有条件地渲染某些内容,您实际上可以使用插值括号{{ }}内的 JavaScript 表达式来实现。

但是,尝试有条件地渲染某个元素或整个组件。这可能并不像在括号内应用表达式那么简单。

v-if指令允许有条件地渲染整个元素,这个元素也可能是一个组件元素,取决于某些条件。条件可以是任何表达式,也可以使用数据属性。例如,我们可以这样做:

<div v-if="1 < 5">hello</div> 

或者:

<div v-if="Math.random() * 10 < 6">hello</div> 

或者:

<div v-if="new Date().getHours() >= 16">Beer Time!</div> 

或者使用组件的数据:

<template> 
  <div> 
    <h1 **v-if="!isadmin"**>Beer Time!</h1> 
  </div> 
</template> 
<script> 
  export default { 
    data () { 
      return { 
        **isadmin: false** 
      } 
    } 
  } 
</script> 

v-show属性做的是同样的工作。唯一的区别是,v-if根据条件渲染或不渲染元素,而v-show属性总是渲染元素,只是在条件结果为false时应用display:none CSS 属性。让我们来看看区别。在chapter4/beer-time文件夹中打开beer-time项目。运行npm installnpm run dev。打开App.vue组件,尝试使用true/false值,并尝试用v-show替换v-if。打开 devtools 并检查elements标签页。

让我们首先检查在isadmin属性值中使用v-if切换truefalse时的外观。

当条件满足时,一切都如预期般出现;元素被渲染并出现在页面上:

使用 v-if 和 v-show 指令进行条件渲染

使用v-if指令进行条件渲染。条件满足。

当条件不满足时,元素不会被渲染:

使用 v-if 和 v-show 指令进行条件渲染

使用v-if指令进行条件渲染。条件不满足。

请注意,当条件不满足时,相应的元素根本不会被渲染!

使用v-show指令来玩弄条件结果值。当条件满足时,它的外观与使用v-if的前一种情况完全相同:

使用 v-if 和 v-show 指令进行条件渲染

使用v-show指令进行条件渲染。条件满足。

现在让我们来看看当条件不满足时,使用v-show指令的元素会发生什么:

使用 v-if 和 v-show 指令进行条件渲染

使用v-show指令进行条件渲染。条件不满足。

在这种情况下,当条件满足时,一切都是一样的,但当条件不满足时,元素也会被渲染,使用display:none CSS 属性。

你如何决定使用哪一个更好?在第一次渲染时,如果条件不满足,v-if指令将根本不渲染元素,从而减少初始渲染时的计算成本。但是,如果属性在运行时频繁更改,渲染/移除元素的成本高于仅应用display:none属性。因此,对于频繁更改的属性,请使用v-show,对于在运行时不会太多更改的条件,请使用v-if

让我们回到我们的番茄钟应用程序。当番茄钟不处于工作状态时,应该有条件地呈现KittensComponent。因此,打开chapter4/pomodoro4文件夹中的 Pomodoro 应用程序代码。

你认为应该使用什么?v-if还是v-show?让我们分析一下。无论我们使用什么,这个元素在初始渲染时都应该可见吗?答案是否定的,因为在初始渲染时,用户开始工作并启动番茄钟计时器。也许最好使用v-if,以免在没有必要时产生初始渲染的成本。但是,让我们分析另一个因素——使小猫组件可见/不可见的状态切换的频率。这将在每个番茄钟间隔发生,对吧?在工作 15-20 分钟后,然后在 5 分钟的休息间隔后,实际上并不那么频繁,不会对渲染造成太大影响。在这种情况下,在我看来,无论你使用哪种,都无所谓。让我们使用v-show。打开App.vue文件,并将v-show指令应用于kittens-component的调用:

<template> 
  <div id="app" class="container"> 
    <...> 
    <transition name="fade"> 
      <kittens-component **v-show="!isworking"**></kittens-component> 
    </transition> 
  </div> 
</template> 

打开页面,尝试在 devtools 控制台中切换data.isworking的值。您将看到小猫容器的出现和消失。

使用 v-for 指令进行数组迭代

你可能记得,数组迭代是使用v-for指令完成的,具体语法如下:

<div v-for item in items> 
  item 
</div> 

或者使用组件:

<component v-for item in items v-bind:**componentitem="item"**></component> 

对于数组中的每个项目,这将呈现一个组件,并将组件的item属性绑定到项目的值。当然,你记得在绑定语法的""内部,你可以使用任何 JavaScript 表达式。所以,要有创意!

提示

不要忘记,在绑定语法(componentitem)中使用的属性应该存在于组件的数据中!

例如,看看我们的购物清单应用程序(chapter4/shopping-list文件夹)。它已经在ItemsComponent中使用了v-for语法来渲染物品列表:

<template> 
  <ul> 
    <item-component **v-for="item in items"** :item="item"></item-component> 
  </ul> 
</template> 

ItemComponent,反过来,使用props声明了item属性:

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

现在,让我们用我们的购物清单应用程序做一些有趣的事情。到目前为止,我们只处理了一个购物清单。想象一下,你想为不同类型的购物准备不同的购物清单。例如,你可能有一个常规的购物清单,用于正常的杂货购物日。你可能有一个不同的购物清单用于假期。当你买新房子时,你可能也想有一个不同的购物清单。让我们利用 Vue 组件的可重用性,将我们的购物清单应用程序转换为购物清单列表!我们将使用 Bootstrap 的选项卡面板来显示它们;有关更多信息,请参考getbootstrap.com/javascript/#tabs

在 IDE 中打开您的购物清单应用程序(chapter4/shopping-list文件夹)。

首先,我们应该添加 Bootstrap 的 JavaScript 文件和 jQuery,因为 Bootstrap 依赖它来进行其惊人的魔术。继续手动将它们添加到index.html文件中:

  <body> 
    <...> 
    <script src="https://code.jquery.com/jquery-3.1.0.js"></script> 
    <script 
      src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"> 
    </script> 
    <...> 
  </body> 

现在,让我们逐步概述一下,我们应该做些什么,以便将我们的应用程序转换为购物清单列表:

  1. 首先,我们必须创建一个新组件。让我们称之为ShoppingListComponent,并将我们当前的App.vue内容移动到那里。

  2. 我们的新ShoppingListComponent应该包含props属性,其中包括它将从App.vue接收的titleitems

  3. ItemsComponent应该从props属性接收items,而不是硬编码它。

  4. App组件的data中,让我们声明并硬编码(暂时)一个shoppinglists数组,每个项目应该有一个标题,一个物品数组和一个 ID。

  5. App.vue应该导入ShoppingListComponent,并在模板中遍历shoppinglists数组,并为每个构建html/jade结构的选项卡面板。

好的,那么,让我们开始吧!

创建 ShoppingListComponent 并修改 ItemsComponent

components文件夹内,创建一个新的ShoppingListComponent.vue。将App.vue文件的内容复制粘贴到这个新文件中。不要忘记声明将包含titleitemsprops,并将items绑定到模板内的items-component调用。此组件的最终代码应该类似于以下内容:

//ShoppingListComponent.vue 
<template> 
  **<div>** 
    <h2>{{ title }}</h2> 
    <add-item-component></add-item-component> 
    **<items-component v-bind:items="items"></items-component>** 
    <div class="footer"> 
      <hr /> 
      <change-title-component></change-title-component> 
    **</div>** 
  </div> 
</template> 

<script> 
  import AddItemComponent from './AddItemComponent' 
  import ItemsComponent from './ItemsComponent' 
  import ChangeTitleComponent from './ChangeTitleComponent' 

  export default { 
    components: { 
      AddItemComponent, 
      ItemsComponent, 
      ChangeTitleComponent 
    } 
    **props: ['title', 'items']** 
  } 
</script> 

<style scoped> 
  **.footer { 
    font-size: 0.7em; 
    margin-top: 20vh; 
  }** 
</style> 

请注意,我们删除了父div的容器样式和容器的class的部分。这部分代码应该留在App.vue中,因为它定义了全局应用程序的容器样式。不要忘记props属性和将props绑定到items-component

打开ItemsComponent.vue,确保它包含带有itemsprops属性:

<script> 
  <...> 
  export default { 
    **props: ['items'],** 
    <...> 
  } 
</script> 

修改 App.vue

现在转到App.vue。删除<script><template>标签内的所有代码。在script标签中,导入ShoppingListComponent并在components属性内调用它:

//App.vue 
<script> 
  import **ShoppingListComponent** from './components/ShoppingListComponent' 

  export default { 
    **components: { 
      ShoppingListComponent 
    }** 
  } 
</script> 

添加一个data属性并在那里创建一个shoppinglists数组。为该数组添加任意数据。该数组的每个对象应该具有idtitleitems属性。正如你记得的那样,items必须包含checkedtext属性。例如,你的data属性可能如下所示:

//App.vue 
<script> 
  import ShoppingListComponent from './components/ShoppingListComponent' 

  export default { 
    components: { 
      ShoppingListComponent 
    }, 
    **data () { 
      return { 
        shoppinglists: [ 
          { 
            id: 'groceries', 
            title: 'Groceries', 
            items: [{ text: 'Bananas', checked: true }, 
                    { text: 'Apples', checked: false }] 
          }, 
          { 
            id: 'clothes', 
            title: 'Clothes', 
            items: [{ text: 'black dress', checked: false }, 
                    { text: 'all stars', checked: false }] 
          } 
        ] 
      } 
    }** 
  } 
</script> 

比我更有创意:添加更多的清单,更多的项目,一些漂亮有趣的东西!

现在让我们为基于购物清单的迭代创建组合 bootstrap 标签面板的结构!让我们首先定义标签工作所需的基本结构。让我们添加所有必要的类和 jade 结构,假装我们只有一个元素。让我们还用大写锁定写出所有将从我们的购物清单数组中重复使用的未知部分:

//App.vue 
<template> 
  <div id="app" class="container"> 
    <ul class="nav nav-tabs" role="tablist"> 
      <li role="presentation"> 
        <a href="**ID**" aria-controls="**ID**" role="tab" data-toggle="tab">**TITLE**</a> 
      </li> 
    </ul> 
    <div class="tab-content"> 
      <div class="tab-pane" role="tabpanel" id="**ID**"> 
        **SHOPPING LIST COMPONENT** 
      </div> 
    </div> 
  </div> 
</template> 

有两个元素需要在购物清单数组上进行迭代——包含<a>属性的<li>标签和tab-pane div。在第一种情况下,我们必须将每个购物清单的 ID 绑定到hrefaria-controls属性,并插入标题。在第二种情况下,我们需要将id属性绑定到id属性,并呈现购物清单项目并将items数组和title绑定到它。简单!让我们开始。首先向每个元素添加v-for指令(对<li>tab-pane div元素):

//App.vue 
<template> 
  <div id="app" class="container"> 
    <ul class="nav nav-tabs" role="tablist"> 
      <li **v-for="list in shoppinglists"** role="presentation"> 
        <a href="ID" aria-controls="ID" role="tab" data-
          toggle="tab">TITLE</a> 
      </li> 
    </ul> 
    <div class="tab-content"> 
      <div **v-for="list in shoppinglists"** class="tab-pane" 
        role="tabpanel" 
        id="ID"> 
        **SHOPPING LIST COMPONENT** 
      </div> 
    </div> 
  </div> 
</template> 

现在用正确的绑定替换大写锁定部分。记住,对于bind属性,我们使用v-bind:<corresponding_attribute>="expression"语法。

对于锚元素的href属性,我们必须定义一个表达式,将 ID 选择器#附加到id: v-bind:href="'#' + list.id"aria-controls属性应该绑定到 ID 的值。title可以使用简单的{{ }}符号插值进行绑定。

对于shopping-list-component,我们必须将titleitems绑定到列表项的相应值。你还记得我们在ShoppingListComponentprops中定义了titleitems属性吗?因此,绑定应该看起来像v-bind:title=list.titlev-bind:items=list.items

因此,在适当的绑定属性之后,模板将如下所示:

//App.vue 
<template> 
  <div id="app" class="container"> 
    <ul class="nav nav-tabs" role="tablist"> 
      <li v-for="list in shoppinglists" role="presentation"> 
        <a **v-bind:href="'#' + list.id" v-bind:aria-controls="list.id"**           role="tab" data-toggle="tab">**{{ list.title }}**</a> 
      </li> 
    </ul> 
    <div class="tab-content"> 
      <div v-for="list in shoppinglists" class="tab-pane" role="tabpanel"
        **v-bind:id="list.id"**> 
        **<shopping-list-component v-bind:** 
 **v-bind:items="list.items"></shopping-list-component>** 
      </div> 
    </div> 
  </div> 
</template> 

我们快完成了!如果你现在打开页面,你会看到标签的标题都出现在页面上:

修改 App.vue

修改后屏幕上看到的标签标题

如果你开始点击标签标题,相应的标签窗格将打开。但这不是我们期望看到的,对吧?我们期望的是第一个标签默认可见(活动状态)。为了实现这一点,我们应该将active类添加到第一个li和第一个tab-pane div中。但是,如果代码对所有标签都是相同的,因为我们正在遍历数组,我们该怎么做呢?

幸运的是,Vue 允许我们在v-for循环内不仅提供迭代项,还提供index,然后在模板中的表达式中重用这个index变量。因此,我们可以使用它来有条件地渲染active类,如果索引是"0"的话。在v-for循环内使用index变量就像下面这样简单:

v-for="**(list, index)** in shoppinglists" 

类绑定的语法与其他所有内容的语法相同(class也是一个属性):

**v-bind:class= "active"** 

你还记得我们可以在引号内写任何 JavaScript 表达式吗?在这种情况下,我们想要编写一个条件,评估index的值,如果是"0",则类的值是active

v-bind:class= "index===0 ? 'active' : ''" 

index变量添加到v-for修饰符和litab-pane元素的class绑定中,使得最终的模板代码看起来像下面这样:

<template> 
  <div id="app" class="container"> 
    <ul class="nav nav-tabs" role="tablist"> 
      <li **v-bind:class= "index===0 ? 'active' : 
        ''" v-for="(list, index) in shoppinglists"** role="presentation"> 
        <a v-bind:href="'#' + list.id" v-bind:aria-controls="list.id" 
          role="tab" data-toggle="tab">{{ list.title }}</a> 
      </li> 
    </ul> 
    <div class="tab-content"> 
      <div **v-bind:class= "index===0 ? 'active' : ''" 
        v-for="(list,index) in shoppinglists"** class="tab-pane" 
        role="tabpanel" v-bind:id="list.id"> 
        <shopping-list-component v-bind: 
          v-bind:items="list.items"></shopping-list-component> 
      </div> 
    </div> 
  </div> 
</template> 

看看这页。现在你应该看到漂亮的标签,它们默认显示内容:

修改 App.vue

正确的类绑定后的购物清单应用程序的外观和感觉

在进行这些修改后,最终的购物清单应用程序代码可以在chapter4/shopping-list2文件夹中找到。

使用 v-on 指令的事件监听器

使用 Vue.js 监听事件并调用回调非常容易。事件监听也是使用特定修饰符的特殊指令完成的。该指令是v-on。修饰符在冒号之后应用:

v-on:click="myMethod" 

好的,你说,我在哪里声明这个方法?你可能不会相信,但所有组件的方法都是在methods属性内声明的!因此,要声明名为myMethod的方法,你应该这样做:

<script> 
  export default { 
    methods: { 
      myMethod () { 
        //do something nice  
      }  
    } 
  } 
</script> 

所有dataprops属性都可以使用this关键字在方法内部访问。

让我们添加一个方法来向items数组中添加新项目。实际上,在上一章中,当我们学习如何在父子组件之间传递数据时,我们已经做过了。我们只是在这里回顾一下这部分。

为了能够在属于ShoppingListComponent的购物清单中向AddItemComponent内添加新项目,我们应该这样做:

  • 确保AddItemComponent有一个名为newItemdata属性。

  • AddItemComponent内创建一个名为addItem的方法,该方法推送newItem并触发add事件。

  • 使用v-on:click指令为Add!按钮应用一个事件监听器。此事件监听器应调用已定义的addItem方法。

  • ShoppingListComponent内创建一个名为addItem的方法,该方法将接收text作为参数,并将其推送到items数组中。

  • v-on指令与自定义的add修饰符绑定到ShoppingListComponent内的add-item-component的调用上。此监听器将调用此组件中定义的addItem方法。

那么,让我们开始吧!使用chapter4/shopping-list2文件夹中的购物清单应用程序并进行操作。

首先打开AddItemComponent,并为Add!按钮和addItem方法添加缺失的v-on指令:

//AddItemComponent.vue 
<template> 
  <div class="input-group"> 
    <input type="text" **v-model="newItem"** 
      placeholder="add shopping list item" class="form-control"> 
    <span class="input-group-btn"> 
      <button **v-on:click="addItem"** class="btn btn-default" 
        type="button">Add!</button> 
    </span> 
  </div> 
</template> 

<script> 
  export default { 
    data () { 
      return { 
        **newItem: ''** 
      } 
    }, 
    **methods: { 
      addItem () { 
        var text 

        text = this.newItem.trim() 
        if (text) { 
          this.$emit('add', this.newItem) 
          this.newItem = '' 
        } 
      } 
    }** 
  } 
</script> 

切换到ShoppingListComponent,并将v-on:add指令绑定到template标签内的add-item-component的调用上:

//ShoppingListComponent.vue 
<template> 
  <div> 
    <h2>{{ title }}</h2> 
    <add-item-component **v-on:add="addItem"**></add-item-component> 
    <items-component v-bind:items="items"></items-component> 
    <div class="footer"> 
      <hr /> 
      <change-title-component></change-title-component> 
    </div> 
  </div> 
</template> 

现在在ShoppingListComponent内创建addItem方法。它应该接收文本,并将其推入this.items数组中:

//ShoppingListComponent.vue 
<script> 
  import AddItemComponent from './AddItemComponent' 
  import ItemsComponent from './ItemsComponent' 
  import ChangeTitleComponent from './ChangeTitleComponent' 

  export default { 
    components: { 
      AddItemComponent, 
      ItemsComponent, 
      ChangeTitleComponent 
    }, 
    props: ['title', 'items'], 
    **methods: { 
      addItem (text) { 
        this.items.push({ 
          text: text, 
          checked: false 
        }) 
      } 
    }** 
  } 
</script> 

打开页面,尝试通过在输入框中输入并点击按钮来将项目添加到列表中。它有效!

现在,我想请你将角色从应用程序的开发人员切换到其用户。在输入框中输入新项目。项目介绍后,用户显而易见的动作是什么?难道你不是想按Enter按钮吗?我敢打赌你是!当什么都没有发生时,这有点令人沮丧,不是吗?别担心,我的朋友,我们只需要向输入框添加一个事件侦听器,并调用与Add!按钮相同的方法。

听起来很容易,对吧?我们按Enter按钮时触发了什么事件?对,就是 keyup 事件。因此,我们只需要在冒号分隔符后使用v-on指令和keyup方法。问题是,当我们按下新的购物清单项目时,每次引入新字母时,该方法都会被调用。这不是我们想要的。当然,我们可以在addItem方法内添加一个条件,检查event.code属性,并且只有在它是13(对应Enter键)时,我们才会调用方法的其余部分。幸运的是,对于我们来说,Vue 提供了一种机制,可以为此方法提供按键修饰符,这样我们只能在按下特定按键时调用方法。它应该使用点(.)修饰符实现。在我们的情况下,如下所示:

v-on:keyup.enter 

让我们将其添加到我们的输入框中。转到AddItemComponent,并将v-on:keyup.enter指令添加到输入中,如下所示:

<template> 
  <div class="input-group"> 
    <input type="text" **v-on:keyup.enter="addItem"** v-model="newItem" 
      placeholder="add shopping list item" class="form-control"> 
    <span class="input-group-btn"> 
      <button v-on:click="addItem" class="btn btn-default" 
        type="button">Add!</button> 
    </span> 
  </div> 
</template> 

打开页面,尝试使用Enter按钮将项目添加到购物清单中。它有效!

让我们对标题更改做同样的事情。唯一的区别是,在添加项目时,我们使用了自定义的add事件,而在这里我们将使用原生的输入事件。我们已经做到了。我们只需要执行以下步骤:

  1. ShoppingListComponent的模板中,使用v-model指令将模型标题绑定到change-title-component

  2. ChangeTitleComponentprops属性中导出value

  3. ChangeTitleComponent内创建一个onInput方法,该方法将使用事件目标的值发出原生的input方法。

  4. value 绑定到 ChangeTitleComponent 组件模板中的 input,并使用带有 onInput 修饰符的 v-on 指令。

因此,在 ShoppingListComponent 模板中的 change-title-component 调用将如下所示:

//ShoppingListComponent.vue 
<change-title-component **v-model="title"**></change-title-component> 

ChangeTitleComponent 将如下所示:

//ChangeTitleComponent.vue 
<template> 
  <div> 
    <em>Change the title of your shopping list here</em> 
    <input **v-bind:value="value" v-on:input="onInput"**/> 
  </div> 
</template> 

<script> 
  export default { 
    props: ['value'], 
    methods: { 
      **onInput (event) { 
        this.$emit('input', event.target.value) 
      }** 
    } 
  } 
</script>  

此部分的最终代码可以在 chapter4/shopping-list3 文件夹中找到。

简写

当然,每次在代码中写 v-bindv-on 指令并不费时。开发人员倾向于认为每次减少代码量,我们就赢了。Vue.js 允许我们赢!只需记住 v-bind 指令的简写是冒号(:),v-on 指令的简写是 @ 符号。这意味着以下代码做了同样的事情:

v-bind:items="items"  :items="items" 
v-bind:class=' $index === 0 ? "active" : ""'  
:class=' $index===0 ? "active" : ""' 
v-on:keyup.enter="addItem"  @keyup.enter="addItem" 

练习

使用我们刚学到的快捷方式重写购物清单应用程序中的所有 v-bindv-on 指令。

通过查看 chapter4/shopping-list4 文件夹来检查自己。

小猫

在本章中,我们并没有涉及到我们的番茄钟应用程序及其可爱的小猫。我向您保证,我们将在下一章中大量涉及它。与此同时,我希望这只小猫会让您开心:

小猫

小猫问:“接下来做什么?”

总结

在本章中,我们对将数据绑定到我们的表示层的所有可能方式进行了广泛的概述。您学会了如何简单地使用句柄括号({{ }})插值数据。您还学会了如何在这样的插值中使用 JavaScript 表达式和过滤器。您学习并应用了诸如 v-bindv-modelv-forv-ifv-show 等指令。

我们修改了我们的应用程序,使它们使用更丰富和更高效的数据绑定语法。

在下一章中,我们将讨论 Vuex,这是受 Flux 和 Redux 启发的状态管理架构,但具有简化的概念。

我们将为我们的两个应用程序创建全局应用程序状态管理存储,并通过使用它来探索它们的潜力。

第五章:Vuex-管理应用程序中的状态

在上一章中,您学习了 Vue.js 中最重要的概念之一:数据绑定。您学习并应用了许多将数据绑定到我们的应用程序的方法。您还学习了如何使用指令,如何监听事件,以及如何创建和调用方法。在本章中,您将看到如何管理表示全局应用程序状态的数据。我们将讨论 Vuex,这是 Vue 应用程序中用于集中状态的特殊架构。您将学习如何创建全局数据存储以及如何在组件内部检索和更改它。我们将定义应用程序中哪些数据是本地的,哪些应该是全局的,并且我们将使用 Vuex 存储来处理其中的全局状态。

总而言之,在本章中,我们将:

  • 了解本地和全局应用程序状态之间的区别

  • 了解 Vuex 是什么以及它是如何工作的

  • 学习如何使用全局存储中的数据

  • 了解存储的 getter、mutation 和 action

  • 安装并在购物清单和番茄钟应用程序中使用 Vuex 存储

父子组件的通信、事件和脑筋急转弯

还记得我们的购物清单应用程序吗?还记得我们的ChangeTitleComponent以及我们如何确保在其输入框中输入会影响属于父组件的购物清单的标题吗?您记得每个组件都有自己的作用域,父组件的作用域不能受到子组件的影响。因此,为了能够将来自子组件内部的更改传播到父组件,我们使用了事件。简单地说,您可以从子组件调用$emit方法,并传递要分发的事件的名称,然后在父组件的v-on指令中监听此事件。

如果是原生事件,比如input,那就更简单了。只需将所需的属性绑定到子组件作为v-model,然后从子组件调用$emit方法并传递事件的名称(例如,input)。

实际上,这正是我们在ChangeTitleComponent中所做的。

打开chapter5/shopping-list文件夹中的代码,并检查我是否正确。(如果您想在浏览器中检查应用程序的行为,您可能还需要运行npm installnpm run dev。)

我们使用v-model指令将标题绑定到ShoppingListComponent模板中的ChangeTitleComponent

//ShoppingListComponent.vue 
<template> 
  <div> 
    <...> 
    <div class="footer"> 
      <hr /> 
      <change-title-component **v-model="title"**></change-title-component> 
    </div> 
  </div> 
</template> 

之后,我们在ChangeTitleComponentprops属性中声明了标题模型的值,并在input动作上发出了input事件:

<template> 
  <div> 
    <em>Change the title of your shopping list here</em> 
    <input **:value="value" @input="onInput"**/> 
  </div> 
</template> 

<script> 
  export default { 
    props: [**'value'**], 
    methods: { 
      onInput (event) { 
        **this.$emit('input', event.target.value)** 
      } 
    } 
  } 
</script> 

看起来非常简单,对吧?

如果我们尝试在输入框中更改标题,我们的购物清单的标题会相应更改:

父子组件的通信、事件和脑筋急转弯

在父子组件之间建立基于事件的通信之后,我们能够改变标题

看起来我们实际上能够实现我们的目的。然而,如果你打开你的开发工具,你会看到一个丑陋的错误:

**[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component rerenders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "title"**

哎呀!Vue 实际上是对的,我们正在改变包含在ShoppingList组件的props属性中的数据。这个属性来自于主父组件App.vue,它又是我们的ShoppingListComponent的父组件。而我们已经知道我们不能从子组件改变父组件的数据。如果标题直接属于ShoppingListComponent,那就没问题,但在这种情况下,我们肯定做错了什么。

另外,如果你足够注意,你可能会注意到还有一个地方包含了相同的数据,尽管我们努力了,它也没有改变。看看标签的标题。它继续显示单词Groceries。但我们也希望它改变!

小小的侧记:我添加了一个新组件,ShoppingListTitleComponent。它代表了标签的标题。你还记得计算属性吗?请注意,这个组件包含一个计算属性,它只是在通过props属性导入的 ID 前面添加#来生成一个锚点:

<template> 
  <a **:href="href"** :aria-controls="id" role="tab" data-toggle="tab">
  {{ title }}</a> 
</template> 
<script> 
  export default{ 
    props: ['id', **'title'**], 
    **computed: { 
      href () { 
        return '#' + this.id 
      } 
    }** 
  } 
</script> 

显示标签标题的锚点包含一个依赖于这个计算属性的href绑定指令。

所以,回到标题更改。当ChangeTitleComponent内部的标题改变时,我们能做些什么来改变这个组件的标题?如果我们能将事件传播到主App.vue组件,我们实际上可以解决这两个问题。每当父组件中的数据改变时,它都会影响所有子组件。

因此,我们需要以某种方式使事件从ChangeTitleComponent流向主App组件。听起来很困难,但实际上,我们只需要在ChangeTitleComponent及其父级中注册我们的自定义事件,并发出它直到它到达App组件。App组件应该通过将更改应用于相应的标题来处理此事件。为了让App.vue确切地知道正在更改哪个购物清单,它的子ShoppingListComponent还应该传递它所代表的购物清单的 ID。为了实现这一点,App.vue应该将id属性传递给组件,购物清单组件应该在其props属性中注册它。

因此,我们将执行以下操作:

  1. App组件的模板中,在ShoppingListComponent的创建时将id属性绑定到ShoppingListComponent

  2. ShoppingList组件内部绑定属性title而不是v-modelchange-title-component

  3. 将自定义事件(我们称之为changeTitle)附加到ChangeTitleComponent内部的input上。

  4. 告诉ShoppingListComponent监听来自change-title-component的自定义changeTitle事件,使用v-on指令处理它,通过发出另一个事件(也可以称为changeTitle)来处理它,应该被App组件捕获。

  5. App.vue内部为shopping-list-component附加changeTitle事件的监听器,并通过实际更改相应购物清单的标题来处理它。

让我们从修改App.vue文件的模板开始,并将购物清单的 ID 绑定到shopping-list-component

//App.vue 
<template> 
  <div id="app" class="container"> 
    <...> 
        <shopping-list-component **:id="list.id"** : 
          :items="list.items"></shopping-list-component> 
    <...> 
  </div> 
</template> 

现在在ShoppingListComponent组件的props中注册id属性:

//ShoppingListComponent.vue 
<script> 
  <...> 
  export default { 
    <...> 
    props: [**'id'**, 'title', 'items'], 
    <...> 
  } 
</script> 

title数据属性绑定到change-title-component而不是v-model指令:

//ShoppingListComponent.vue 
<template> 
  <...> 
      <change-title-component **:**></change-title-component> 
  <...> 
</template> 

//ChangeTitleComponent.vue 
<template> 
  <div> 
    <em>Change the title of your shopping list here</em> 
    <input **:value="title"** @input="onInput"/> 
  </div> 
</template> 

<script> 
  export default { 
    props: ['value', **'title'**], 
    <...> 
  } 
</script> 

ChangeTitleComponent发出自定义事件而不是input,并在其父组件中监听此事件:

//ChangeTitleComponent.vue 
<script> 
  export default { 
    <...> 
    methods: { 
      onInput (event) { 
        this.$emit(**'changeTitle'**, event.target.value) 
      } 
    } 
  } 
</script> 

//ShoppingListComponent.vue 
<template> 
  <...> 
      <change-title-component :  
        **v-on:changeTitle="onChangeTitle"**></change-title-component> 
  <...> 
</template> 

ShoppingListComponent中创建onChangeTitle方法,该方法将发出自己的changeTitle事件。使用v-on指令在App.vue组件中监听此事件。请注意,购物清单组件的onChangeTitle方法应发送其 ID,以便App.vue知道正在更改哪个购物清单的标题。因此,onChangeTitle方法及其处理将如下所示:

//ShoppingListComponent.vue 
<script> 
  <...> 

  export default { 
    <...> 
    methods: { 
      <...> 
      **onChangeTitle (text) { 
        this.$emit('changeTitle', this.id, text) 
      }** 
    } 
  } 
</script> 

//App.vue 
<template> 
  <...> 
  <shopping-list-component :id="list.id" : 
    :items="list.items" **v-on:changeTitle="onChangeTitle"**>
  </shopping-list-component> 
  <...> 
</template> 

最后,在App.vue中创建一个changeTitle方法,该方法将通过其 ID 在shoppinglists数组中找到一个购物清单并更改其标题:

<script> 
  <...> 
  import _ from 'underscore' 

  export default { 
    <...> 
    methods: { 
      **onChangeTitle (id, text) { 
        _.findWhere(this.shoppinglists, { id: id }).title = text 
      }** 
    } 
  } 
</script> 

请注意,我们使用了underscore类的findWhere方法(underscorejs.org/#findWhere)来使我们通过 ID 查找购物清单的任务更容易。

而且...我们完成了!检查这个提示的最终代码在chapter5/shopping-list2文件夹中。在浏览器中检查页面。尝试在输入框中更改标题。你会看到它在所有地方都改变了!

承认这是相当具有挑战性的。试着自己重复所有的步骤。与此同时,让我随机地告诉你两个词:全局和局部。想一想。

我们为什么需要一个全局状态存储?

作为开发人员,你已经熟悉全局和局部的概念。有一些全局变量可以被应用程序的每个部分访问,但方法也有它们自己的(局部)作用域,它们的作用域不可被其他方法访问。

基于组件的系统也有它的局部和全局状态。每个组件都有它的局部数据,但应用程序有一个全局的应用程序状态,可以被应用程序的任何组件访问。我们在前面段落中遇到的挑战,如果我们有某种全局变量存储器,其中包含购物清单的标题,并且每个组件都可以访问和修改它们,那么这个挑战将很容易解决。幸运的是,Vue 的创作者为我们考虑到了这一点,并创建了 Vuex 架构。这种架构允许我们创建一个全局应用程序存储——全局应用程序状态可以被存储和管理的地方!

什么是 Vuex?

如前所述,Vuex 是用于集中状态管理的应用程序架构。它受 Flux 和 Redux 的启发,但更容易理解和使用:

什么是 Vuex?

Vuex 架构;图片取自 Vuex GitHub 页面,网址为 https://github.com/vuejs/vuex

看着镜子(不要忘记对自己微笑)。你看到一个漂亮的人。然而,里面有一个复杂的系统。当你感到冷时你会怎么做?当天气炎热时你会有什么感觉?饥饿是什么感觉?非常饥饿又是什么感觉?摸一只毛茸茸的猫是什么感觉?人可以处于各种状态(快乐,饥饿,微笑,生气等)。人还有很多组件,比如手、胳膊、腿、胃、脸等。你能想象一下,如果比如一只手能够直接影响你的胃,让你感到饥饿,而你却不知情,那会是什么感觉?

我们的工作方式与集中式状态管理系统非常相似。我们的大脑包含事物的初始状态(快乐,不饿,满足等)。它还提供了允许在其中拉动的机制,可以影响状态。例如,微笑感到满足鼓掌等。我们的手、胃、嘴巴和其他组件不能直接影响状态。但它们可以告诉我们的大脑触发某些改变,而这些改变反过来会影响状态。

例如,当你饿了的时候,你会吃东西。你的胃在某个特定的时刻告诉大脑它已经饱了。这个动作会改变饥饿状态为满足状态。你的嘴巴组件与这个状态绑定,让你露出微笑。因此,组件与只读的大脑状态绑定,并且可以触发改变状态的大脑动作。这些组件彼此不知道对方,也不能直接以任何方式修改对方的状态。它们也不能直接影响大脑的初始状态。它们只能调用动作。动作属于大脑,在它们的回调中,状态可以被修改。因此,我们的大脑是唯一的真相来源。

提示

信息系统中的唯一真相来源是一种设计应用架构的方式,其中每个数据元素只存储一次。这些数据是只读的,以防止应用程序的组件破坏被其他组件访问的状态。Vuex 商店的设计方式使得不可能从任何组件改变它的状态。

商店是如何工作的,它有什么特别之处?

Vuex 存储基本上包含两件事:状态变化。状态是表示应用程序数据的初始状态的对象。变化也是一个包含影响状态的动作函数的对象。Vuex 存储只是一个普通的 JavaScript 文件,它导出这两个对象,并告诉 Vue 使用 Vuex(Vue.use(Vuex))。然后它可以被导入到任何其他组件中。如果你在主App.vue文件中导入它,并在Vue应用程序初始化时注册存储,它将传递给整个子代链,并且可以通过this.$store变量访问。因此,非常粗略地,以一种非常简化的方式,我们将创建一个存储,在主应用程序中导入它,并在组件中使用它的方式:

**//CREATE STORE** 
//initialize state 
const state = { 
  msg: 'Hello!' 
} 
//initialize mutations 
const mutations = { 
  changeMessage(state, msg) { 
    state.msg = msg 
  } 
} 
//create store with defined state and mutations 
export default new Vuex.Store({ 
  state: state 
  mutations: mutations 
}) 

**//CREATE VUE APP** 
<script> 
  **import store from './vuex/store'** 
  export default { 
    components: { 
      SomeComponent 
    }, 
    **store: store** 
  } 
</script> 

**//INSIDE SomeComponent** 
<script> 
  export default { 
    computed: { 
      msg () { 
        return **this.$store.state.msg**; 
      } 
    }, 
    methods: { 
      changeMessage () { 
        **this.$store.commit('changeMessage', newMsg);**      
      } 
    } 
  } 
</script> 

一个非常合乎逻辑的问题可能会出现:为什么创建 Vuex 存储而不是只有一个共享的 JavaScript 文件导入一些状态?当然,你可以这样做,但是然后你必须确保没有组件可以直接改变状态。当然,能够直接更改存储属性会更容易,但这可能会导致错误和不一致。Vuex 提供了一种干净的方式来隐式保护存储状态免受直接访问。而且,它是反应性的。将所有这些放在陈述中:

  • Vuex 存储是反应性的。一旦组件从中检索状态,它们将在状态更改时自动更新其视图。

  • 组件无法直接改变存储的状态。相反,它们必须分派存储声明的变化,这样可以轻松跟踪更改。

  • 因此,我们的 Vuex 存储成为了唯一的真相来源。

让我们创建一个简单的问候示例,看看 Vuex 的运作方式。

带存储的问候

我们将创建一个非常简单的 Vue 应用程序,其中包含两个组件:其中一个将包含问候消息,另一个将包含input,允许我们更改此消息。我们的存储将包含表示初始问候的初始状态,以及能够更改消息的变化。让我们从创建 Vue 应用程序开始。我们将使用vue-cliwebpack-simple模板:

**vue init webpack-simple simple-store**

安装依赖项并按以下方式运行应用程序:

**cd simple-store npm install npm run dev**

应用程序已启动!在localhost:8080中打开浏览器。实际上,问候已经存在。现在让我们添加必要的组件:

  • ShowGreetingsComponent将只显示问候消息

  • ChangeGreetingsComponent将显示输入字段,允许更改消息

src文件夹中,创建一个components子文件夹。首先将ShowGreetingsComponent.vue添加到这个文件夹中。

它看起来就像下面这样简单:

<template> 
  <h1>**{{ msg }}**</h1> 
</template> 
<script> 
  export default { 
    **props: ['msg']** 
  } 
</script> 

之后,将ChangeGreetingsComponent.vue添加到这个文件夹中。它必须包含带有v-model='msg'指令的输入:

<template> 
  <input **v-model='msg'**> 
</template> 
<script> 
  export default { 
    **props: ['msg']** 
  } 
</script> 

现在打开App.vue文件,导入组件,并用这两个组件替换标记。不要忘记将msg绑定到它们两个。所以,修改后的App.vue将看起来像下面这样:

<template> 
  <div> 
    **<show-greetings-component :msg='msg'></show-greetings-component> 
    <change-greetings-component :msg='msg'></change-greetings-component>** 
  <div> 
</template> 

<script> 
import ShowGreetingsComponent from './components/ShowGreetingsComponent.vue' 
import ChangeGreetingsComponent from './components/ChangeGreetingsComponent.vue' 

export default { 
  **components: { ShowGreetingsComponent, ChangeGreetingsComponent }**, 
  data () { 
    return { 
      msg: 'Hello Vue!' 
    } 
  } 
} 
</script> 

打开浏览器。你会看到带有我们问候语的输入框;然而,在其中输入不会改变标题中的消息。我们已经预料到了,因为我们知道组件不能直接影响彼此的状态。现在让我们引入 store!首先,我们必须安装vuex

**npm install vuex --save**

src文件夹中创建一个名为vuex的文件夹。创建一个名为store.js的 JavaScript 文件。这将是我们的状态管理入口。首先导入VueVuex,并告诉Vue我们想在这个应用程序中使用Vuex

//store.js
import Vue from 'vue'
import Vuex from 'vuex'
  **Vue.use(Vuex)**

现在创建两个常量,statemutationsState将包含消息msg,而mutations将导出允许我们修改msg的方法:

const state = { 
  msg: 'Hello Vue!' 
} 

const mutations = { 
  changeMessage(state, msg) { 
    state.msg = msg 
  } 
} 

现在使用已创建的statemutations初始化 Vuex store:

export default new Vuex.Store({ 
  state: state, 
  mutations: mutations 
}) 

提示

由于我们使用 ES6,{state: state, mutations: mutations}的表示法可以简单地替换为{state, mutations}

我们整个商店的代码看起来就像下面这样:

//store.js 
import Vue from 'vue' 
import Vuex from 'vuex' 

Vue.use(Vuex) 
const state = { 
  **msg: 'Hello Vue!'** 
} 
const mutations = { 
  **changeMessage(state, msg) { 
    state.msg = msg 
  }** 
} 
export default new Vuex.Store({ 
  **state, 
  mutations** 
}) 

现在我们可以在App.vue中导入 store。通过这样做,我们告诉所有组件它们可以使用全局 store,因此我们可以从App.vue中删除数据。而且,我们不再需要将数据绑定到组件:

//App.vue 
<template> 
  <div> 
    <show-greetings-component></show-greetings-component> 
    <change-greetings-component></change-greetings-component> 
  </div> 
</template> 

<script> 
import ShowGreetingsComponent from './components/ShowGreetingsComponent.vue' 
import ChangeGreetingsComponent from './components/ChangeGreetingsComponent.vue' 
**import store from './vuex/store'** 

export default { 
  components: {ShowGreetingsComponent, ChangeGreetingsComponent}, 
  **store** 
} 
</script>    

现在让我们回到我们的组件,并重用 store 中的数据。为了能够重用 store 状态中的响应式数据,我们应该使用计算属性。Vue 是如此聪明,它将为我们做所有的工作,以便在状态更改时,反应地更新这些属性。不,我们不需要在组件内部导入 store。我们只需使用this.$store变量就可以访问它。因此,我们的ShowGreetingsComponent将看起来像下面这样:

//ShowGreetingsComponent.vue 
<template> 
  <h1>{{ msg }}</h1> 
</template> 
<style> 
</style> 
<script> 
  export default { 
    **computed: { 
      msg () { 
        return this.$store.state.msg 
      } 
    }** 
  } 
</script> 

按照相同的逻辑在ChangeGreetingsComponent中重用存储的msg。现在我们只需要在每个keyup事件上分发变异。为此,我们只需要创建一个方法,该方法将提交相应的存储变异,并且我们将从输入的keyup监听器中调用它:

//ChangeGreetingsComponent.vue 
<template> 
  <input v-model='msg' **@keyup='changeMsg'**> 
</template> 
<script> 
  export default { 
    computed: { 
      msg() { 
        return this.$store.state.msg 
      } 
    }, 
    **methods: { 
      changeMsg(ev) { 
        this.$store.commit('changeMessage', ev.target.value) 
      } 
    }** 
  } 
</script> 

打开页面。尝试更改标题。Et voilà!它奏效了!

商店问候

使用 Vuex 存储调用变异并通过组件传播更改存储状态

我们不再需要绑定v-model指令,因为所有的更改都是由调用存储的变异方法引起的。因此,msg属性可以绑定为输入框的值属性:

<template> 
  <input **:value='msg'** @keyup='changeMsg'> 
</template> 

检查chapter5/simple-store文件夹中的此部分的代码。在这个例子中,我们使用了一个非常简化的存储版本。然而,复杂的单页应用程序SPAs)需要更复杂和模块化的结构。我们可以并且应该将存储的 getter 和分发变化的操作提取到单独的文件中。我们还可以根据相应数据的责任对这些文件进行分组。在接下来的章节中,我们将看到如何通过使用 getter 和 action 来实现这样的模块化结构。

存储状态和 getter

当然,我们可以在组件内部重用this.$store.state关键字是好的。但想象一下以下情景:

  • 在一个大型应用程序中,不同的组件使用$this.store.state.somevalue访问存储的状态,我们决定更改somevalue的名称。这意味着我们必须更改每个使用它的组件内部变量的名称!

  • 我们想要使用状态的计算值。例如,假设我们想要一个计数器。它的初始状态是“0”。每次我们使用它,我们都想要递增它。这意味着每个组件都必须包含一个重用存储值并递增它的函数,这意味着在每个组件中都有重复的代码,这一点一点也不好!

对不起,情景不太好,伙计们!幸运的是,有一种不会陷入其中任何一种情况的好方法。想象一下,中央获取器访问存储状态并为每个状态项提供获取器函数。如果需要,此获取器可以对状态项应用一些计算。如果我们需要更改某些属性的名称,我们只需在此获取器中更改一次。这更像是一种良好的实践或约定,而不是强制性的架构系统,但我强烈建议即使只有几个状态项,也要使用它。

让我们为我们的简单问候应用程序创建这样的获取器。只需在vuex文件夹中创建一个getters.js文件,并导出一个将返回state.msggetMessage函数:

//getters.js 
export default { 
  **getMessage(state) { 
    return state.msg 
  }** 
} 

然后它应该被存储导入并在新的Vuex对象中导出,这样存储就知道它的获取器是什么:

//store.js 
import Vue from 'vue' 
import Vuex from 'vuex' 
**import getters from './getters'** 

Vue.use(Vuex) 

const state = { 
  msg: 'Hello Vue!' 
} 

const mutations = { 
  changeMessage(state, msg) { 
    state.msg = msg 
  } 
} 

export default new Vuex.Store({ 
  state, mutations, **getters** 
}) 

然后,在我们的组件中,我们使用获取器而不是直接访问存储状态。只需在两个组件中替换您的computed属性为以下内容:

computed: { 
  msg () { 
    return **this.$store.getters.getMessage** 
  } 
}, 

打开页面;一切都像魅力一样工作!

仍然this.$store.getters表示法包含太多要写的字母。我们,程序员是懒惰的,对吧?Vue 很好地为我们提供了一种支持我们懒惰的简单方法。它提供了一个mapGetters助手,正如其名称所示,为我们的组件提供了所有存储的获取器。只需导入它并在您的computed属性中使用它,如下所示:

//ShowGreetingsComponent.vue 
<template> 
  <h1>**{{ getMessage }}**</h1> 
</template> 
<script> 
  **import { mapGetters } from 'vuex'** 

  export default { 
    **computed: mapGetters(['getMessage'])** 
  } 
</script> 

//ChangeGreetingsComponent.vue 
<template> 
  <input :value='**getMessage**' @keyup='changeMsg'> 
</template> 
<script> 
  **import { mapGetters } from 'vuex'** 

  export default { 
    **computed: mapGetters(['getMessage'])**, 
    methods: { 
      changeMsg(ev) { 
        this.$store.commit('changeMessage', ev.target.value) 
      } 
    } 
  } 
</script> 

请注意,我们已更改模板中使用的属性,使其与获取器方法名称相同。但是,也可以将相应的获取器方法名称映射到我们在组件中想要使用的属性名称。

//ShowGreetingsComponent.vue 
<template> 
  <h1>**{{ msg }}**</h1> 
</template> 
<style> 
</style> 
<script> 
  **import { mapGetters } from 'vuex'** 

  export default { 
    **computed: mapGetters({ 
      msg: 'getMessage' 
    })** 
  } 
</script> 

//ChangeGreetingsComponent.vue 
<template> 
  <input :value='**msg**' @keyup='changeMsg'> 
</template> 
<script> 
  **import { mapGetters } from 'vuex'** 

  export default { 
    **computed: mapGetters({ 
      msg: 'getMessage' 
    })**, 
    methods: { 
      changeMsg(ev) { 
        this.$store.commit('changeMessage', ev.target.value) 
      } 
    } 
  } 
</script> 

因此,我们能够将msg属性的获取器提取到中央存储的获取器文件中。

现在,如果您决定为msg属性添加一些计算,您只需要在一个地方做就可以了。只在一个地方!

存储状态和获取器

Rick 总是在所有组件中更改代码,刚刚发现只需在一个地方更改代码就可以了

例如,如果我们想在所有组件中重用大写消息,我们可以在获取器中应用uppercase函数,如下所示:

//getters.js 
export default { 
  getMessage(state) { 
    **return (state.msg).toUpperCase()** 
  } 
} 

从现在开始,每个使用获取器检索状态的组件都将具有大写消息:

存储状态和获取器

ShowTitleComponent 将消息大写。toUpperCase 函数应用在 getter 内部

还要注意,当您在输入框中输入时,消息如何平稳地变成大写!查看chapter5/simple-store2文件夹中此部分的最终代码。

如果我们决定更改状态属性的名称,我们只需要在 getter 函数内部进行更改。例如,如果我们想将msg的名称更改为message,我们将在我们的 store 内部进行更改:

const state = { 
  **message**: 'Hello Vue!' 
} 

const mutations = { 
  changeMessage(state, msg) { 
    state.**message** = msg 
  } 
} 

然后,我们还将在相应的 getter 函数内部进行更改:

export default { 
  getMessage(state) { 
    return (**state.message**).toUpperCase() 
  } 
} 

就是这样!应用的其余部分完全不受影响。这就是这种架构的力量。在一些非常复杂的应用程序中,我们可以有多个 getter 文件,为应用程序的不同属性导出状态。模块化是推动可维护性的力量;利用它!

变化

从前面的例子中,应该清楚地看到,变化不过是由名称定义的简单事件处理程序函数。变化处理程序函数将state作为第一个参数。其他参数可以用于向处理程序函数传递不同的参数:

const mutations = { 
  **changeMessage**(state, msg) { 
    state.message = msg 
  }, 
  **incrementCounter**(state) { 
    state.counter ++; 
  } 
} 

变化的一个特点是它们不能直接调用。为了能够分发一个变化,我们应该调用一个名为commit的方法,其中包含相应变化的名称和参数:

store.commit('changeMessage', 'newMessage') 
store.commit('incrementCounter') 

提示

在 Vue 2.0 之前,分发变化的方法被称为“dispatch”。因此,您可以按照以下方式调用它:store.dispatch('changeMessage', 'newMessage')

您可以创建任意数量的变化。它们可以对相同状态项执行不同的操作。您甚至可以进一步声明变化名称为常量在一个单独的文件中。这样,您可以轻松地导入它们并使用它们,而不是字符串。因此,对于我们的例子,我们将在vuex目录内创建一个文件,并将其命名为mutation_types.js,并在那里导出所有的常量名称:

//mutation_types.js 
export const INCREMENT_COUNTER = '**INCREMENT_COUNTER**' 
export const CHANGE_MSG = '**CHANGE_MSG**' 

然后,在我们的 store 中,我们将导入这些常量并重复使用它们:

//store.js 
<...> 
**import { CHANGE_MSG, INCREMENT_COUNTER } from './mutation_types'** 
   <...>     
const mutations = { 
  **[CHANGE_MSG]**(state, msg) { 
    state.message = msg 
  }, 
  **[INCREMENT_COUNTER]**(state) { 
    state.counter ++ 
  } 
} 

在分发变化的组件内部,我们将导入相应的变化类型,并使用变量名进行分发:

this.$store.commit(**CHANGE_MSG**, ev.target.value) 

这种结构在大型应用程序中非常有意义。同样,您可以根据它们为应用程序提供的功能对 mutation 类型进行分组,并仅在组件中导入那些特定组件所需的 mutations。这再次涉及最佳实践、模块化和可维护性。

动作

当我们分发一个 mutation 时,我们基本上执行了一个 action。说我们 commit 一个 CHANGE_MSG mutation 就等同于说我们 执行了一个 改变消息的 action。为了美观和完全抽取,就像我们将 store 状态的项抽取到 getters 和将 mutations 名称常量抽取到 mutation_types 一样,我们也可以将 mutations 抽取到 actions 中。

注意

因此,action 实际上只是一个分发 mutation 的函数!

function changeMessage(msg) { store.commit(CHANGE_MSG, msg) }

让我们为我们的改变消息示例创建一个简单的 actions 文件。但在此之前,让我们为 store 的初始状态创建一个额外的项 counter,并将其初始化为 "0" 值。因此,我们的 store 将如下所示:

**//store.js** 
import Vue from 'vue' 
import Vuex from 'vuex' 
import { CHANGE_MSG, INCREMENT_COUNTER } from './mutation_types' 

Vue.use(Vuex) 

const state = { 
  message: 'Hello Vue!', 
  **counter: 0** 
} 

const mutations = { 
  CHANGE_MSG { 
    state.message = msg 
  }, 
  **INCREMENT_COUNTER { 
    state.counter ++; 
  }** 
} 

export default new Vuex.Store({ 
  state, 
  mutations 
}) 

让我们还在 getters 文件中添加一个计数器 getter,这样我们的 getters.js 文件看起来像下面这样:

**//getters.js** 
export default { 
  getMessage(state) { 
    return (state.message).toUpperCase() 
  }, 
  **getCounter(state)**
**{**
**return (state.counter) 
  }** 
} 

最后,让我们在 ShowGreetingsComponent 中使用计数器的 getter 来显示消息 msg 被改变的次数:

<template> 
  <div> 
    <h1>{{ msg }}</h1> 
    **<div>the message was changed {{ counter }} times</div>** 
  </div> 
</template> 
<script> 
  import { mapGetters } from 'vuex' 

  export default { 
    computed: mapGetters({ 
      msg: 'getMessage', 
      **counter: 'getCounter'** 
    }) 
  } 
</script> 

现在让我们为计数器和改变消息的两个 mutations 创建 actions。在 vuex 文件夹中,创建一个 actions.js 文件并导出 actions 函数:

**//actions.js** 
import { CHANGE_MSG, INCREMENT_COUNTER } from './mutation_types'

export const changeMessage = (store, msg) => { 
 store.commit(CHANGE_MSG, msg)
}
export const incrementCounter = (store) => { 
 store.commit(INCREMENT_COUNTER)
}

我们可以并且应该使用 ES2015 参数解构,使我们的代码更加优雅。让我们也在单个 export default 语句中导出所有的 actions:

**//actions.js** 
import **{ CHANGE_MSG, INCREMENT_COUNTER }** from './mutation_types' 

export default { 
  changeMessage (**{ commit }**, msg) { 
    **commit(CHANGE_MSG, msg)** 
  }, 
  incrementCounter (**{ commit }**) { 
    **commit(INCREMENT_COUNTER)** 
  } 
} 

好的,现在我们有了漂亮而美丽的 actions。让我们在 ChangeGreetingsComponent 中使用它们!为了能够在组件中使用 actions,我们首先应该将它们导入到我们的 store 中,然后在新的 Vuex 对象中导出。然后可以在组件中使用 this.$store.dispatch 方法来分发 actions:

// ChangeGreetingsComponent.vue 
<template> 
  <input :value="msg" @keyup="changeMsg"> 
</template> 
<script> 
  import { mapGetters } from 'vuex' 

  export default { 
    computed: mapGetters({ 
      msg: 'getMessage' 
    }), 
    methods: { 
      changeMsg(ev) { 
        **this.$store.dispatch('changeMessage', ev.target.value)** 
      } 
    } 
  } 
</script> 

那么实际上有什么区别呢?我们继续编写 this.$store 代码,唯一的区别是,我们不再调用 commit 方法,而是调用 dispatch。你还记得我们是如何发现 mapGetters 辅助函数的吗?是不是很好?实际上,Vue 也提供了一个 mapActions 辅助函数,它允许我们避免编写冗长的 this.$store.dispatch 方法。只需像导入 mapGetters 一样导入 mapActions,并在组件的 methods 属性中使用它:

//ChangeGreetingsComponent.vue 
<template> 
  <input :value="msg" @keyup="**changeMessage**"> 
</template> 
<script> 
  import { mapGetters } from 'vuex' 
  **import { mapActions } from 'vuex'** 

  export default { 
    computed: mapGetters({ 
      msg: 'getMessage' 
    }), 
    methods:  mapActions([**'changeMessage'**, **'incrementCounter'**]) 
  } 
</script> 

请注意,我们已经改变了keyup事件的处理函数,所以我们不必将事件名称映射到相应的 actions。然而,就像mapGetters一样,我们也可以将自定义事件名称映射到相应的 actions 名称。

我们还应该改变changeMessage的调用,因为我们现在不在 actions 中提取任何事件的目标值;因此,我们应该在调用中进行提取:

//ChangeGreetingsComponent.vue 
<template> 
  <input :value="msg" **@keyup="changeMessage($event.target.value)"**> 
</template>  

最后,让我们将incrementCounter action 绑定到用户的输入上。例如,让我们在输入模板中在keyup.enter事件上调用这个 action:

<template> 
  <input :value="msg" @keyup="changeMessage" 
  **@keyup.enter="incrementCounter"**> 
</template> 

如果你打开页面,尝试改变标题并按下Enter按钮,你会发现每次按下Enter时计数器都会增加:

Actions

使用 actions 来增加页面上的计数器。

所以,你看到了使用 actions 而不是直接访问 store 来模块化我们的应用是多么容易。你在 Vuex store 中导出 actions,在组件中导入mapActions,并在模板中的事件处理程序指令中调用它们。

你还记得我们的“人体”例子吗?在那个例子中,我们将人体的部分与组件进行比较,将人脑与应用状态的存储进行比较。想象一下你在跑步。这只是一个动作,但有多少变化被派发,有多少组件受到这些变化的影响?当你跑步时,你的心率增加,你出汗,你的手臂移动,你的脸上露出微笑,因为你意识到跑步是多么美好!当你吃东西时,你也会微笑,因为吃东西是美好的。当你看到小猫时,你也会微笑。因此,不同的 actions 可以派发多个变化,同一个变化也可以被多个 action 派发。

我们的 Vuex 存储和它的 mutations 和 actions 也是一样的。在同一个 action 中,可以派发多个 mutation。例如,我们可以在同一个 action 中派发改变消息和增加计数器的 mutation。让我们在action.js文件中创建这个 action。让我们称之为handleMessageInputChanges,并让它接收一个参数:event。它将使用event.target.value派发CHANGE_MSG mutation,并且如果event.keyCodeenter,它将派发INCREMENT_COUNTER mutation。

//actions.js 
handleMessageInputChanges ({ commit }, event) { 
  **commit(CHANGE_MSG, event.target.value)** 
  if (event.keyCode === 13) { 
    **commit(INCREMENT_COUNTER)** 
  } 
} 

现在让我们在ChangeGreetingsComponent组件的mapActions对象中导入这个 action,并使用它调用带有$event参数的 action:

//ChangeGreetingsComponent.vue 
<template> 
  <input :value="msg" **@keyup="handleMessageInputChanges($event)"** /> 
</template> 
<script> 
  import { mapGetters, mapActions } from 'vuex' 

  export default { 
    computed: mapGetters({ 
      msg: 'getMessage' 
    }), 
    **methods:  mapActions(['handleMessageInputChanges'])** 
  } 
</script> 

打开页面,尝试更改问候消息并通过点击Enter按钮增加计数器。它有效!

简单存储示例的最终代码可以在chapter5/simple-store3文件夹中找到。

在我们的应用程序中安装和使用 Vuex store

现在我们知道了 Vuex 是什么,如何创建 store,分发 mutations,以及如何使用 getter 和 action,我们可以在我们的应用程序中安装 store,并用它来完成它们的数据流和通信链。

您可以在以下文件夹中找到要处理的应用程序:

不要忘记在两个应用程序上运行npm install

首先安装vuex,并在两个应用程序中定义必要的目录和文件结构。

要安装vuex,只需运行以下命令:

**npm install vuex --save** 

安装vuex后,在每个应用程序的src文件夹中创建一个名为vuex的子文件夹。在此文件夹中,创建四个文件:store.jsmutation_types.jsactions.jsgetters.js

准备store.js结构:

//store.js 
import Vue from 'vue' 
import Vuex from 'vuex' 
import getters from './getters' 
import actions from './actions' 
import mutations from './mutations' 

Vue.use(Vuex) 

const state = { 
} 

export default new Vuex.Store({ 
  state,  
  mutations,  
  getters,  
  actions 
}) 

在主App.vue中导入并使用 store:

//App.vue 
<script> 
  <...> 
  import store from './vuex/store' 

  export default { 
    store, 
    <...> 
  } 
</script> 

我们现在将定义每个应用程序中的全局状态和局部状态,定义缺少的数据和绑定,划分数据,并使用我们刚学到的内容添加所有缺失的内容。

在购物清单应用程序中使用 Vuex store

我希望您还记得我们在本章开头面临的挑战。我们希望在组件之间建立通信,以便可以轻松地从ChangeTitleComponent更改购物清单的标题,并将其传播到ShoppingListTitleShoppingListComponent。让我们从App.vue中删除硬编码的购物清单数组,并将其复制到 store 的状态中:

//store.js 
<...> 
const state = { 
  **shoppinglists**: [ 
    { 
      id: 'groceries', 
      title: 'Groceries', 
      items: [{ text: 'Bananas', checked: true }, 
              { text: 'Apples', checked: false }] 
    }, 
    { 
      id: 'clothes', 
      title: 'Clothes', 
      items: [{ text: 'black dress', checked: false }, 
              { text: 'all-stars', checked: false }] 
    } 
  ] 
} 

<...> 

让我们为购物清单定义 getter:

//getters.js 
export default { 
  getLists: state => state.shoppinglists 
} 

现在,在App.vue中导入mapGetters,并将shoppinglists值映射到getLists方法,以便App.vue组件内的<script>标签看起来像下面这样:

//App.vue 
<script> 
  import ShoppingListComponent from './components/ShoppingListComponent' 
  import ShoppingListTitleComponent from  
  './components/ShoppingListTitleComponent' 
  import _ from 'underscore' 
  **import store from './vuex/store' 
  import { mapGetters } from 'vuex'** 

  export default { 
    components: { 
      ShoppingListComponent, 
      ShoppingListTitleComponent 
    }, 
    **computed: mapGetters({ 
      shoppinglists: 'getLists' 
    }),** 
    methods: { 
      onChangeTitle (id, text) { 
        _.findWhere(this.shoppinglists, { id: id }).title = text 
      } 
    }, 
    store 
  } 
</script> 

其余部分保持不变!

现在让我们在存储中定义一个负责更改标题的 mutation。很明显,它应该是一个接收新标题字符串作为参数的函数。但是,有一些困难。我们不知道应该更改哪个购物清单的标题。如果我们可以从组件将列表的 ID 传递给此函数,实际上我们可以编写一段代码来通过其 ID 找到正确的列表。我刚刚说如果我们可以?当然可以!实际上,我们的ShoppingListComponent已经从其父级App.vue接收了 ID。让我们只是将这个 ID 从ShoppingListComponent传递给ChangeTitleComponent。这样,我们将能够从实际更改标题的组件中调用必要的操作,而无需通过父级链传播事件。

因此,只需将 ID 绑定到ShoppingListComponent组件模板中的change-title-component,如下所示:

//ShoppingListComponent.vue 
<template> 
  <...> 
      <change-title-component : **:id="id"** v- 
        on:changeTitle="onChangeTitle"></change-title-component> 
  <...> 
</template> 

不要忘记向ChangeTitleComponent组件的props属性添加id属性:

//ChangeTitleComponent.vue 
<script> 
  export default { 
    props: ['title', **'id'**], 
    <...> 
  } 
</script> 

现在,我们的ChangeTitleComponent可以访问购物清单的titleid。让我们向存储中添加相应的 mutation。

我们可以先编写一个通过其 ID 查找购物清单的函数。为此,我将使用underscore类的_.findWhere方法,就像我们在App.vue组件的changeTitle方法中所做的那样。

mutations.js中导入underscore并添加findById函数如下:

//mutations.js 
<...> 
function findById (state, id) { 
  return **_.findWhere(state.shoppinglists, { id: id })** 
} 
<...> 

现在让我们添加 mutation,并将其命名为CHANGE_TITLE。此 mutation 将接收data对象作为参数,其中包含titleid,并将接收到的标题值分配给找到的购物清单项的标题。首先,在mutation_types.js中声明一个常量CHANGE_TITLE,并重用它而不是将 mutation 的名称写为字符串:

//mutation_types.js 
export const **CHANGE_TITLE** = 'CHANGE_TITLE' 

//mutations.js 
import _ from 'underscore' 
**import * as types from './mutation_types'** 

function findById (state, id) { 
  return _.findWhere(state.shoppinglists, { id: id }) 
} 

export default { 
  **[types.CHANGE_TITLE] (state, data) { 
    findById(state, data.id).title = data.title 
  }** 
} 

我们快要完成了。现在让我们在actions.js文件中定义一个changeTitle操作,并在我们的ChangeTitleComponent中重用它。打开actions.js文件并添加以下代码:

//actions.js 
import { CHANGE_TITLE } from './mutation_types' 

export default { 
  changeTitle: ({ commit }, data) => { 
    **commit(CHANGE_TITLE, data)** 
  } 
} 

最后一步。打开ChangeTitleComponent.vue,导入mapActions辅助程序,将onInput方法映射到changeTitle操作,并在template中调用它,对象映射标题为event.target.value和 ID 为id参数。因此,ChangeTitleComponent的代码将如下所示:

//ChangeTitleComponent.vue 
<template> 
  <div> 
    <em>Change the title of your shopping list here</em> 
    <input :value="title" **@input="onInput({ title: $event.target.value,** 
 **id: id })"**/> 
  </div> 
</template> 

<script> 
  **import { mapActions } from 'vuex'** 

  export default { 
    props: ['title', 'id'], 
    **methods: mapActions({ 
      onInput: 'changeTitle' 
    })** 
  } 
</script> 

现在,您可以从ShoppingListComponent和主App组件中删除所有事件处理代码。

打开页面并尝试在输入框中输入!标题将在所有位置更改:

在购物清单应用程序中使用 Vuex 存储

使用存储、突变和操作——所有组件都可以更新其状态,而无需事件处理机制

应用存储功能后购物清单应用程序的最终代码可以在chapter5/shopping-list3文件夹中找到。

在 Pomodoro 应用程序中使用 Vuex 存储

最后,我们回到了我们的 Pomodoro!你上次休息了多久?让我们使用 Vuex 架构构建我们的 Pomodoro 应用程序,然后休息一下,看看小猫。让我们从chapter5/pomodoro文件夹中的基础开始,您已经包含了 Vuex 存储的基本结构(如果没有,请转到在我们的应用程序中安装和使用 Vuex 存储部分的开头)。

为启动、暂停和停止按钮注入生命

让我们首先分析我们的番茄钟定时器实际上可以做什么。看看页面。我们只有三个按钮:启动、暂停和停止。这意味着我们的应用程序可以处于这三种状态之一。让我们在store.js文件中定义并导出它们:

//store.js 
<...> 
const state = { 
  **started**: false, 
  **paused**: false, 
  **stopped**: false 
} 
<...> 

最初,所有这些状态都设置为false,这是有道理的,因为应用程序尚未启动,尚未暂停,当然也没有停止!

现在让我们为这些状态定义 getter。打开getters.js文件,并为所有三种状态添加 getter 函数:

//getters.js 
export default { 
  **isStarted**: state => state.started, 
  **isPaused**: state => state.paused, 
  **isStopped**: state => state.stopped 
} 

对于每个定义的状态,我们的控制按钮应该发生什么变化:

  • 当应用程序启动时,启动按钮应该变为禁用。然而,当应用程序暂停时,它应该再次启用,以便我们可以使用此按钮恢复应用程序。

  • 暂停按钮只能在应用程序启动时启用(因为我们不能暂停尚未启动的东西)。但是,如果应用程序已暂停,它应该被禁用(因为我们不能暂停已经暂停的东西)。

  • 停止按钮只能在应用程序启动时启用。

让我们通过根据应用程序状态有条件地向我们的控制按钮添加disabled类来将其翻译成代码。

提示

一旦我们应用了disabled类,Bootstrap 将通过不仅应用特殊样式而且禁用交互元素来为我们处理按钮的行为。

为了能够使用已定义的 getter,我们必须在组件的<script>标签中导入mapGetters。之后,我们必须通过在computed属性对象中导出它们来告诉组件我们想要使用它们:

//ControlsComponent.vue 
<script> 
  **import { mapGetters } from 'vuex'** 

  export default { 
    **computed: mapGetters(['isStarted', 'isPaused', 'isStopped'])** 
  } 
</script> 

现在这些 getter 可以在模板中使用。因此,我们将把disabled类应用于以下内容:

  • 当应用程序启动且未暂停时启动按钮(isStarted && !isPaused

  • 当应用程序未启动或已暂停时暂停按钮(!isStarted || isPaused

  • 当应用程序未启动时停止按钮(!isStarted

我们的模板现在看起来像这样:

//ControlsComponent.vue 
<template> 
  <span> 
    <button  **:disabled='isStarted && !isPaused'**> 
      <i class="glyphicon glyphicon-play"></i> 
    </button> 
    <button  **:disabled='!isStarted || isPaused'**> 
      <i class="glyphicon glyphicon-pause"></i> 
    </button> 
    <button  **:disabled='!isStarted'**> 
      <i class="glyphicon glyphicon-stop"></i> 
    </button> 
  </span> 
</template> 

现在你看到暂停和停止按钮看起来不同了!如果你将鼠标悬停在它们上面,光标不会改变,这意味着它们真的被禁用了!让我们为禁用按钮内部的图标创建一个样式,以更加突出禁用状态:

 //ControlsComponent.vue 
 <style scoped> 
  **button:disabled i { 
    color: gray; 
  }** 
</style> 

好了,现在我们有了漂亮的禁用按钮,让我们为它们注入一些生命吧!

让我们考虑一下当我们启动、暂停或停止应用程序时实际上应该发生什么:

  • 当我们启动应用程序时,状态started应该变为true,而pausedstopped状态肯定会变为false

  • 当我们暂停应用程序时,状态pausedtrue,状态stoppedfalse,而状态startedtrue,因为暂停的应用程序仍然是启动的。

  • 当我们停止应用程序时,状态stopped变为true,而pausedstarted状态变为false。让我们将所有这些行为转化为 mutation_types、mutations 和 actions!

打开mutation_types.js并添加三种 mutation 类型如下:

//mutation_types.js 
export const START = 'START' 
export const PAUSE = 'PAUSE' 
export const STOP = 'STOP' 

现在让我们定义 mutations!打开mutations.js文件并为每种 mutation 类型添加三种 mutations。因此,我们决定当我们:

  • 启动应用程序:状态startedtrue,而状态pausedstoppedfalse

  • 暂停应用程序:状态startedtrue,状态pausedtrue,而stoppedfalse

  • 停止应用程序:状态stoppedtrue,而状态startedpausedfalse

现在让我们把它放到代码中。将mutation_types导入到mutations.js中,并编写所有三个必要的 mutations 如下:

//mutations.js 
import * as types from './mutation_types' 

export default { 
  [types.START] (state) { 
    state.started = true 
    state.paused = false 
    state.stopped = false 
  }, 
  [types.PAUSE] (state) { 
    state.paused = true 
    state.started = true 
    state.stopped = false 
  }, 
  [types.STOP] (state) { 
    state.stopped = true 
    state.paused = false 
    state.started = false 
  } 
} 

现在让我们定义我们的 actions!转到actions.js文件,导入 mutation 类型,并导出三个函数:

//actions.js 
import * as types from './mutation_types' 

export default { 
  start: ({ commit }) => { 
    **commit(types.START)** 
  }, 
  pause: ({ commit }) => { 
    **commit(types.PAUSE)** 
  }, 
  stop: ({ commit }) => { 
    **commit(types.STOP)** 
  } 
} 

为了使我们的按钮生效,最后一步是将这些 actions 导入到ControlsComponent中,并在每个按钮的click事件上调用它们。让我们来做吧。你还记得如何在 HTML 元素上应用事件时调用 action 吗?如果我们谈论的是click事件,就是下面这样的:

@click='someAction' 

因此,在我们的ControlsComponent.vue中,我们导入mapActions对象,将其映射到组件的methods属性,并将其应用于相应按钮的点击事件。就是这样!ControlsComponent<script>标签看起来像下面这样:

//ControlsComponent.vue 
<script> 
  **import { mapGetters, mapActions } from 'vuex'** 

  export default { 
    computed: mapGetters(['isStarted', 'isPaused', 'isStopped']), 
    **methods: mapActions(['start', 'stop', 'pause'])** 
  } 
</script> 

现在在模板中的事件处理程序指令内调用这些函数,使得ControlsComponent<template>标签看起来像下面这样:

//ControlsComponent.vue 
<template> 
  <span> 
    <button  :disabled='isStarted && !isPaused'
    **@click="start"**> 
      <i class="glyphicon glyphicon-play"></i> 
    </button> 
    <button  :disabled='!isStarted || isPaused' 
    **@click="pause"**> 
      <i class="glyphicon glyphicon-pause"></i> 
    </button> 
    <button  :disabled='!isStarted' **@click="stop"**> 
      <i class="glyphicon glyphicon-stop"></i> 
    </button> 
  </span> 
</template> 

尝试点击按钮。它们确实做到了我们需要它们做的事情。干得漂亮!在chapter5/pomodoro2文件夹中查看。然而,我们还没有完成。我们仍然需要将我们的番茄钟定时器变成一个真正的定时器,而不仅仅是一些允许您点击按钮并观察它们从禁用状态到启用状态的页面。

绑定番茄钟的分钟和秒

在上一节中,我们能够定义番茄钟应用的三种不同状态:开始暂停停止。然而,让我们不要忘记番茄钟应用应该用于什么。它必须倒计时一定的工作时间,然后切换到休息倒计时器,然后再回到工作,依此类推。

这让我们意识到,还有一个非常重要的番茄钟应用状态:在工作休息时间段之间切换的二进制状态。这个状态不能由按钮切换;它应该以某种方式由我们应用的内部逻辑来管理。

让我们首先定义两个状态属性:一个用于随着时间减少的计数器,另一个用于区分工作状态和非工作状态。假设当我们开始番茄钟时,我们开始工作,所以工作状态应该设置为 true,倒计时计数器应该设置为我们定义的工作番茄钟时间。为了模块化和可维护性,让我们在外部文件中定义工作和休息的时间。比如,我们称之为config.js。在项目的根目录下创建config.js文件,并添加以下内容:

**//config.js** 
export const WORKING_TIME = **20 * 60** 
export const RESTING_TIME = **5 * 60** 

通过这些初始化,我的意思是我们的番茄钟应该倒计时20分钟的工作番茄钟间隔和5分钟的休息时间。当然,你可以自由定义最适合你的值。现在让我们在我们的存储中导出config.js,并重用WORKING_TIME值来初始化我们的计数器。让我们还创建一个在工作/休息之间切换的属性,并将其命名为isWorking。让我们将其初始化为true

所以,我们的新状态将如下所示:

//store.js 
<...> 
import { WORKING_TIME } from '../config' 

const state = { 
  started: false, 
  paused: false, 
  stopped: false, 
  **isWorking: true, 
  counter: WORKING_TIME** 
} 

所以,我们有了这两个新的属性。在开始创建方法、操作、突变和其他减少计数器和切换isWorking属性的事情之前,让我们考虑依赖这些属性的可视元素。

我们没有那么多元素,所以很容易定义。

  • isWorking状态影响标题:当是工作时间时,我们应该显示工作!,当是休息时间时,我们应该显示休息!

  • isWorking状态也影响着小猫组件的可见性:只有当isWorkingfalse时才应该显示。

  • counter属性影响minutesecond:每次它减少时,second的值也应该减少,每减少 60 次,minute的值也应该减少。

让我们为isWorking状态和minutesecond定义获取函数。在定义这些获取函数之后,我们可以在我们的组件中重用它们,而不是使用硬编码的值。让我们首先定义一个用于isWorking属性的获取器:

//getters.js 
export default { 
  isStarted: state => state.started, 
  isPaused: state => state.paused, 
  isStopped: state => state.stopped, 
  **isWorking: state => state.isWorking** 
} 

让我们在使用在App.vue组件中定义的硬编码isworking的组件中重用此 getter。 打开App.vue,删除对isworking硬编码变量的所有引用,导入mapGetters对象,并将isworking属性映射到computed属性中的isWorking方法,如下所示:

//App.vue 
<script> 
<...> 
**import { mapGetters } from 'vuex'** 

export default { 
  <...> 
  **computed: mapGetters({ 
    isworking: 'isWorking' 
  }),** 
  store 
} 
</script> 

StateTitleComponent中重复相同的步骤。 导入mapGetters并用映射的computed属性替换props

//StateTitleComponent.vue 
<script> 
  **import { mapGetters } from 'vuex'** 

  export default { 
    data () { 
      return { 
        workingtitle: 'Work!', 
        restingtitle: 'Rest!' 
      } 
    }, 
    **computed: mapGetters({ 
      isworking: 'isWorking' 
    })** 
  } 
</script> 

这两个组件中的其余部分保持不变! 在模板内,使用isworking属性。 此属性仍然存在; 它只是从响应式的 Vuex 存储中导入,而不是从硬编码数据中导入!

现在我们必须为分钟和秒定义 getter。 这部分比较棘手,因为在这些 getter 中,我们必须对计数器状态的属性应用一些计算。 这一点一点也不难。 我们的计数器表示总秒数。 这意味着我们可以通过将计数器除以 60 并四舍五入到最低整数(Math.floor)来轻松提取分钟。 秒数可以通过取除以 60 的余数来提取。 因此,我们可以以以下方式编写我们的分钟和秒的 getter:

//getters.js 
export default { 
  <...> 
  **getMinutes**: state => **Math.floor(state.counter / 60)**, 
  **getSeconds**: state => **state.counter % 60** 
} 

就是这样! 现在让我们在CountdownComponent中重用这些 getter。 导入mapGetters并将其相应的方法映射到computed属性中的minsec属性。 不要忘记删除硬编码的数据。 因此,我们的CountdownComponent.vuescript标签如下所示:

//CountdownComponent.vue 
<script> 
  **import { mapGetters } from 'vuex'** 

  export default { 
    **computed: mapGetters({ 
      min: 'getMinutes', 
      sec: 'getSeconds' 
    })** 
  } 
</script> 

其余部分完全不变! 模板引用了minsec属性,它们仍然存在。 到目前为止的代码可以在chapter5/pomodoro3文件夹中找到。 看看页面; 现在显示的分钟和秒数对应于我们在配置文件中定义的工作时间! 如果您更改它,它也会随之更改:

绑定番茄钟分钟和秒

更改工作时间的配置将立即影响番茄钟应用程序视图

创建番茄钟定时器

好的,现在一切准备就绪,可以开始倒计时我们的工作时间,这样我们最终可以休息一下! 让我们定义两个辅助函数,togglePomodorotick

第一个将只是切换isWorking属性。它还将重新定义状态的计数器。当状态为isWorking时,计数器应该对应工作时间,当状态不工作时,计数器应该对应休息时间。

tick函数将只是减少计数器并检查是否已达到“0”值,在这种情况下,将切换 Pomodoro 状态。其余的已经被照顾好了。因此,togglePomodoro`函数将如下所示:

//mutations.js 
function togglePomodoro (state, toggle) { 
  if (_.isBoolean(toggle) === false) { 
    toggle = **!state.isWorking** 
  } 
  **state.isWorking = toggle 
  state.counter = state.isWorking ? WORKING_TIME : RESTING_TIME** } 

啊,不要忘记从我们的配置中导入WORKING_TIMERESTING_TIME!还有,不要忘记导入underscore,因为我们在_.isBoolean检查中使用它:

//mutations.js 
import _ from 'underscore' 
import { WORKING_TIME, RESTING_TIME } from './config' 

然后,tick函数将只是减少计数器并检查是否已达到“0”值:

//mutations.js 
function tick (state) { 
  if (state.counter === 0) { 
    togglePomodoro(state) 
  } 
  state.counter-- 
} 

好的!还不够。我们需要设置一个间隔,每秒调用一次tick函数。它应该在哪里设置?嗯,很明显,当我们开始 Pomodoro 时,应该这样做在START变异中!

但是,如果我们在START变异中设置了间隔,并且它每秒调用一次tick函数,那么在点击暂停或停止按钮时,它将如何停止或暂停?这就是为什么存在setIntervalclearInterval JavaScript 函数,这也是为什么我们有一个存储可以保存interval值的初始状态的地方!让我们首先在存储状态中将interval定义为null

//store.js 
const state = { 
  <...> 
  interval: null 
} 

现在,在我们的START变异中,让我们添加以下代码来初始化间隔:

//mutations.js 
export default { 
  [types.START] (state) { 
    state.started = true 
    state.paused = false 
    state.stopped = false 
    **state.interval = setInterval(() => tick(state), 1000)** 
  }, 
  <...> 
} 

我们刚刚设置了一个间隔,每秒调用一次tick函数。反过来,tick函数将减少计数器。依赖于计数器值的值——分钟和秒——将改变,并且会将这些更改反应地传播到视图中。

如果你现在点击开始按钮,你将启动倒计时!耶!几乎完成了。我们只需要在pausestop变异方法上添加clearInterval函数。除此之外,在stop方法上,让我们调用togglePomodoro函数,并传入true,这将重置 Pomodoro 计时器到工作状态:

//mutations.js 
export default { 
  [types.START] (state) { 
    state.started = true 
    state.paused = false 
    state.stopped = false 
    **state.interval = setInterval(() => tick(state), 1000)** 
  }, 
  [types.PAUSE] (state) { 
    state.paused = true 
    state.started = true 
    state.stopped = false 
    **clearInterval(state.interval)** 
  }, 
  [types.STOP] (state) { 
    state.stopped = true 
    state.paused = false 
    state.started = false 
    **togglePomodoro(state, true)** 
  } 
} 

更改小猫咪

我希望你工作了很多,你的休息时间终于到了!如果没有,或者如果你等不及了,只需在config.js文件中将WORKING_TIME的值更改为相当小的值,然后等待。我认为我终于应该休息一下了,所以我已经盯着这张漂亮的图片看了几分钟了:

更改小猫咪

我盯着这张图片,猫也盯着我。

你不想有时候显示的图片改变吗?当然想!为了实现这一点,我们只需向图像源附加一些内容,以便随着时间的推移而改变,并向我们提供一个非缓存的图像。

提示

提供非缓存内容的最佳实践之一是将时间戳附加到请求的 URL 中。

例如,我们可以在存储中有另一个属性,比如timestamp,它将随着每次计数器减少而更新,并且它的值将被附加到图像源 URL。让我们做吧!让我们首先在我们存储的状态中定义一个timestamp属性,如下所示:

//store.js 
const state = { 
  <...> 
  **timestamp: 0** 
} 

告诉tick函数在每次滴答时更新这个值:

//mutations.js 
function tick(state) { 
  <...> 
  **state.timestamp = new Date().getTime()** 
} 

getters.js中为这个值创建 getter,并在KittensComponent中使用它,通过在computed属性中访问this.$store.getters.getTimestamp方法:

//getters.js 
export default { 
  <...> 
  **getTimestamp: state => state.timestamp** 
} 

//KittensComponent.vue 
<script> 
  export default { 
    computed: { 
      catimgsrc () { 
        return 'http://thecatapi.com/api/images/get?size=med**&ts='** 
 **+ this.$store.getters.getTimestamp** 
      } 
    } 
  } 
</script> 

现在速度有点太快了,对吧?让我们定义一个时间来展示每只小猫。这一点都不难。例如,如果我们决定每只小猫展示 3 秒钟,在tick函数内改变时间戳状态之前,我们只需要检查计数器的值是否可以被 3 整除。让我们也把展示小猫的秒数变成可配置的。在config.js中添加以下内容:

//config.js 
export const WORKING_TIME = 0.1 * 60 
export const RESTING_TIME = 5 * 60 
**export const KITTEN_TIME = 5** //each kitten is visible for 5 seconds 

现在将其导入到mutations.js文件中,并在tick函数中使用它来检查是否是改变时间戳值的时候:

//mutations.js 
import { WORKING_TIME, RESTING_TIME, **KITTEN_TIME** } from './config' 
<...> 
function tick(state) { 
  <...> 
  **if (state.counter % KITTEN_TIME === 0) { 
    state.timestamp = new Date().getTime() 
  }** 
} 

我们完成了!您可以在chapter5/pomodoro4文件夹中检查本节的最终代码。是的,我将工作时间设置为 6 秒,这样您就可以休息一下,并欣赏一些来自thecatapi.com的非常可爱的小猫。

因此,在阅读本章摘要并开始下一章之前,休息一下!就像这个美妙的物种一样:

改变小猫

美好的事物需要休息。像它一样。休息一下。

总结

在本章中,您看到了如何使用事件处理和触发机制来将组件的数据更改传播到它们的父级。

最重要的是,您利用了 Vuex 架构的力量,能够在组件之间建立数据流。您看到了如何创建存储库以及其主要部分,即 mutations 和 states。您学会了如何构建使用存储库的应用程序,使其变得模块化和可维护。您还学会了如何创建存储库的 getters 以及如何定义分派存储库状态变化的 actions。我们将所有学到的机制应用到我们的应用程序中,并看到了数据流的实际操作。

到目前为止,我们能够在 Vue 应用程序中使用任何数据交换机制,从简单的组件内部的本地数据绑定开始,逐渐扩展到全局状态管理。到目前为止,我们已经掌握了在 Vue 应用程序中操作数据的所有基础知识。我们快要完成了!

在下一章中,我们将深入探讨 Vue 应用程序的插件系统。您将学习如何使用现有的插件并创建自己的插件,以丰富您的应用程序的自定义行为。

第六章:插件-用自己的砖头建造你的房子

在上一章中,你学会了如何使用 Vuex 架构管理全局应用程序存储。你学到了很多新概念并应用了它们。你还学会了如何创建一个存储,如何定义它的状态和变化,以及如何使用操作和获取器。我们利用在这一章中获得的知识,让我们的购物清单和番茄钟应用程序焕发生机。

在这一章中,我们将重新审视 Vue 插件,看看它们是如何工作的,以及它们必须如何创建。我们将使用一些现有的插件并创建我们自己的插件。

总结一下,在这一章中,我们将做以下事情:

  • 了解 Vue 插件的性质

  • 在购物清单应用程序中使用资源插件

  • 创建一个生成白色、粉色和棕色噪音的插件,并将其应用到我们的番茄钟应用程序中

Vue 插件的性质

在 Vue.js 中,插件的用途与在任何其他范围中使用的目的完全相同:为系统的核心功能无法实现的一些良好功能添加一些功能。为 Vue 编写的插件可以提供各种功能,从定义一些全局 Vue 方法,甚至实例方法,到提供一些新的指令、过滤器或转换。

为了能够使用现有的插件,你必须首先安装它:

**npm install some-plugin --save-dev** 

然后,告诉 Vue 在你的应用程序中使用它:

var Vue = require('vue') 
var SomePlugin = require('some-plugin') 

**Vue.use(SomePlugin)** 

我们也可以创建我们自己的插件。这也很容易。你的插件必须提供一个install方法,在这个方法中你可以定义任何全局或实例方法,或自定义指令:

MyPlugin.**install** = function (Vue, options) { 
  // 1\. add global method or property 
  Vue.**myGlobalMethod** = ... 
  // 2\. add a global asset 
  Vue.**directive**('my-directive', {}) 
  // 3\. add an instance method 
  Vue.prototype.**$myMethod** = ... 
} 

然后它可以像任何其他现有的插件一样使用。在这一章中,我们将使用现有的resource插件为 Vue(github.com/vuejs/vue-resource)并创建我们自己的插件,生成白色、粉色和棕色噪音。

在购物清单应用程序中使用 vue-resource 插件

打开购物清单应用程序(chapter6/shopping-list文件夹)并运行npm installnpm run dev。这很好,但它仍然使用硬编码的购物清单列表。如果我们能够添加新的购物清单,删除它们,并存储有关更新后的购物清单的信息,那将非常好,这样当我们重新启动应用程序时,显示的信息将与重新启动前看到的信息相对应。为了能够做到这一点,我们将使用resource插件,它允许我们轻松创建 REST 资源并在其上调用 REST 方法。在开始之前,让我们总结一下我们需要做的一切:

  • 首先,我们需要一个简单的服务器,其中包含一些存储,我们可以从中检索和存储我们的购物清单。这个服务器必须为所有这些功能提供所需的端点。

  • 创建我们的服务器和所有所需的端点后,我们应该安装并使用vue-resource插件来创建一个资源和调用提供的端点上的方法。

  • 为了保证数据的完整性,我们应该调用更新服务器状态的操作,以便在每次购物清单更新时更新服务器的状态。

  • 在应用程序启动时,我们应该从服务器获取购物清单并将它们分配给我们存储的状态。

  • 我们还应该提供一个机制来创建新的购物清单并删除现有的清单。

听起来不太困难,对吧?那么让我们开始吧!

创建一个简单的服务器

为了简单起见,我们将使用一个非常基本和易于使用的 HTTP 服务器,它将数据存储在一个常规的 JSON 文件中。它被称为json-server,托管在github.com/typicode/json-server。在购物清单应用程序的目录中安装它:

**cd shopping-list 
npm install --save-dev json-server** 

创建一个带有db.json文件的server文件夹,并在其中添加以下内容:

//shopping-list/server/db.json 
{ 
  "shoppinglists": [ 
  ] 
} 

这将是我们的数据库。让我们向package.json文件添加脚本条目,以便我们可以轻松地启动我们的服务器:

  "scripts": { 
    "dev": "node build/dev-server.js ", 
    **"server": "node_modules/json-server/bin/index.js --watch  
    server/db.json"**, 
    <...> 
  }, 

现在,要启动服务器,只需运行以下命令:

**cd shopping-list 
npm run server** 

http://localhost:3000/shoppinglists上打开浏览器页面。您将看到一个空数组作为结果。这是因为我们的数据库仍然是空的。尝试使用curl插入一些数据:

**curl -H "Content-Type:application/json" -d '{"title":"new","items":[]}' http://localhost:3000/shoppinglists** 

如果现在刷新页面,您将看到您新插入的值。

现在我们的简单 REST 服务器已经启动运行,让我们借助vue-resource插件在我们的购物清单应用程序中使用它!

安装 vue-resource,创建资源及其方法

在深入使用vue-resource插件之前,请查看其文档github.com/vuejs/vue-resource/blob/master/docs/resource.md。基本上,文档提供了一种根据给定 URL(在我们的情况下,将是http://localhost:3000/shoppinglists)创建资源的简单方法。创建资源后,我们可以在其上调用getdeletepostupdate方法。

在项目文件夹中安装它:

**cd shopping-list 
npm install vue-resource --save-dev** 

现在让我们为我们的 API 创建入口点。在购物清单应用程序的src文件夹内,创建一个子文件夹并将其命名为api。在其中创建一个index.js文件。在这个文件中,我们将导入vue-resource插件并告诉Vue去使用它:

**//api/index.js** 
import Vue from 'vue' 
import VueResource from 'vue-resource' 

Vue.use(VueResource) 

很好!现在我们准备创建ShoppingListsResource并为其附加一些方法。使用vue-resource插件创建资源,我们只需在Vue上调用resource方法并将 URL 传递给它:

const ShoppingListsResource = Vue.resource(**'http://localhost:3000/' + 'shoppinglists{/id}'**) 

ShoppingListsResource常量现在公开了实现CRUD创建,读取,更新和删除)操作所需的所有方法。它非常容易使用,以至于我们基本上可以导出资源本身。但让我们为每个 CRUD 操作导出好的方法:

export default { 
  fetchShoppingLists: () => { 
    return **ShoppingListsResource.get()** 
  }, 
  addNewShoppingList: (data) => { 
    return **ShoppingListsResource.save(data)** 
  }, 
  updateShoppingList: (data) => { 
    return **ShoppingListsResource.update({ id: data.id }, data)** 
  }, 
  deleteShoppingList: (id) => { 
    return **ShoppingListsResource.remove({ id: id })** 
  } 
} 

api/index.js文件的完整代码可以在此处的 gist 中查看gist.github.com/chudaol/d5176b88ba2c5799c0b7b0dd33ac0426

就是这样!我们的 API 已经准备好使用并填充我们的响应式 Vue 数据!

获取应用程序开始的所有购物清单

让我们首先创建一个操作,该操作将获取并填充存储的shoppinglists状态。创建后,我们可以在主App.vue准备状态上调用它。

mutation_types.js文件中定义一个常量,如下所示:

//mutation_types.js 
export const POPULATE_SHOPPING_LISTS = 'POPULATE_SHOPPING_LISTS' 

现在创建一个 mutation。这个 mutation 将只接收一个shoppinglists数组并将其分配给shoppinglists状态:

//mutations.js 
export default { 
  [types.CHANGE_TITLE] (state, data) { 
    findById(state, data.id).title = data.title 
  }, 
  **[types.POPULATE_SHOPPING_LISTS] (state, lists) { 
    state.shoppinglists = lists 
  }** 
} 

好了!现在我们只需要一个使用 API 的get方法并分派填充 mutation 的操作。在actions.js文件中导入 API 并创建相应的 action 方法:

import { CHANGE_TITLE, POPULATE_SHOPPING_LISTS } from './mutation_types' 
**import api from '../api'** 

export default { 
  changeTitle: ({ commit }, data) => { 
    commit(CHANGE_TITLE, data) 
  }, 
  **populateShoppingLists: ({ commit }) => { 
    api.fetchShoppingLists().then(response => { 
      commit(POPULATE_SHOPPING_LISTS, response.data) 
    })** 
  } 
} 

在上述代码的前面几行中,我们执行了一个非常简单的任务——调用fetchShoppingLists API 的方法,该方法反过来调用资源的get方法。这个方法执行一个http GET调用,并在数据从服务器返回时解析一个 promise。

然后使用这些数据来分发填充变异。这种方法将把这些数据分配给存储的状态shoppinglists属性。这个属性是响应式的;你还记得吗?这意味着依赖于shoppinglists属性 getter 的所有视图都将被更新。现在让我们在主App.vue组件的mounted状态中使用这个操作。在官方 Vue 文档页面的mounted状态钩子中查看更多信息。

打开App.vue组件,导入mapActions对象,在组件的methods属性中映射populateShoppingLists操作,并在mounted处理程序中调用它。因此,在更改后,App.vuescript标签如下所示:

<script> 
  import ShoppingListComponent from './components/ShoppingListComponent' 
  import ShoppingListTitleComponent from   
  './components/ShoppingListTitleComponent' 
  import store from './vuex/store' 
  import { mapGetters, **mapActions** } from 'vuex' 

  export default { 
    components: { 
      ShoppingListComponent, 
      ShoppingListTitleComponent 
    }, 
    computed: mapGetters({ 
      shoppinglists: 'getLists' 
    }), 
    **methods: mapActions(['populateShoppingLists']),** 
    store, 
    **mounted () { 
      this.populateShoppingLists() 
    }** 
  } 
</script> 

如果您现在打开页面,您将看到我们使用curl创建的唯一购物清单,如下面的屏幕截图所示:

获取应用程序启动时的所有购物清单

显示的购物清单是由我们简单的服务器提供的!

尝试使用curl添加更多项目,甚至直接修改db.json文件。刷新页面,看看它是如何像魅力一样工作的!

在更改时更新服务器状态

非常好,现在我们的购物清单是由我们的 REST API 提供的,一切都运作良好并且看起来很好。尝试添加一些购物清单项目或更改购物清单的标题,并检查或取消检查项目。在所有这些交互之后,刷新页面。哎呀,列表是空的,什么也没发生。这完全正确,我们有一个用于更新给定购物清单的 API 方法,但我们没有在任何地方调用它,因此我们的服务器不知道应用的更改。

让我们首先定义哪些组件对我们的购物清单进行了一些操作,以便将这些更改发送到服务器。购物清单及其项目可能发生以下三种情况:

  • 清单的标题可以在ChangeTitleComponent中更改

  • 新项目可以在AddItemComponent中添加到购物清单

  • 购物清单中的项目可以在ItemComponent中进行勾选或取消勾选

我们必须创建一个必须在所有这些更改上触发的动作。在此动作中,我们应该调用update API 的方法。仔细查看api/index.js模块中的更新方法;它必须接收整个购物清单对象作为参数:

//api/index.js 
updateShoppingList: (**data**) => { 
  return ShoppingListsResource.update(**{ id: data.id }, data**) 
} 

让我们创建一个接收id作为参数的动作,通过其 ID 检索购物清单,并调用 API 的方法。在此之前,在getters.js文件中创建一个getListById方法,并将其导入到动作中:

//getters.js 
**import _ from 'underscore'** 

export default { 
  getLists: state => state.shoppinglists, 
  **getListById: (state, id) => { 
    return _.findWhere(state.shoppinglists, { id: id }) 
  }** 
} 

//actions.js 
**import getters from './getters'** 

现在我们准备定义更新购物清单的动作:

//actions.js 
<...> 
export default { 
  <...> 
  updateList: (store, id) => { 
    let shoppingList = **getters.getListById**(store.state, id) 

    **api.updateShoppingList(shoppingList)** 
  } 
} 

实际上,我们现在可以从mutations.js中删除findById方法,只需从getters.js中重用此方法:

//mutations.js 
import * as types from './mutation_types' 
**import getters from './getters'** 

export default { 
  [types.CHANGE_TITLE] (state, data) { 
    **getters.getListById**(state, data.id).title = data.title 
  }, 
  [types.POPULATE_SHOPPING_LISTS] (state, lists) { 
    state.shoppinglists = lists 
  } 
} 

好了,现在我们已经定义了调用 API 的updateList方法的动作。现在我们只需在组件内部发生的每个更改上调用该动作!

让我们从AddItemComponent开始。我们必须在addItem方法中使用this.$store.dispatch方法分派updateList动作,使用动作的名称。但是,有一个小问题 - 我们必须将列表项 ID 传递给updateList方法,而我们在此组件内部没有对其的引用。但这实际上很容易解决。只需在组件的props中添加 ID,并将其绑定到ShoppingListComponent中的组件调用。因此,我们的AddItemComponent组件的script标签如下所示:

//AddItemComponent.vue 
<script> 
  export default { 
    **props: ['id']**, 
    data () { 
      return { 
        newItem: '' 
      } 
    }, 
    methods: { 
      addItem () { 
        var text 

        text = this.newItem.trim() 
        if (text) { 
          this.$emit('add', this.newItem) 
          this.newItem = '' 
          **this.$store.dispatch('updateList', this.id)** 
        } 
      } 
    } 
  } 
</script> 

而且,在ShoppingListComponent中,在add-item-component调用时,将 ID 绑定到它:

//ShoppingListComponent.vue 
<template> 
  <...> 
    <add-item-component **:id="id"** @add="addItem"></add-item-component> 
  <...> 
</template> 

现在,如果您尝试向购物清单添加项目并刷新页面,新添加的项目将出现在列表中!

现在我们应该对ChangeTitleComponent做同样的事情。打开ChangeTitleComponent.vue文件并检查代码。现在,它在输入时调用changeTitle动作:

//ChangeTitleComponent.vue 
<template> 
  <div> 
    <em>Change the title of your shopping list here</em> 
    <input :value="title" **@input="onInput({ title: 
      $event.target.value, id: id })"**/> 
  </div> 
</template> 

<script> 
  **import { mapActions } from 'vuex'** 

  export default { 
    props: ['title', 'id'], 
    **methods: mapActions({ 
      onInput: 'changeTitle'** 
    }) 
  } 
</script> 

当然,我们可以导入updateList动作,并在调用changeTitle动作后立即调用它。但是在动作本身内部执行可能更容易。您可能记得,为了调度存储的动作,我们应该调用应用于存储的dispatch方法,并将动作的名称作为参数。因此,我们可以在changeTitle动作内部执行。只需打开action.js文件,找到我们的changeTitle动作,并添加对updateList的调用:

//actions.js 
export default { 
  changeTitle: (store, data) => { 
    store.commit(CHANGE_TITLE, data) 
    **store.dispatch('updateList', data.id)** 
  }, 
  <...> 
} 

完成了!打开页面,修改页面的标题,并刷新页面。标题应保持其修改后的状态!

我们需要确保持久化的最后一个更改是购物清单中物品的checked属性的更改。让我们看看ItemComponent,决定我们应该在哪里调用updateList动作。

让我们首先像我们在AddItemComponent中做的那样,在props属性中添加 ID:

//ItemComponent.vue 
<script> 
  export default { 
    props: ['item', **'id'**] 
  } 
</script> 

我们还必须将id属性绑定到组件的调用中,这是在ItemsComponent内完成的:

//ItemsComponent.vue 
<template> 
  <ul> 
    <item-component v-for="item in items" :item="item" **:id="id"**>
    </item-component> 
  </ul> 
</template> 

<script> 
  import ItemComponent from './ItemComponent' 

  export default { 
    components: { 
      ItemComponent 
    }, 
    props: ['items', 'id'] 
  } 
</script> 

这也意味着我们必须将id属性绑定到item-component内部的ShoppingListComponent中:

//ShoppingListComponent.vue 
<template> 
  <...> 
    <items-component :items="items" **:id="id"**></items-component> 
  <...> 
</template> 

我们还应该在ItemComponent内部导入mapActions对象,并在methods属性中导出updateList方法:

//ItemComponent.vue 
<script> 
  **import { mapActions } from 'vuex'** 

  export default { 
    props: ['item', 'id'], 
    **methods: mapActions(['updateList'])** 
  } 
</script> 

好的,一切都与一切相连;现在我们只需要找到ItemComponent内部调用updateList动作的正确位置。

这事实上并不是一件容易的任务,因为与其他组件不同,我们在这里没有事件处理程序处理更改并调用相应的函数,而是只有绑定到复选框元素的类和模型绑定。幸运的是,Vue提供了一个watch选项,允许我们将监听器附加到组件的任何数据并将处理程序绑定到它们。在我们的情况下,我们想要监视item.checked属性并调用动作。所以,只需将watch属性添加到组件选项中,如下所示:

//ItemComponent.vue 
<script> 
  import { mapActions } from 'vuex' 

  export default { 
    props: ['item', 'id'], 
    methods: mapActions(['updateList']), 
    **watch: { 
      'item.checked': function () { 
        this.updateList(this.id) 
      } 
    }** 
  } 
</script> 

然后...我们完成了!尝试向购物清单中添加物品,勾选,取消勾选,然后再次勾选。刷新页面。一切看起来都和刷新前一样!

创建一个新的购物清单

好的,我们已经从服务器获取了购物清单;我们还存储了应用的更改,所以一切都很好。但是,如果我们能够使用我们应用程序的用户界面创建购物清单,而不是修改db.json文件或使用curl post请求,那不是也很好吗?当然,那会很好。当然,我们可以用几行代码做到!

让我们首先添加调用相应 API 方法的动作,如下所示:

//actions.js 
export default { 
  <...> 
  **createShoppingList: ({ commit }, shoppinglist) => { 
    api.addNewShoppingList(shoppinglist) 
  }** 
} 

现在我们必须提供一个可视化机制来调用这个动作。为此,我们可以在选项卡列表中创建一个额外的选项卡,其中包含加号按钮,当点击时将调用该动作。我们将在App.vue组件内完成。我们已经导入了mapActions对象。让我们只需将createShoppingList方法添加到导出的methods属性中:

//App.vue 
<script> 
  import ShoppingListComponent from './components/ShoppingListComponent' 
  import ShoppingListTitleComponent from 
  './components/ShoppingListTitleComponent' 
  import store from './vuex/store' 
  import { mapGetters, mapActions } from 'vuex' 

  export default { 
    components: { 
      ShoppingListComponent, 
      ShoppingListTitleComponent 
    }, 
    computed: mapGetters({ 
      shoppinglists: 'getLists' 
    }), 
    methods: mapActions(['populateShoppingLists', 
    **'createShoppingList'**]), 
    store, 
    mounted () { 
      this.populateShoppingLists() 
    } 
  } 
</script> 

此刻,我们的 App.vue 组件可以访问 createShoppingList 动作,并且可以在事件处理程序上调用它。问题是——使用什么数据?createShoppingList 方法正在等待接收一个对象,然后将其发送到服务器。让我们创建一个方法,它将生成一个带有硬编码标题的新列表,并在这个方法内部,使用这个新对象调用动作。但是这个方法应该放在哪里呢?组件的 methods 属性已经被 mapActions 辅助程序的调用占用了。嗯,mapActions 方法返回一个方法映射。我们可以简单地扩展这个映射,加入我们的本地方法:

//App.vue 
methods: _.**extend**({}, 
    mapActions(['populateShoppingLists', 'createShoppingList']), 
    { 
      **addShoppingList ()** { 
        let list = { 
          title: 'New Shopping List', 
          items: [] 
        } 

        **this.createShoppingList(list)** 
      } 
    }), 

现在我们只需要添加一个按钮,并将 addShoppingList 方法绑定到它的 click 事件上。你可以在页面的任何地方创建自己的按钮。我的按钮代码如下:

App.vue 
<template> 
  <div id="app" class="container"> 
    <ul class="nav nav-tabs" role="tablist"> 
      <li :class="index===0 ? 'active' : ''" v-for="(list, index) in 
        shoppinglists" role="presentation"> 
        <shopping-list-title-component :id="list.id" 
          :title="list.title"></shopping-list-title-component> 
      </li> 
      **<li> 
        <a href="#" @click="addShoppingList"> 
          <i class="glyphicon glyphicon-plus-sign"></i> 
        </a> 
      </li>** 
    </ul> 
    <div class="tab-content"> 
      <div :class="index===0 ? 'active' : ''" v-for="(list, index) in 
      shoppinglists" class="tab-pane" role="tabpanel" :id="list.id"> 
        <shopping-list-component :id="list.id" :title="list.title" 
        :items="list.items"></shopping-list-component> 
      </div> 
    </div> 
  </div> 
</template> 

看看页面;现在我们在最后一个标签上有一个漂亮的加号按钮,清楚地表明可以添加新的购物清单,如下截图所示:

创建新的购物清单

现在我们可以使用这个漂亮的加号按钮添加新的购物清单

尝试点击按钮。哎呀,什么也没发生!但是,如果我们查看网络面板,我们可以看到请求实际上已经执行成功了:

创建新的购物清单

创建请求已成功执行;但是,页面上没有任何变化

实际上,这是完全有道理的。我们更新了服务器上的信息,但客户端并不知道这些变化。如果我们能在成功创建购物清单后填充购物清单,那就太好了,不是吗?我说“如果我们能”吗?当然我们可以!只需回到 actions.js 并在 promise 的 then 回调中使用 store.dispatch 方法调用 populateShoppingLists 动作:

//actions.js 
createShoppingList: (**store**, shoppinglist) => { 
  api.addNewShoppingList(shoppinglist).**then**(() => { 
    **store.dispatch('populateShoppingLists')** 
  }) 
}  

现在,如果你点击加号按钮,你会立即看到新创建的清单出现在标签窗格中,如下截图所示:

创建新的购物清单

重新填充我们的清单后新增的购物清单

现在你可以点击新的购物清单,更改它的名称,添加它的项目,并对其进行检查和取消检查。当你刷新页面时,一切都和刷新前一样。太棒了!

删除现有的购物清单

我们已经能够创建和更新我们的购物清单。现在我们只需要能够删除它们。在本章学到的所有知识之后,这将是最容易的部分。我们应该添加一个动作,调用我们的 API 的deleteShoppingList方法,为每个购物清单添加删除按钮,并在按钮点击时调用该动作。

让我们从添加动作开始。与我们创建购物清单时一样,我们将在删除购物清单后立即调用populate方法,因此我们的动作将如下所示:

//action.js 
deleteShoppingList: (store, id) => { 
  **api.deleteShoppingList(id)**.then(() => { 
    store.dispatch('populateShoppingLists') 
  }) 
} 

现在让我们想一想应该在哪里添加删除按钮。我希望在选项卡标题中的购物清单标题附近看到它。这是一个名为ShoppingListTitleComponent的组件。打开它并导入mapActions助手。在methods属性中导出它。因此,这个组件的script标签内的代码如下所示:

//ShoppingListTitleComponent.vue 
<script> 
  **import { mapActions } from 'vuex'** 

  export default{ 
    props: ['id', 'title'], 
    computed: { 
      href () { 
        return '#' + this.id 
      } 
    }, 
    **methods: mapActions(['deleteShoppingList'])** 
  } 
</script> 

现在让我们添加删除按钮,并将deleteShoppingList方法绑定到其click事件侦听器上。我们应该将 ID 传递给这个方法。我们可以直接在模板内部做到这一点:

//ShoppingListTitleComponent.vue 
<template> 
  <a :href="href" :aria-controls="id" role="tab" data-toggle="tab">
    {{ title }} 
    **<i class="glyphicon glyphicon-remove" 
      @click="deleteShoppingList(id)"></i>** 
  </a> 
</template> 

我还为删除图标添加了一点样式,使其看起来更小更优雅:

<style scoped> 
  i { 
    font-size: x-small; 
    padding-left: 3px; 
    cursor: pointer; 
  } 
</style> 

就是这样!打开页面,你会看到每个购物清单标题旁边有一个小的x按钮。尝试点击它,你会立即看到变化,如下面的截图所示:

删除现有购物清单

带有删除 X 按钮的购物清单,允许我们删除未使用的购物清单

恭喜!现在我们有一个完全功能的应用程序,可以让我们为任何场合创建购物清单,删除它们,并管理每个清单上的物品!干得好!本节的最终代码可以在chapter6/shopping-list2文件夹中找到。

练习

我们的购物清单彼此非常相似。我想提出一个小的样式练习,你应该在其中为你的清单附加颜色,以使它们彼此不同。这将要求你在购物清单创建时添加一个背景颜色字段,并在组件内部使用它以用给定的颜色绘制你的清单。

在番茄钟应用程序中创建和使用插件

既然我们知道如何在 Vue 应用程序中使用现有的插件,为什么不创建我们自己的插件呢?我们的番茄钟应用程序中已经有一点动画效果,当状态从工作的番茄钟间隔变为休息间隔时,屏幕会完全改变。然而,如果我们不看标签,我们就不知道是应该工作还是休息。向我们的番茄钟添加一些声音会很好!

在思考时间管理应用中的声音时,我想到了适合工作的声音。我们每个人都有自己喜欢的工作播放列表。当然,这取决于每个人的音乐偏好。这就是为什么我决定在工作时间段内向我们的应用程序添加一些中性声音。一些研究证明了不同的噪音(白噪声、粉红噪声、棕噪声等)对于需要高度集中注意力的工作是有益的。关于这些研究的维基百科条目可以在en.wikipedia.org/wiki/Sound_masking找到。一些 Quora 专家讨论这个问题可以在bit.ly/2cmRVW2找到。

在这一部分,我们将使用 Web Audio API(developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API)为 Vue 创建一个插件,用于生成白噪声、粉红噪声和棕噪声。我们将提供一种机制,使用 Vue 指令来实例化一个噪声或另一个噪声,并且我们还将提供全局 Vue 方法来启动和暂停这些声音。之后,我们将使用这个插件在休息时观看猫时切换到静音状态,而在工作时切换到嘈杂状态。听起来有挑战性和有趣吗?我真的希望是!那么让我们开始吧!

创建 NoiseGenerator 插件

我们的插件将存储在一个单独的 JavaScript 文件中。它将包含三种方法,一种用于生成每种噪声,并提供一个Vue.install方法,其中将定义指令和所需的 Vue 方法。使用chapter6/pomodoro文件夹作为起点。首先,在src文件夹中创建一个plugins子文件夹,并在其中添加VueNoiseGeneratorPlugin.js文件。现在让我们创建以下三种方法:

  • 生成白噪声

  • 生成粉红噪声

  • 生成棕噪声

我不会重复造轮子,只会复制并粘贴我在互联网上找到的已有代码。当然,我要非常感谢我在noisehack.com/generate-noise-web-audio-api/找到的这个很棒的资源。话虽如此,我们在复制代码并将其组织成函数后,插件应该如下所示:

// plugins/VueNoiseGenerator.js 
import _ from 'underscore' 

// Thanks to this great tutorial: 
//http://noisehack.com/generate-noise-web-audio-api/ 
var audioContext, bufferSize, noise 
audioContext = new (window.AudioContext || window.webkitAudioContext)() 

function **generateWhiteNoise** () { 
  var noiseBuffer, output 

  bufferSize = 2 * audioContext.sampleRate 
  noiseBuffer = audioContext.createBuffer(1, bufferSize, 
    audioContext.sampleRate) 

  output = noiseBuffer.getChannelData(0) 
  _.times(bufferSize, i => { 
    output[i] = Math.random() * 2 - 1 
  }) 

  noise = audioContext.createBufferSource() 
  noise.buffer = noiseBuffer 
  noise.loop = true 
  noise.start(0) 

  return noise 
} 

function **generatePinkNoise** () { 
  bufferSize = 4096 
  noise = (function () { 
    var b0, b1, b2, b3, b4, b5, b6, node 
    b0 = b1 = b2 = b3 = b4 = b5 = b6 = 0.0 
    node = audioContext.createScriptProcessor(bufferSize, 1, 1) 
    node.onaudioprocess = function (e) { 
      var output 

      output = e.outputBuffer.getChannelData(0) 
      _.times(bufferSize, i => { 
        var white = Math.random() * 2 - 1 
        b0 = 0.99886 * b0 + white * 0.0555179 
        b1 = 0.99332 * b1 + white * 0.0750759 
        b2 = 0.96900 * b2 + white * 0.1538520 
        b3 = 0.86650 * b3 + white * 0.3104856 
        b4 = 0.55000 * b4 + white * 0.5329522 
        b5 = -0.7616 * b5 - white * 0.0168980 
        output[i] = b0 + b1 + b2 + b3 + b4 + b5 + b6 + white * 0.5362 
        output[i] *= 0.11 // (roughly) compensate for gain 
        b6 = white * 0.115926 
      }) 
    } 
    return node 
  })() 

  return noise 
} 

function **generateBrownNoise** () { 
  bufferSize = 4096 

  noise = (function () { 
    var lastOut, node 

    lastOut = 0.0 
    node = audioContext.createScriptProcessor(bufferSize, 1, 1) 
    node.onaudioprocess = function (e) { 
      var output = e.outputBuffer.getChannelData(0) 
      _.times(bufferSize, i => { 
        var white = Math.random() * 2 - 1 
        output[i] = (lastOut + (0.02 * white)) / 1.02 
        lastOut = output[i] 
        output[i] *= 3.5 // (roughly) compensate for gain 
      }) 
    } 
    return node 
  })() 

  return noise 
} 

您可以在jsfiddle.net/chudaol/7tuewm5z/的 JSFiddle 中测试所有这些噪音。

好的,我们已经实现了所有三种噪音。现在我们必须导出install方法,该方法将被Vue调用。此方法接收Vue实例,并可以在其上创建指令和方法。让我们创建一个指令,称之为noise。这个指令可以有三个值,whitepinkbrown,根据接收到的值,将通过调用相应的噪音创建方法来实例化noise变量。因此,在install方法中创建指令将如下所示:

// plugins/VueNoiseGeneratorPlugin.js 
export default { 
  install: function (Vue) { 
    **Vue.directive('noise'**, (value) => { 
      var noise 

      switch (value) { 
        case **'white'**: 
          noise = **generateWhiteNoise**() 
          break 
        case **'pink'**: 
          noise = **generatePinkNoise**() 
          break 
        case **'brown'**: 
          noise = **generateBrownNoise**() 
          break 
        default: 
          noise = generateWhiteNoise() 
      } 
      noise.connect(audioContext.destination) 
      audioContext.suspend() 
    }) 
  } 
} 

实例化后,我们将noise连接到已经实例化的audioContext,并将其暂停,因为我们不希望它在指令绑定时立即开始产生噪音。我们希望它在某些事件(例如,单击开始按钮)上被实例化,并在其他事件(例如,有人单击暂停按钮时)上被暂停。为此,让我们为启动、暂停和停止我们的audioContext提供方法。我们将这三种方法放在名为noise的全局 Vue 属性上。我们将这些方法称为startpausestop。在start方法中,我们希望在pausestop方法中恢复audioContext并将其暂停。因此,我们的方法将如下所示:

// plugins/VueNoiseGeneratorPlugin.js 
export default { 
  install: function (Vue) { 
    Vue.directive('noise', (value) => { 
      <...> 
    }) 
    **Vue.noise** = { 
      **start** () { 
        audioContext.resume() 
      }, 
      **pause** () { 
        audioContext.suspend() 
      }, 
      **stop** () { 
        audioContext.suspend() 
      } 
    } 
  } 
} 

就是这样!我们的插件已经完全准备好使用了。当然,它并不完美,因为我们只有一个audioContext,它只被实例化一次,然后由所选的噪音之一填充,这意味着我们将无法在页面上多次使用noise指令,但再次强调,这只是一个原型,您完全可以增强它并使其完美并公开!

在番茄钟应用程序中使用插件

好了,现在我们有了一个很好的噪音产生插件,唯一缺少的就是使用它!您已经知道如何做了。打开main.js文件,导入VueNoiseGeneratorPlugin,并告诉Vue使用它:

import VueNoiseGeneratorPlugin from 
'./plugins/VueNoiseGeneratorPlugin' 

Vue.use(VueNoiseGeneratorPlugin) 

从现在开始,我们可以在 Pomodoro 应用程序的任何部分附加noise指令并使用Vue.noise方法。让我们将其绑定到App.vue组件中的主模板中:

//App.vue 
<template> 
  <div id="app" class="container" **v-noise="'brown'"**> 
    <...> 
  </div> 
</template> 

请注意,我们在指令的名称中使用了v-noise而不仅仅是noise。当我们学习自定义指令时已经讨论过这一点。要使用指令,我们应该始终在其名称前加上v-前缀。还要注意,我们在单引号内使用双引号来包裹brown字符串。如果我们不这样做,Vue 将搜索名为brown的数据属性,因为这就是 Vue 的工作原理。由于我们可以在指令绑定赋值中编写任何 JavaScript 语句,因此我们必须使用双引号传递字符串。您还可以进一步创建一个名为noise的数据属性,并将您想要的值(whitebrownpink)分配给它,并在指令绑定语法中重用它。

完成后,让我们在我们的start mutation 中调用Vue.noise.start方法:

//mutations.js 
**import Vue from 'vue'** 
<...> 

export default { 
  [types.START] (state) { 
    <...> 
    **if (state.isWorking) { 
      Vue.noise.start() 
    }** 
  }, 
<...> 

检查页面并点击开始按钮。您将听到一种悦耳的棕色噪音。但是要小心,不要吵醒您的同事,也不要吓到您的家人(反之亦然)。尝试更改噪音指令的值,并选择您喜欢的噪音进行工作。

但我们还没有完成。我们创建了一个启动噪音的机制,但它正在变成一个永无止境的噪音。让我们分别在pausestop mutations 中调用Vue.noise.pauseVue.noise.stop方法:

//mutations.js 
export default { 
  <...> 
  [types.PAUSE] (state) { 
    <...> 
    **Vue.noise.pause()** 
  }, 
  [types.STOP] (state) { 
    <...> 
    **Vue.noise.stop()** 
  } 
} 

看看页面。现在,如果您点击暂停或停止按钮,噪音就会被暂停!我们还没有完成。请记住,我们的目的是只在工作时间而不是休息时间播放噪音。因此,让我们看看mutations.js中的tooglePomodoro方法,并添加一个根据番茄钟当前状态启动或停止噪音的机制:

//mutations.js 
function togglePomodoro (state, toggle) { 
  if (_.isBoolean(toggle) === false) { 
    toggle = !state.isWorking 
  } 
  state.isWorking = toggle 
  **if (state.isWorking) { 
    Vue.noise.start() 
  } else { 
    Vue.noise.pause() 
  }** 
  state.counter = state.isWorking ? WORKING_TIME : RESTING_TIME 
} 

所有这些修改后的番茄钟应用程序的代码可以在chapter6/pomodoro2文件夹中找到。查看当我们启动应用程序时噪音是如何开始的,当工作番茄钟完成时它是如何暂停的,以及当我们应该回到工作时它是如何重新开始的。还要查看启动、暂停和停止按钮如何触发噪音。干得好!

创建切换声音的按钮

我们很高兴将噪音声音绑定到番茄钟应用程序的工作状态。当我们暂停应用程序时,声音也会暂停。然而,有时候可能需要能够暂停声音而不必暂停整个应用程序。想想那些你想要完全安静工作的情况,或者你可能想接听 Skype 电话的情况。在这些情况下,即使是美妙的粉色噪音也不好。让我们在应用程序中添加一个按钮来切换声音。首先声明一个名为soundEnabled的 store 属性,并将其初始化为true。还要为此属性创建一个getter。因此,store.jsgetters.js开始看起来像下面这样:

//store.js 
<...> 
const state = { 
  <...> 
  **soundEnabled: true** 
} 

//getters.js 
export default { 
  <...> 
  **isSoundEnabled: state => state.soundEnabled** 
} 

现在我们必须提供一个切换声音的机制。让我们创建一个用于此目的的 mutation 方法,并添加一个触发此 mutation 的 action。首先声明一个名为TOGGLE_SOUND的 mutation 类型:

//mutation_types.js 
<...> 
**export const TOGGLE_SOUND = 'TOGGLE_SOUND'** 

现在让我们打开mutations.js并添加一个切换soundEnabled存储属性的 mutation 方法:

//mutations.js 
[types.TOGGLE_SOUND] (state) { 
  state.soundEnabled = !state.soundEnabled 
  if (state.soundEnabled) { 
    Vue.noise.start() 
  } else { 
    Vue.noise.pause() 
  } 
} 

现在让我们添加触发这个 mutation 的动作:

//actions.js 
export default { 
  <...> 
  toggleSound: ({ commit }) => { 
    **commit(types.TOGGLE_SOUND)** 
  } 
} 

好的,现在我们有了创建切换声音按钮所需的一切!让我们在ControlsComponent中完成。首先在方法映射中添加必要的 getter 和 action:

//ControlsComponent.vue 
<script> 
  import { mapGetters, mapActions } from 'vuex' 

  export default { 
    computed: mapGetters(['isStarted', 'isPaused', 'isStopped', 
    **'isSoundEnabled'**]), 
    methods: mapActions(['start', 'stop', 'pause', **'toggleSound'**]) 
  } 
</script> 

现在我们可以在模板中添加按钮。我建议它是一个带有glyphicon类的图标,将其对齐到右侧。

让我们只在应用程序启动未暂停时显示此图标,并且只在番茄钟状态工作时显示,这样我们就不会在根本不应该有声音的状态下搞乱切换声音按钮。这意味着我们在这个元素上的v-show指令将如下所示:

v-show="isStarted && !isPaused && isWorking" 

请注意,我们在这里使用了尚未导入的isWorking属性。将其添加到方法映射中:

//ControlsComponents.vue 
<script> 
  import { mapGetters, mapActions } from 'vuex' 

  export default { 
    computed: mapGetters(['isStarted', 'isPaused', 'isStopped', 
    **'isWorking'**, 'isSoundEnabled']), 
    methods: mapActions(['start', 'stop', 'pause', 'toggleSound']) 
  } 
</script> 

让我们还在这个元素上使用glyphicon-volume-offglyphicon-volume-on类。它们将指示调用切换声音状态的操作。这意味着当声音启用时应用glyphicon-volume-off类,当声音禁用时应用glyphicon-volume-on类。将其放入代码中,我们的类指令应该如下所示:

:class="{ 'glyphicon-volume-off': **isSoundEnabled**, 'glyphicon-volume-up': **!isSoundEnabled** }" 

最后但同样重要的是,当点击按钮时,我们应该调用toggleSound动作。这意味着我们还应该将click事件监听器绑定到这个元素,代码如下所示:

@click='**toggleSound**' 

因此,这个按钮的整个 jade 标记代码将如下所示:

//ControlsComponent.vue 
<template> 
  <span> 
    <...> 
    **<i class="toggle-volume glyphicon" v-show="isStarted &&** 
 **!isPaused && isWorking" :class="{ 'glyphicon-volume-off':** 
 **isSoundEnabled, 'glyphicon-volume-up': !isSoundEnabled }"** 
 **@click="toggleSound"></i>** 
  </span> 
</template> 

让我们给这个按钮添加一点样式,使它看起来与右侧对齐:

<style scoped> 
  <...> 
  **.toggle-volume { 
    float: right; 
    cursor: pointer; 
  }** 
</style> 

打开页面并启动 Pomodoro 应用程序。现在你可以在右上角看到一个漂亮的按钮,它将允许你关闭声音,如下截图所示:

创建一个切换声音的按钮

现在我们可以在工作时关闭声音!

如果你点击这个按钮,它将变成另一个按钮,其目的是再次打开声音,如下截图所示:

创建一个切换声音的按钮

现在我们可以再次打开它!

现在考虑以下情景:我们启动应用程序,关闭声音,暂停应用程序,然后恢复应用程序。我们当前的逻辑表明每次启动应用程序时都会启动声音。我们将处于一个不一致的状态——应用程序已启动,声音正在播放,但切换声音按钮建议打开声音。这不对,对吧?但这有一个简单的解决办法——只需在启动变异中添加一个条件,不仅应该检查isWorking是否为true,还应该检查声音是否启用:

//mutations.js 
types.START { 
  <...> 
  if (state.isWorking && **state.soundEnabled**) { 
    Vue.noise.start() 
  } 
}, 

现在我们很好。所有这些修改后的代码可以在chapter6/pomodoro3文件夹中找到。

检查代码,运行应用程序,享受声音,并不要忘记休息!

练习

在我们的 Pomodoro 间隔期间,如果我们还能享受一些愉快的音乐,看着猫会很好。创建一个播放选择的 mp3 文件的插件,并在 Pomodoro 间隔期间使用它。

总结

当我为本章编写最后几行代码并检查页面时,有一次我被这张图片吸引住了:

Summary

很多猫盯着我问:这一章什么时候结束?

我甚至暂停了应用程序,好好看了一下这张图片(是的,当你在休息时间暂停番茄钟应用程序时,图片也会暂停,因为缓存破坏时间戳不再更新)。这些猫似乎在问我们休息一下?而且它们的数量与我们在本章学到的东西的数量非常接近!

在本章中,您学习了 Vue.js 插件系统的工作原理。我们使用现有的resource插件将服务器端行为附加到我们的购物清单应用程序上。现在我们可以创建、删除和更新我们的购物清单。

我们还创建了自己的插件!我们的插件能够发出声音,有助于在工作期间集中注意力。我们不仅创建了它,还在我们的番茄钟应用程序中使用了它!现在在番茄钟工作时我们可以更好地集中注意力,并随时切换声音!

现在我们手头有两个非常好的应用程序。你知道什么比一个好的应用程序更好吗?

唯一比一个好的应用程序更好的是一个经过良好测试的应用程序!

考虑到这一点,是时候测试我们的应用程序了。在下一章中,我们将检查并应用一些测试技术。我们将使用 Karma 测试运行器和 Jasmine 作为断言库编写单元测试。我们还将使用 Nightwatch 编写端到端测试。我喜欢测试应用程序,希望你也会喜欢。让我们开始吧!

第七章:测试-是时候测试我们到目前为止所做的了!

在上一章中,您学会了如何使用和创建 Vue 插件。我们使用现有的resource插件为 Vue 创建了自己的NoiseGenerator插件。

在本章中,我们将确保番茄钟和购物清单应用程序的质量。我们将使用不同的测试技术来测试这些应用程序。首先,我们将对 Vue 组件和与 Vuex 相关的代码(如 actions、mutations 和 getters)执行经典的单元测试。之后,我们将学习如何使用 Nightwatch 执行端到端测试。因此,在本章中,我们将做以下事情:

  • 谈论单元测试和端到端测试的重要性

  • 为番茄钟和购物清单应用程序实现单元测试

  • 学习如何在单元测试中模拟服务器响应

  • 使用 Nightwatch 为两个应用程序实现端到端测试

为什么单元测试?

在我们开始编写单元测试之前,让我们试着理解我们试图通过编写它们来实现什么。为什么单元测试如此重要?有时当我写我的测试时,我唯一能想到的就是我的代码覆盖率;我想要达到 100%的水平。

代码覆盖率是一个非常重要的指标,对于理解代码流程和需要测试的内容有很大帮助。但这并不是单元测试质量的指标。这不是代码质量好坏的指标。你可以让你的代码 100%覆盖,只是因为你在测试代码中调用了所有的函数,但如果你的断言是错误的,那么代码也可能是错误的。编写良好的单元测试是一门需要时间和耐心的艺术。但是当你的单元测试足够好,当你专注于做出良好的断言时,关于边界情况和分支覆盖,它们提供以下内容:

  • 帮助我们识别算法和逻辑中的失败

  • 帮助我们提高代码质量

  • 让我们编写易于测试的代码

  • 防止未来的更改破坏功能

  • 帮助我们拥有更可预测的截止日期和估算

易于进行单元测试覆盖的代码同时也是易于阅读的代码。易于阅读的代码更不容易出错,更易于维护。可维护性是应用程序质量的主要支柱之一。

注意

chudaol.github.io/presentation-unit-testing的演示中了解更多关于单元测试的内容。

让我们为我们的应用程序编写一些单元测试。

我们将使用 Karma 测试运行器,Mocha 测试框架,Chai 期望库和 Sinon 进行模拟。

有关这些工具的更多信息,请参考以下内容:

如果我们没有使用vue-cli webpack进行应用程序的引导,我们将不得不通过npm安装所有这些工具。但在我们的情况下,我们不需要进行这种安装。检查你的package.json文件,你会发现所有这些东西已经在那里:

  "devDependencies": { 
    <...> 
    "**chai**": "³.5.0", 
    <...> 
    "**karma**": "⁰.13.15", 
    "karma-chrome-launcher": "².0.0", 
    "karma-coverage": "⁰.5.5", 
    "karma-mocha": "⁰.2.2", 
    "karma-phantomjs-launcher": "¹.0.0", 
    "**karma-sinon-chai**": "¹.2.0", 
    "**mocha**": "².4.5", 
    <...> 
  } 

你肯定知道为简单函数编写单元测试有多简单。这几乎就像说人类语言一样。它(这个函数)如果输入是Y,应该返回X。我期望它是X

因此,如果我们有一个模块导出了一个返回两个参数之和的函数,那么这个函数的单元测试必须使用不同的参数调用该函数并期望一些输出。因此,让我们假设我们有一个如下的函数:

function sum(a, b) { 
  return a + b 
} 

然后我们的单元测试可能如下所示:

it('should follow commutative law', () => { 
  let a = 2; 
  let b = 3; 

  expect(sum(a, b)).to.equal(5); 
  expect(sum(b, a)).to.equal(5); 
}) 

当我们考虑对正在进行单元测试的函数的可能输入时,我们绝不应该害羞。空输入,负输入,字符串输入,一切都重要!你看过这条著名的推文吗(twitter.com/sempf/status/514473420277694465)?

为什么要进行单元测试?

关于 QA 工程师思维方式的病毒推文

考虑所有可能的输入和适当的输出。用期望和断言来表达这一点。运行测试。看看哪里出了问题。修复你的代码。

Vue 应用的单元测试

首先,让我们检查一些关于单元测试我们的 Vue 应用程序及其组件的特殊情况。为了能够为组件实例编写测试,首先必须实例化它!非常合乎逻辑,对吧?问题是,我们如何实例化 Vue 组件,以便其方法变得可访问和易于测试?要测试组件初始状态的基本断言,你只需导入它们并断言它们的属性。如果你想测试动态属性——一旦组件绑定到 DOM 后会发生变化的属性——你只需做以下三件事:

  1. 导入一个组件。

  2. 通过将其传递给Vue函数来实例化它。

  3. 挂载它。

提示

当实例绑定到物理 DOM 时,一旦实例化,编译立即开始。在我们的情况下,我们没有将实例绑定到任何真正的物理 DOM 元素,因此我们必须通过手动调用mount方法($mount)来显式地使其编译。

现在你可以使用创建的实例并访问它的方法。在伪代码中,它看起来像下面这样

**import** MyComponent from <path to my component> 
var vm = **new Vue**(MyComponent).**$mount()** 

现在我们可以访问所有vm实例方法并测试它们。其余的东西,比如dataprops等等,我们可以伪造。伪造东西没有问题,因为它为我们提供了轻松尝试各种输入并测试每种输入的所有可行输出的可能性。

如果你想在测试使用props的组件时拥有更真实的场景,这些props是由其父组件绑定到组件的,或者访问vuex存储等等,你可以使用ref属性将组件绑定到Vue实例。这个Vue实例,反过来,实例化存储和数据,并以通常的方式将数据项绑定到组件。之后,你可以通过使用$refs Vue 属性访问组件实例。这种绑定看起来像下面这样:

import store from <path to store> 
import **MyComponent** from <path to my component> 
// load the component with a vue instance 
var vm = new Vue({ 
  template: '<div><test :items="items" :id="id" ref=testcomponent></test></div>', 
  components: { 
    'test': **MyComponent** 
  }, 
  data() { 
    return { 
      items: [], 
      id: 'myId' 
    } 
  }, 
  store 
}).$mount(); 

var myComponent = **vm.$refs.testcomponent**; 

现在你可以测试myComponent的所有方法,而不用担心覆盖它的propsmethods和其他实例相关的东西。这是这种方法的一个好处;然而,正如你所看到的,这并不是最容易的设置,你应该考虑一切。例如,如果你的组件调用了一些存储的动作,这些动作调用了一些 API 的方法,你应该准备好伪造服务器的响应。

我个人喜欢尽可能简单地保持事情,伪造所有的数据输入,并集中在测试函数的可能输出和所有可能的边缘情况。但这只是我的个人观点,而且我们应该尝试生活中的一切,所以在这一章中,我们将尝试不同的方法。

编写购物清单应用的单元测试

在实际编写单元测试之前,让我们建立一些规则。对于我们的每个.js.vue文件,都会存在一个相应的测试规范文件,它将具有相同的名称和一个.spec.js扩展名。这些规范的结构将遵循这种方法:

  • 它将描述我们正在测试的文件

  • 它将为正在测试的每个方法有一个describe方法

  • 它将为我们描述的每种情况都有一个it方法

因此,如果我们有一个myBeautifulThing.js文件和它的规范,它可能看起来像下面这样:

**// myBeautifulThing.js** 
export myBeautifulMethod1() { 
  return 'hello beauty' 
} 

export myBeautifulMethod2() { 
  return 'hello again' 
} 

**// myBeautifulThing.spec.js** 
import myBeautifulThing from <path to myBeautifulThing> 

describe('myBeautifulThing', () => { 
  //define needed variables 

  describe('myBeautifulMethod1', () => { 
    it('should return hello beauty', () { 
      expect(myBeautifulThing.myBeautifulMethod1()).to.equal('hello  
        beauty') 
    }) 
  }) 
}) 

让我们从覆盖vuex文件夹中的所有内容开始进行单元测试。

测试操作、getter 和 mutations

在本节中,请使用chapter7/shopping-list文件夹中的代码。不要忘记运行npm install命令。请注意,有两个新的 mutations:ADD_SHOPPING_LISTDELETE_SHOPPING_LIST。这些 mutations 会将新的购物清单添加到列表中,并通过其 ID 删除列表。它们在createShoppingListdeleteShoppingList操作中被用于 promise 失败处理程序内:

//actions.js  
createShoppingList: (store, shoppinglist) => { 
  api.addNewShoppingList(shoppinglist).then(() => { 
    store.dispatch('populateShoppingLists') 
  }, () => { 
    **store.commit(ADD_SHOPPING_LIST, shoppinglist)** 
  }) 
}, 
deleteShoppingList: (store, id) => { 
  api.deleteShoppingList(id).then(() => { 
    store.dispatch('populateShoppingLists') 
  }, () => { 
    **store.commit(DELETE_SHOPPING_LIST, id)** 
  }) 
} 

因此,即使我们的后端服务器宕机,我们仍然不会失去这个功能。

如果你再次检查你的项目结构,你会看到已经存在一个名为test的现有目录。在这个目录中,有两个目录,unite2e。现在,我们应该进入unit文件夹。在这里,你会看到另一个名为specs的目录。这是我们所有单元测试规范的所在地。让我们首先在specs内创建一个名为vuex的目录。这是我们所有与 Vuex 相关的 JavaScript 文件的规范所在地。

让我们从测试mutations.js方法开始。

创建一个mutations.spec.js文件。在这个文件中,我们应该导入mutations.js和 mutation 类型,以便我们可以轻松地调用 mutations。看一下mutations.js中声明的 mutations。它们都接收state和一些其他参数。让我们还创建一个带有shoppinglist数组的假state对象,这样我们就可以在我们的测试中使用它。

在每次测试之前,让我们也将其重置为空数组。

因此,在所有准备工作完成后,mutations.js的引导规范如下:

// mutations.spec.js 
import mutations from 'src/vuex/mutations' 
import { ADD_SHOPPING_LIST, DELETE_SHOPPING_LIST, POPULATE_SHOPPING_LISTS, CHANGE_TITLE } from 'src/vuex/mutation_types' 

describe('mutations.js', () => { 
  var state 

  beforeEach(() => { 
    state = { 
      shoppinglists: [] 
    } 
  }) 
}) 

现在让我们为ADD_SHOPPING_LISTmutation 添加测试。

再次检查它在做什么:

[types.ADD_SHOPPING_LIST] (state, newList) { 
  state.shoppinglists.push(newList) 
}, 

这个 mutation 只是将接收到的对象推送到shoppinglists数组中。非常直接和容易测试。

首先创建一个带有函数名称的describe语句:

describe(**'ADD_SHOPPING_LIST'**, () => { 
}) 

现在,在这个describe回调中,我们可以添加带有所需断言的it语句。让我们想一想当我们将新的购物清单添加到shoppinglists数组时会发生什么。首先,数组的长度会增加,它还将包含新添加的购物清单对象。这是最基本的测试。我们的it函数与所需的断言将如下所示:

  it('should add item to the shopping list array and increase its 
    length', () => { 
  //call the add_shopping_list mutations 
  **mutationsADD_SHOPPING_LIST** 
  //check that the array now equals array with new object 
  **expect(state.shoppinglists).to.eql([{id: '1'}])** 
  //check that array's length had increased 
  **expect(state.shoppinglists).to.have.length(1)** 
}) 

创建完这个函数后,整个规范的代码应该如下所示:

// mutations.spec.js 
import mutations from 'src/vuex/mutations' 
import { ADD_SHOPPING_LIST, DELETE_SHOPPING_LIST, POPULATE_SHOPPING_LISTS, CHANGE_TITLE } from 'src/vuex/mutation_types' 

describe('mutations.js', () => { 
  var state 

  beforeEach(() => { 
    state = { 
      shoppinglists: [] 
    } 
  }) 

  describe('ADD_SHOPPING_LIST', () => { 
    it('should add item to the shopping list array and increase its 
      length', () => { 
      mutationsADD_SHOPPING_LIST 
      expect(state.shoppinglists).to.eql([{id: '1'}]) 
      expect(state.shoppinglists).to.have.length(1) 
    }) 
  }) 
}) 

让我们运行测试!在项目目录中打开控制台,运行以下命令:

**npm run unit** 

你应该看到以下输出:

测试操作、获取器和变异

运行我们的测试的输出

还记得关于 QA 工程师的笑话吗?我们可以测试add_shopping_list函数的所有可能输入。例如,如果我们在不传递任何对象的情况下调用它,会发生什么?理论上,它不应该添加到购物清单数组中,对吧?让我们测试一下。创建一个新的it语句,尝试在不传递第二个参数的情况下调用该函数。断言为空列表。

这个测试将看起来像下面这样:

it('should not add the item if item is empty', () => { 
  mutationsADD_SHOPPING_LIST 
  **expect(state.shoppinglists).to.have.length(0)** 
}) 

使用npm run unit命令运行测试。哦,糟糕!它失败了!错误如下:

expected [ undefined ] to have a length of 0 but got 1 

为什么?看看相应的变异。它只是将接收到的参数推送到数组中,而没有任何检查。这就是为什么我们能够添加任何垃圾、任何未定义和任何其他不合适的值!你还记得我说过编写良好的单元测试可以帮助我们创建更少容易出错的代码吗?现在我们意识到在将新项目推送到数组之前,我们应该可能运行一些检查。让我们添加检查,确保接收到的项目是一个对象。打开mutations.js文件中的ADD_SHOPPING_LIST变异,并将其重写如下:

//mutations.js 
types.ADD_SHOPPING_LIST { 
  if (**_.isObject(newList)**) { 
    state.shoppinglists.push(newList) 
  } 
} 

现在运行测试。它们都通过了!

当然,我们可以更加精确。我们可以检查和测试空对象,还可以对该对象进行一些验证,以确保包含iditemstitle等属性。我会把这个留给你作为一个小练习。尝试考虑所有可能的输入和所有可能的输出,编写所有可能的断言,并使代码与它们相对应。

良好的测试标准

一个好的单元测试是当你改变你的代码时会失败的测试。想象一下,例如,我们决定在将新的购物清单推送到数组之前为其分配一个默认标题。因此,变异看起来像下面这样:

types.ADD_SHOPPING_LIST { 
  if (_.isObject(newList)) { 
    **newList.title = 'New Shopping List'**     
    state.shoppinglists.push(newList) 
  } 
} 

如果你运行测试,它们会失败:

良好的测试标准

当代码发生变化时,单元测试失败

这非常好。当你的代码发生变化后测试失败,可能的结果是你修复测试,因为代码执行了预期的行为,或者你修复你的代码。

代码覆盖率

我相信你在运行测试后的控制台输出中已经注意到了一些测试统计信息。这些统计数据显示了我们在运行时测试所达到的不同类型的覆盖率。现在看起来是这样的:

代码覆盖率

在为 ADD_SHOPPING_LIST mutation 编写两个测试后的 mutations.js 的代码覆盖率

你还记得我说过良好的代码覆盖率并不意味着我们的测试和代码是完美的吗?我们实际上有相当不错的语句、分支和行覆盖率,但我们只测试了一个文件的一个函数,甚至没有覆盖这个函数的所有可能输入。但数字不会说谎。我们几乎有 100%的分支覆盖率,因为我们的代码几乎没有分支。

如果你想看到更详细的报告,只需在浏览器中打开test/unit/coverage/lcov-report目录下的index.html文件。它会给你一个完整的代码图片,显示出你的代码覆盖了什么,以及覆盖了什么。目前看起来是这样的:

代码覆盖率

我们代码库覆盖率的整体图片

你可以深入到文件夹中,打开文件,检查我们的代码是如何被覆盖的。让我们来检查mutations.js

代码覆盖率

actions.js 的覆盖率报告准确显示了哪些代码被覆盖了,哪些没有被覆盖

现在你知道还有什么需要测试。你想看看它如何报告if…else缺失的分支覆盖率吗?只需跳过我们的第二个测试:

it.**skip**('should not add the item if item is empty', () => { 
  mutationsADD_SHOPPING_LIST 
  expect(state.shoppinglists).to.have.length(0) 
}) 

运行测试并刷新actions.js的报告。你会在if语句左边看到一个E图标:

代码覆盖率

在 if 语句附近的 E 图标表示 else 分支没有被测试覆盖

这表明我们没有覆盖else分支。如果你跳过第一个测试,只留下一个空对象的测试,你会看到I图标,表示我们跳过了if分支:

代码覆盖率

在 if 语句附近的 I 图标表示 if 分支没有被测试覆盖

为其余的变异编写测试。至少执行以下检查:

  • 对于DELETE_SHOPPING_LIST变异,检查我们传递的 ID 对应的列表是否实际上被删除,如果它之前存在于列表中,并且调用具有在列表中不存在的 ID 的变异不会引起任何改变

  • 对于POPULATE_SHOPPING_LISTS变异,检查当我们调用这个变异时,shoppinglist数组是否被我们传递的数组覆盖

  • 对于CHANGE_TITLE变异,检查当我们传递新标题和 ID 时,确切地改变了这个对象的标题

最后,你的mutation.spec.js文件可能看起来像这个gist

经过这些测试,mutation.js的覆盖率看起来相当不错:

代码覆盖率

在为所有变异编写单元测试后,mutations.js的覆盖率为 100%

以完全相同的方式,我们可以测试我们的getters.js。创建一个getters.spec.js文件,并填充它以测试我们的两个 getter 函数。最后,它可能看起来像这个gist

在单元测试中缺少的唯一重要的存储组件是actions.js。但是我们的actions.js广泛使用了 API,而 API 又执行 HTTP 请求。它的函数也是异步的。这种类型的东西能像我们刚刚测试 getter 和 action 一样灵活和简单地进行单元测试吗?是的,可以!让我们看看如何使用sinon.js伪造服务器响应,以及如何使用mocha.js编写异步测试。

伪造服务器响应和编写异步测试

打开actions.js文件,检查第一个动作方法:

//actions.js 
populateShoppingLists: ({ commit }) => { 
  api.fetchShoppingLists().then(response => { 
    commit(POPULATE_SHOPPING_LISTS, response.data) 
  }) 
} 

首先,让我们给这个函数添加一个return语句,使其返回一个 promise。我们这样做是为了让我们能够在 promise 解析后调用.then方法,以便我们可以测试期间发生的一切。因此,我们的函数看起来像下面这样:

//actions.js 
populateShoppingLists: ({ commit }) => { 
  **return** api.fetchShoppingLists().then(response => { 
    commit(POPULATE_SHOPPING_LISTS, response.data) 
  }) 
} 

现在,检查这里发生了什么:

  1. 这个函数接收带有dispatch方法的store

  2. 它执行对 API 的调用。API 又调用资源get方法,该方法只是向我们的服务器执行 HTTP 请求。

  3. 在 API 的fetchShoppingLists承诺解决后,我们的方法将使用两个参数调用存储的commit方法:一个POPULATE_SHOPPING_LISTS字符串和响应中传入的数据。

我们如何对这个工作流进行单元测试?如果我们能够捕获请求并模拟响应,我们可以检查我们提供给服务器模拟的响应是否调用了commit方法(由我们传递,这意味着它也可以被模拟)。听起来混乱吗?一点也不!步骤如下:

  1. store及其commit方法创建一个模拟。

  2. 为假设的服务器响应创建一个模拟。

  3. 创建一个假服务器,它将拦截 GET 请求并返回模拟的响应。

  4. 检查commit方法是否以我们模拟的响应和POPULATE_SHOPPING_LISTS字符串被调用。

这意味着我们的测试可能看起来像下面这样:

it('should test that commit is called with correct parameters', () => { 
  actions.populateShoppingLists({ commit }).then(() => { 
    expect(commit).to.have.been.calledWith(<...>) 
  }) 
}) 

这里的问题是我们的测试是同步的,这意味着代码永远不会达到我们.then回调中的内容。幸运的是,mocha.js提供了对异步测试的支持。在mochajs.org/#asynchronous-code查看。你所需要做的就是将done回调传递给it(),并在测试完成时调用它。这样,我们对这个测试的伪代码看起来如下:

it('should test that commit is called with correct parameters', 
(**done**) => { 
  actions.populateShoppingLists({ commit }).then(() => { 
   expect(commit).to.have.been.calledWith(<...>) 
   **done()** 
  }) 
}) 

现在让我们编码!创建一个测试规范并将其命名为actions.spec.js,并编写所有所需的引导代码:

// actions.spec.js 
import actions from 'src/vuex/actions' 
import { CHANGE_TITLE, POPULATE_SHOPPING_LISTS } from 'src/vuex/mutation_types' 

describe('actions.js', () => { 
  describe('populateShoppingLists', () => { 
    //here we will add our test case 
  }) 
}) 

现在让我们按步骤进行。首先,让我们模拟服务器响应。只需创建lists变量并在beforeEach方法中初始化它:

//actions.spec.js 
describe('actions.js', () => { 
  **var lists** 

  beforeEach(() => { 
    **// mock shopping lists 
    lists = [{ 
      id: '1', 
      title: 'Groceries' 
    }, { 
      id: '2', 
      title: 'Clothes' 
    }]** 
  }) 

  describe('populateShoppingLists', () => { 
  }) 
}) 

现在,让我们模拟存储的commit方法:

// actions.spec.js 
describe('actions.js', () => { 
  var lists, **store** 

  beforeEach(() => { 
    <...> 
    //mock store commit method 
    **store = { 
      commit: (method, data) => {}, 
      state: { 
        shoppinglists: lists 
      } 
    }** 
  }) 
  <...> 
}) 

现在,我们必须对这个commit方法进行间谍活动,以便能够断言它是否以所需的参数被调用。我们将使用sinon.stub方法来实现这一点。在这个问题上查看sinon.js的文档:sinonjs.org/docs/#stubs。在给定函数上创建一个存根非常容易。只需调用sinon.stub方法,并将我们想要进行间谍活动的对象及其方法传递给它:

sinon.stub(store, 'commit')  

因此,我们的beforeEach函数将如下所示:

beforeEach(() => { 
    <...> 
    // mock store commit method 
    store = { 
      commit: (method, data) => {}, 
      state: { 
        shoppinglists: lists 
      } 
    } 

    sinon.stub(store, 'commit') 
}) 

非常重要的是,在每个方法之后,我们恢复存根,以便每个测试方法在不受其他测试影响的干净环境中运行。为此,创建一个afterEach方法并添加以下行:

afterEach(function () { 
  //restore stub 
  store.commit.restore() 
}) 

现在我们唯一需要做的就是用我们模拟的数据伪造服务器响应。让我们使用 Sinon 的fakeServer来实现这个目的。在sinonjs.org/docs/#fakeServer查看 sinon 的文档。我们只需要创建fakeServer并告诉它响应我们模拟的 GET 请求的响应:

describe('actions.js', () => { 
  var lists, store, server 

  beforeEach(() => { 
    <...> 
    //mock server 
    **server = sinon.fakeServer.create() 
    server.respondWith('GET', /shoppinglists/, xhr => { 
      xhr.respond(200, {'Content-Type': 'application/json'}, 
      JSON.stringify(lists)) 
    })** 
  }) 
  <...> 
}) 

在做好这些准备之后,每个进行请求的测试都应该调用服务器的respond方法来调用服务器的功能。

然而,我们可以通过告诉服务器自动响应每个捕获的请求来简化这个过程:

server.autoRespond = true 

因此,我们模拟服务器的代码将如下所示:

beforeEach(() => { 
    <...> 
    //mock server 
    server = sinon.fakeServer.create() 
    server.respondWith('GET', /shoppinglists/, xhr => { 
      xhr.respond(200, {'Content-Type': 'application/json'}, 
      JSON.stringify(lists) 
    }) 
    **server.autoRespond = true**   
}) 

非常重要的是,在每个测试之后,我们要恢复我们的伪造服务器,以便这个测试不会影响其他测试。因此,在afterEach方法中添加以下行:

afterEach(() => { 
  //restore stubs and server mock 
  store.commit.restore() 
  **server.restore()** 
}) 

现在我们已经模拟了一切可能模拟的东西,我们终于可以编写我们的测试用例了!所以,你记得,我们创建一个带有done回调的it()语句,调用我们的populateShoppingLists方法,并检查解析后的响应是否与我们模拟的list对象相同。进入describe方法,只需将我们刚刚描述的内容翻译成代码:

it('should call commit method with POPULATE_SHOPPING_LIST and with mocked lists', done => { 
  actions.populateShoppingLists(store).then(() => { 
    **expect(store.commit).to.have.been.calledWith(POPULATE_SHOPPING_LISTS,
    lists) 
    done()** 
  }).catch(done) 
}) 

我们整个测试规范现在看起来像这个要点gist.github.com/chudaol/addb6657095406234bc6f659970f3eb8

npm run unit运行测试。它有效了!

现在我们只需要模拟 PUT、POST 和 DELETE 方法的服务器响应。这些方法不返回任何数据;然而,为了能够测试响应,让我们返回伪造的成功消息,并在每个测试中检查返回的数据是否对应这些响应。在规范的顶部添加以下变量:

  var server, store, lists, successPut, successPost, successDelete 

  **successDelete = {'delete': true} 
  successPost = {'post': true} 
  successPut = {'put': true}** 

并且在我们的服务器中添加以下伪造响应的方法:

    server.respondWith(**'POST'**, /shoppinglists/, xhr => { 
      xhr.respond(200, {'Content-Type': 'application/json'}, 
        JSON.stringify(**successPost**)) 
    }) 
    server.respondWith(**'PUT'**, /shoppinglists/, xhr => { 
      xhr.respond(200, {'Content-Type': 'application/json'}, 
        JSON.stringify(**successPut**)) 
    }) 
    server.respondWith(**'DELETE'**, /shoppinglists/, xhr => { 
      xhr.respond(200, {'Content-Type': 'application/json'}, 
        JSON.stringify(**successDelete**)) 
    }) 

让我们看看它将如何工作,例如,对于changeTitle方法。在这个测试中,我们想要测试commit方法是否会以给定的 ID 和标题被调用。因此,我们的测试将如下所示:

describe(**'changeTitle'**, () => { 
  it('should call commit method with CHANGE_TITLE string', (done) => { 
    let title = 'new title' 

    actions.changeTitle(store, {title: title, id: '1'}).then(() => { 
      **expect(store.commit).to.have.been.calledWith(CHANGE_TITLE, 
      {title: title, id: '1'})** 
      done() 
    }).catch(done) 
  }) 
}) 

为了使这个工作正常,我们还应该模拟存储的dispatch方法,因为它被用在changeTitle动作中。只需将dispatch属性添加到我们存储的模拟中,并返回一个 resolved promise:

// mock store commit and dispatch methods 
store = { 
  commit: (method, data) => {}, 
  **dispatch: () => { 
    return Promise.resolve() 
  },** 
  state: { 
    shoppinglists: lists 
  } 
} 

在这一刻检查单元测试的最终代码gist.github.com/chudaol/1405dff6a46b84c284b0eae731974050

通过为updateListcreateShoppingListdeleteShoppingList方法添加单元测试来完成actions.js的测试。在chapter7/shopping-list2文件夹中检查到目前为止的所有单元测试代码。

测试组件

现在我们所有与 Vuex 相关的函数都经过了单元测试,是时候应用特定的 Vue 组件测试技术来测试我们购物清单应用程序的组件了。

你还记得本章第一节中提到的,为了准备Vue实例进行单元测试,我们必须导入、初始化(将其传递给新的Vue实例)并挂载它。让我们开始吧!在test/unit/specs目录下创建一个components文件夹。让我们从测试AddItemComponent组件开始。创建一个AddItemComponent.spec.js文件并导入VueAddItemComponent

//AddItemComponent.spec.js 
import Vue from 'vue' 
import AddItemComponent from 'src/components/AddItemComponent' 

describe('AddItemComponent.vue', () => { 

}) 

变量AddItemComponent可以用来直接访问组件的初始数据。因此,我们可以断言,例如,组件数据初始化为一个等于空字符串的newItem属性:

describe('initialization', () => { 
  it('should initialize the component with empty string newItem', () => { 
    **expect(AddItemComponent.data()).to.eql({ 
      newItem: '' 
    })** 
  }) 
}) 

让我们现在检查一下这个组件的哪些方法可以用单元测试来覆盖。

这个组件只有一个方法,就是addItem方法。让我们来看看这个方法做了什么:

//AddItemComponent.vue 
addItem () { 
  var text 

  text = this.newItem.trim() 
  if (text) { 
    this.$emit('add', this.newItem) 
    this.newItem = '' 
    this.$store.dispatch('updateList', this.id) 
  } 
} 

这个方法访问了存储,所以我们必须使用另一种初始化组件的策略,而不是直接使用导入的值。在这种情况下,我们应该将 Vue 主组件初始化为AddItemComponent的子组件,将所有必要的属性传递给它,并使用$refs属性访问它。因此,在测试方法中,组件的初始化将如下所示:

var vm, addItemComponent; 

vm = new Vue({ 
  template: '<add-item-component :items="items" :id="id" 
  **ref="additemcomponent"**>' + 
  '</add-item-component>', 
  components: { 
    AddItemComponent 
  }, 
  data() { 
    return { 
      items: [], 
      id: 'niceId' 
    } 
  }, 
  store 
}).$mount(); 

**addItemComponent = vm.$refs.additemcomponent** 

回到方法的功能。所以,addItem方法获取实例的newItem属性,修剪它,检查它是否为假,如果不是,则触发自定义事件add,重置newItem属性,并在存储上调度updateList操作。我们可以通过为component.newItemcomponent.id分配不同的值并检查输出是否符合我们的期望来测试这个方法。

提示

正面测试意味着通过提供有效数据来测试系统。负面测试意味着通过提供无效数据来测试系统。

在我们的正面测试中,我们应该使用一个有效的字符串来初始化component.newItem属性。调用方法后,我们应该确保各种事情:

  • 组件的$emit方法已经使用add和我们分配给newItem属性的文本进行了调用

  • component.newItem已重置为空字符串

  • store 的dispatch方法已经使用组件的id属性调用了

走吧!让我们从为addItem函数添加describe方法开始:

describe(**'addItem'**, () => { 

}) 

现在我们可以添加it()方法,我们将为component.newItem分配一个值,调用addItem方法,并检查我们需要检查的一切:

//AddItemComponent.spec.js 
it('should call $emit method', () => { 
  let newItem = 'Learning Vue JS' 
  // stub $emit method 
  sinon.stub(component, '$emit') 
  // stub store's dispatch method 
  sinon.stub(store, 'dispatch') 
  // set a new item 
  **component.newItem = newItem** 
  component.addItem() 
  // newItem should be reset 
  **expect(component.newItem).to.eql('')** 
  // $emit should be called with custom event 'add' and a newItem value 
  **expect(component.$emit).to.have.been.calledWith('add', newItem)** 
  // dispatch should be called with updateList and the id of the list 
  **expect(store.dispatch).to.have.been.calledWith('updateList', 
  'niceId')** 
  store.dispatch.restore() 
  component.$emit.restore() 
}) 

运行测试并检查它们是否通过,一切都正常。检查chapter7/shopping-list3文件夹中的AddItemComponent的最终代码。

尝试为购物清单应用程序的其余组件编写单元测试。记得编写单元测试来覆盖你的代码,这样如果你改变了代码,它就会出错。

为我们的番茄钟应用程序编写单元测试

好的!让我们转到我们的番茄钟应用程序!顺便问一下,你上次休息是什么时候?也许,现在是时候在浏览器中打开应用程序,等待几分钟的番茄工作时间计时器,然后检查一些小猫。

我刚刚做了,这让我感觉真的很好,很可爱。

为我们的番茄钟应用程序编写单元测试

我不是你的衣服...请休息一下

让我们从 mutations 开始。打开chapter7/pomodoro文件夹中的代码。打开mutations.js文件并检查那里发生了什么。有四个 mutations 发生:STARTSTOPPAUSETOGGLE_SOUND。猜猜我们将从哪一个开始。是的,你猜对了,我们将从start方法开始。在test/unit/specs文件夹内创建一个vuex子文件夹,并添加mutations.spec.js文件。让我们准备好进行测试:

// mutations.spec.js 
import Vue from 'vue' 
import mutations from 'src/vuex/mutations' 
import * as types from 'src/vuex/mutation_types' 

describe('mutations', () => { 
  var state 

  beforeEach(() => { 
    state = {} 
    // let's mock Vue noise plugin 
    //to be able to listen on its methods 
    **Vue.noise = { 
      start: () => {}, 
      stop: () => {}, 
      pause: () => {} 
    }** 
    sinon.spy(Vue.noise, 'start') 
    sinon.spy(Vue.noise, 'pause') 
    sinon.spy(Vue.noise, 'stop') 
  }) 

  afterEach(() => { 
    **Vue.noise.start.restore() 
    Vue.noise.pause.restore() 
    Vue.noise.stop.restore()** 
  }) 

  describe(**'START'**, () => { 
  }) 
}) 

请注意,我对噪音生成器插件的所有方法进行了模拟。这是因为在这个规范中,我们不需要测试插件的功能(实际上,在发布之前,我们必须在插件本身的范围内进行测试)。在这个测试范围内,我们应该测试插件的方法在需要调用时是否被调用。

为了能够测试 start 方法,让我们思考应该发生什么。在点击开始按钮后,我们知道应用程序的 startedpausedstopped 状态必须获得一些特定的值(实际上分别是 truefalsefalse)。我们还知道应用程序的间隔应该启动。我们还知道如果番茄钟的状态是 working,并且声音已启用,噪音生成器插件的 start 方法应该被调用。实际上,这就是我们的方法实际在做的事情:

[types.START] (state) { 
  state.started = true 
  state.paused = false 
  state.stopped = false 
  state.interval = setInterval(() => tick(state), 1000) 
  if (state.isWorking && state.soundEnabled) { 
    Vue.noise.start() 
  } 
}, 

但即使它没有做所有这些事情,我们已经编写了测试来测试它,我们会立即意识到我们的代码中缺少了一些东西,并加以修复。让我们写我们的测试。让我们首先定义 it() 方法,测试所有属性是否被正确设置。为了确保在调用方法之前它们没有被设置,让我们还断言在测试开始时这些属性都未被定义:

it('should set all the state properties correctly after start', () => { 
  // ensure that all the properties are undefined 
  // before calling the start method 
  expect(state.started).to.be.undefined 
  expect(state.stopped).to.be.undefined 
  expect(state.paused).to.be.undefined 
  expect(state.interval).to.be.undefined 
  // call the start method 
  mutationstypes.START 
  // check that all the properties were correctly set 
  expect(state.started).to.be.true 
  expect(state.paused).to.be.false 
  expect(state.stopped).to.be.false 
  expect(state.interval).not.to.be.undefined 
}) 

现在让我们检查 Vue.noise.start 方法。我们知道只有当 state.isWorkingtruestate.soundEnabledtrue 时才应该调用它。让我们写一个正面测试。在这个测试中,我们会将两个布尔状态都初始化为 true,并检查 noise.start 方法是否被调用:

it('should call Vue.noise.start method if both state.isWorking and state.soundEnabled are true', () => { 
  state.**isWorking** = true 
  state.**soundEnabled** = true 
  mutationstypes.START 
  expect(Vue.noise.start).**to.have.been.called** 
}) 

让我们为每个状态添加两个负面测试,isWorkingsoundEnabled 都设为 false

it('should not call Vue.noise.start method if state.isWorking is not true', () => { 
  **state.isWorking = false** 
  state.soundEnabled = true 
  mutationstypes.START 
  expect(Vue.noise.start).**to.not.have.been.called** 
}) 

it('should not call Vue.noise.start method if state.soundEnabled is not true', () => { 
  state.isWorking = true 
  **state.soundEnabled = false** 
  mutationstypes.START 
  expect(Vue.noise.start).**to.not.have.been.called** 
}) 

我们的 start 变异已经很好地测试了!在 chapter7/pomodoro2 文件夹中检查代码的最终状态。我建议你现在写其余的单元测试,不仅测试变异,还要测试所有存储相关的函数,包括在 getters 和 actions 中的函数。之后,应用我们刚学到的技术来测试 Vue 组件,并测试我们番茄钟应用程序的一些组件。

在这一点上,我们已经完成了单元测试!

什么是端到端测试?

端到端e2e)测试是一种技术,用于测试应用程序的整个流程。在这种测试中,既不使用模拟对象也不使用存根,而是对真实系统进行测试。进行端到端测试可以测试应用程序的所有方面——API、前端、后端、数据库、服务器负载,从而确保系统集成的质量。

在 Web 应用程序的情况下,这些测试是通过 UI 测试执行的。每个测试都描述了从打开浏览器到关闭浏览器的所有步骤。必须描述为实现某些系统功能而需要执行的所有步骤。实际上,这与您在应用程序页面上单击并执行一些操作的方式相同,但是是自动化和快速的。在本节中,我们将看到 Selenium webdriver 是什么,Nightwatch 是什么,以及它们如何用于为我们的应用程序创建端到端测试。

端到端的 Nightwatch

如果您已经使用过测试自动化,或者与使用测试自动化的人一起工作过,那么肯定已经听说过 Selenium 这个神奇的词语——Selenium 可以打开浏览器,点击,输入,像人一样做任何事情,以并行、良好分布、多平台和跨浏览器的方式。实际上,Selenium 只是一个包含 API 的 JAR 文件,用于在浏览器上执行不同的操作(点击、输入、滚动等)。

注意

查看 Selenium 的文档www.seleniumhq.org/

当执行这个 JAR 文件时,它会连接到指定的浏览器,打开 API,并等待在浏览器上执行命令。发送到 Selenium 服务器的命令可以以各种不同的方式和语言执行。

有很多现有的实现和框架可以让您用几行代码调用 Selenium 命令:

在我们的案例中,我们将使用 Nightwatch,这是一个很好且非常易于使用的测试框架,可以使用 JavaScript 调用 Selenium 的命令。

查看 Nightwatch 的文档nightwatchjs.org/

Vue 应用程序使用vue-cli webpack方法引导时,已经包含了对 Nightwatch 测试的支持,无需安装任何东西。基本上,每个测试规范看起来都有点像下面这样:

module.exports = { 
  'e2e test': function (browser) { 
    browser 
    .**url**('http://localhost:8080') 
      .**waitForElementVisible**('#app', 5000) 
      .assert.**elementPresent**('.logo') 
      .assert.**containsText**('h1', 'Hello World!') 
      .assert.**elementCount**('p', 3) 
      .end() 
  } 
} 

语法很好,易于理解。每个突出显示的方法都是一个 Nightwatch 命令,其背后会被转换为 Selenium 命令并被调用。在官方文档页面nightwatchjs.org/api#commands上检查 Nightwatch 命令的完整列表。

为番茄钟应用编写端到端测试

现在我们知道了 UI 测试背后的所有理论,我们可以为我们的番茄钟应用创建我们的第一个端到端测试。让我们定义我们将执行的步骤和我们应该测试的事情。首先,我们应该打开浏览器。然后,我们可能应该检查我们的容器(具有#app ID)是否在页面上。

我们还可以尝试检查暂停和停止按钮是否禁用,以及页面上是否不存在声音切换按钮。

然后我们可以点击开始按钮,检查声音切换按钮是否出现,开始按钮是否变为禁用状态,暂停和停止按钮是否变为启用状态。还有无数种可能的点击和检查,但让我们至少执行描述的步骤。让我们用项目符号的形式写出来:

  1. http://localhost:8080上打开浏览器。

  2. 检查页面上是否有#app元素。

  3. 检查.toggle-volume图标是否不可见。

  4. 检查'[title=pause]''[title=stop]'按钮是否禁用,'[title=start]'按钮是否启用。

  5. 点击'[title=start]'按钮。

  6. 检查'[title=pause]''[title=stop]'按钮是否现在启用,'[title=start]'按钮是否禁用。

  7. 检查.toggle-volume图标现在是否可见。

让我们开始吧!只需打开tests/e2e/specs文件夹中的test.js文件,删除其内容,并添加以下代码:

module.exports = { 
  'default e2e tests': (browser) => { 
    // open the browser and check that #app is on the page 
    browser.url('http://localhost:8080') 
      .waitForElementVisible('#app', 5000); 
    // check that toggle-volume icon is not visible 
    browser.expect.element('.toggle-volume') 
      .to.not.be.visible 
    // check that pause button is disabled 
    browser.expect.element('[title=pause]') 
      .to.have.attribute('disabled') 
    // check that stop button is disabled 
    browser.expect.element('[title=stop]') 
      .to.have.attribute('disabled') 
    // check that start button is not disabled            
    browser.expect.element('[title=start]') 
      .to.not.have.attribute('disabled') 
    // click on start button, check that toggle volume 
    // button is visible 
    browser.click('[title=start]') 
      .waitForElementVisible('.toggle-volume', 5000) 
    // check that pause button is not disabled 
    browser.expect.element('[title=pause]') 
      .to.not.have.attribute('disabled') 
    // check that stop button is not disabled 
    browser.expect.element('[title=stop]') 
      .to.not.have.attribute('disabled') 
    // check that stop button is disabled 
    browser.expect.element('[title=start]') 
      .to.have.attribute('disabled') 
    browser.end() 
  } 
} 

你看到这种语言是多么友好吗?现在让我们进行一项检查,看看在工作时间结束后,小猫元素是否出现在屏幕上。为了使测试更短,不必等待很长时间才能通过测试,让我们将工作时间设定为 6 秒。在我们的config.js文件中更改这个值:

//config.js 
export const WORKING_TIME = 0.1 * 60 

包含猫图片的元素具有'div.well.kittens'选择器,因此我们将检查它是否可见。让我们在这个测试中检查,在小猫元素出现后,图像的来源是否包含'thecatapi'字符串。这个测试将如下所示:

'wait for kitten test': (browser) => { 
  browser.url('http://localhost:8080') 
    .waitForElementVisible('#app', 5000) 
  // initially the kitten element is not visible 
  browser.expect.element('.well.kittens') 
    .to.not.be.visible 
  // click on the start button and wait for 7s for 
  //kitten element to appear 
  browser.click('[title=start]') 
    .waitForElementVisible('.well.kittens', 7000) 
  // check that the image contains the src element 
  //that matches thecatapi string 
  browser.expect.element('.well.kittens img') 
    .to.have.attribute('src') 
    .which.matches(/thecatapi/); 
  browser.end() 
} 

运行测试。为了做到这一点,调用e2e npm 命令:

**npm run e2e** 

你会看到浏览器自己打开并执行所有操作。

这是一种魔法!

我们所有的测试都通过了,所有的期望都得到了满足;查看控制台:

为番茄钟应用程序编写 e2e 测试

所有测试都通过了!

恭喜!你刚刚学会了如何使用 Nightwatch 编写 e2e 测试。检查chapter7/pomodoro3文件夹中的代码。为我们的番茄钟应用程序编写更多的测试用例。不要忘记我们的购物清单应用程序,它可能有更多的 UI 测试场景。编写它们并检查 Selenium 如何为你工作。如果你决定增强代码,你的代码质量不仅受到单元测试的保护,而且现在还应用了回归测试。每次更改代码时,只需运行一个命令来运行两种类型的测试:

**npm test** 

现在你肯定值得休息一下。拿一杯咖啡或茶,打开番茄钟应用程序页面,等待 6 秒,欣赏我们的小毛绒朋友:

为番茄钟应用程序编写 e2e 测试

实际上,这不是来自 thecatapi 的小猫。这是我的猫 Patuscas 祝愿大家有一个愉快的休息时间!

总结

在这一章中,我们已经测试了我们的两个应用程序。我们为 Vuex 方法和 Vue 组件编写了单元测试。我们使用了简单的单元测试和异步单元测试,并熟悉了 Sinon 的模拟技术,比如对方法进行间谍操作和伪造服务器响应。我们还学会了如何使用 Nightwatch 创建 UI 测试。我们的应用程序现在经过了测试,准备部署到生产环境!我们将在下一章中了解如何部署它们,下一章将专门讨论使用 Heroku 云应用平台部署应用程序。

第八章:部署-时间上线!

在上一章中,您学会了如何测试您的 Vue 应用程序。我们应用了不同的测试技术进行测试。一开始,我们对 Vue 组件和与 Vuex 相关的模块(如 actions、mutations 和 getters)进行了经典的单元测试。之后,我们学会了如何使用 Nightwatch 应用端到端测试技术。

在本章中,我们将通过将应用程序部署到服务器并使其对世界可用来使我们的应用程序上线。我们还将保证我们的应用程序进行持续集成和持续部署。这意味着每当我们提交对应用程序所做的更改时,它们将自动进行测试和部署。

考虑到这一点,在本章中,我们将做以下事情:

  • 使用 Travis 设置持续集成流程

  • 使用 Heroku 设置持续部署

软件部署

在开始部署我们的应用程序之前,让我们首先尝试定义它实际上意味着什么:

“软件部署是使软件系统可供使用的所有活动。” - 维基百科:https://en.wikipedia.org/wiki/Software_deployment

这个定义意味着在我们执行所有必要的活动之后,我们的软件将对公众可用。在我们的情况下,由于我们正在部署 Web 应用程序,这意味着将有一个公共 URL,任何人都可以在其浏览器中输入此 URL 并访问该应用程序。如何实现这一点?最简单的方法是向您的朋友提供您自己的 IP 地址并运行该应用程序。因此,在您的私人网络内的人将能够在其浏览器上访问该应用程序。因此,例如,运行番茄钟应用程序:

**> cd <path to pomodoro> 
> npm run dev** 

然后检查你的 IP:

**ifconfig**

软件部署

使用 ifconfig 命令检查 IP 地址

然后与在同一私人网络上的朋友分享地址。在我的情况下,它将是http://192.168.1.6:8080

然而,只有在你的网络内的朋友才能访问该应用程序,显然这样并不那么有趣。

您可以使用一些软件来创建一个公共可访问的地址,从而将您的计算机转变为一个托管提供者,例如ngrokngrok.com/)。运行该应用程序,然后运行以下命令:

**ngrok http 8080** 

这将创建一个地址,可以从任何地方访问,就像一个常规网站:

软件部署

使用 ngrok 为本地主机提供隧道

在我的情况下,它将是http://5dcb8d46.ngrok.io。我可以在我的社交网络上分享这个地址,每个人都可以访问并尝试 Pomodoro 应用程序!但是停下…我可以让我的笔记本电脑整夜开着,但我不能永远让它开着。一旦我关闭它,网络连接就会丢失,我的应用程序就无法访问了。而且,即使我可以让它永远开着,我也不喜欢这个网站地址。这是一堆字母和数字,我希望它有意义。

还有更强大的方法。例如,我可以在AWS亚马逊网络服务)上购买一个虚拟实例,将我的应用程序复制到这个实例上,在 GoDaddy 等域名提供商购买一个域名,将该域名与购买的实例 IP 关联,并在那里运行应用程序,它将是可访问的,维护、备份和由亚马逊服务照料。令人惊讶,但…贵得要命。让我们在我们的应用程序达到相应规模和回报水平时考虑这个解决方案。

就目前而言,在这一章中,我们希望我们的部署解决方案是便宜的(便宜意味着免费)、强大和简单。这就是为什么我们将部署我们的应用程序到 Heroku,一个云应用平台。为了做到这一点,我们将首先将我们的应用程序托管在 GitHub 上。你还记得部署是使我们的应用程序准备好使用的东西吗?我认为一个应用程序在经过测试并且测试没有失败时才能使用。这就是为什么在实际部署之前,我们还将使用 Travis 来保证我们应用程序的质量。因此,我们部署应用程序的必要活动将是以下内容:

  1. 为应用程序创建 GitHub 存储库,并将应用程序移入存储库。

  2. 使用 Travis 进行持续集成。

  3. 将应用程序连接到 Heroku,并设置和配置它们,以便 Heroku 运行它们并向世界公开它们。

在接下来的三个小节中,我将简要介绍 GitHub、Travis 和 Heroku。

GitHub 是什么?

GitHub 是基于 Git 的项目的托管提供商。

它可以在小型个人规模上用于个人私人和公共项目。它也可以用于大型企业项目和所有与开发相关的活动,如代码审查,持续集成等等。

生活在开源软件世界的每个人都知道 GitHub。如果你正在阅读这本关于 Vue 的书,它托管在 GitHub 上(github.com/vuejs/),我相信你会跳过这一小节,所以我可能会在这里写一些愚蠢的笑话,而你永远不会注意到它们!开玩笑!

Travis 是什么?

Travis 是 GitHub 的一个工具,它允许我们将 GitHub 项目连接到它,并确保它们的质量。它在您的项目中运行测试,并告诉您构建是否通过,或者警告您构建失败了。在travis-ci.org/上了解更多关于 Travis 以及如何使用它。

Heroku 是什么?

Heroku 是一个用于部署应用程序的云平台。它非常容易使用。您只需创建一个应用程序,给它一个好的有意义的名称,将其连接到您的 GitHub 项目,然后就完成了!每次您推送到特定分支(例如master分支),Heroku 将运行您提供的脚本作为应用程序的入口点脚本,并重新部署它。

它是高度可配置的,还提供了命令行界面,这样您就可以从本地命令行访问所有应用程序,而无需检查 Heroku 仪表板网站。让我们开始学习并亲自做一切。

将应用程序移动到 GitHub 存储库

让我们从为我们的应用程序创建 GitHub 存储库开始。

请使用chapter8/pomodorochapter8/shopping-list目录中的代码。

如果您还没有 GitHub 帐户,请创建一个。现在登录到您的 GitHub 帐户并创建两个存储库,PomodoroShoppingList

将应用程序移动到 GitHub 存储库

在 GitHub 上创建存储库

一旦你点击创建存储库按钮,会出现一个包含不同指令的页面。我们特别关注第二段,它说...或在命令行上创建一个新的存储库。复制它,粘贴到 Pomodoro 应用程序目录中的命令行中,删除第一行(因为我们已经有了 README 文件),并修改第三行以添加目录中的所有内容,然后点击Enter按钮:

**git init**
**git add** 
**git commit -m "first commit"**
**git remote add origin https://github.com/chudaol/Pomodoro.git**
**git push -u origin master**

刷新你的 GitHub 项目页面,你会看到所有的代码都在那里!在我的情况下,它在github.com/chudaol/Pomodoro

对于购物清单应用程序也是一样。我刚刚做了,现在在这里:github.com/chudaol/ShoppingList

如果你不想创建自己的存储库,你可以直接 fork 我的。开源就是开放的!

使用 Travis 设置持续集成

为了能够使用 Travis 设置持续集成,首先你必须将你的 Travis 账户与你的 GitHub 账户连接起来。打开travis-ci.org/,点击使用 GitHub 登录按钮:

使用 Travis 设置持续集成

点击使用 GitHub 登录按钮

现在你可以添加要由 Travis 跟踪的存储库。点击加号(+):

使用 Travis 设置持续集成

点击加号添加你的 GitHub 项目

点击加号按钮后,你的 GitHub 项目的整个列表会出现。选择你想要跟踪的项目:

使用 Travis 设置持续集成

选择你想要用 Travis 跟踪的项目

现在我们的项目已经连接到 Travis 构建系统,它会监听对master分支的每次提交和推送,我们需要告诉它一些东西,一旦它检测到变化。所有 Travis 的配置都应该存储在.travis.yml文件中。将.travis.yml文件添加到这两个项目中。至少我们要告诉它应该使用哪个节点版本。检查你系统的 Node 版本(这是你完全确定可以与我们的项目一起工作的版本)。只需运行以下命令:

**node --version** 

在我的情况下,它是v5.11.0。所以我会把它添加到.travis.yml文件中:

//.travis.yml 
language: node_js 
node_js: 
  - "**5.11.0**" 

如果你现在提交并推送,你会发现 Travis 会自动开始运行测试。默认情况下,它会在项目上调用npm test命令。等待几分钟,观察结果。不幸的是,在执行端到端(Selenium)测试时会失败。为什么会发生这种情况呢?

默认情况下,Travis 构建和测试环境的虚拟镜像没有安装 Chrome 浏览器。而我们的 Selenium 测试正试图在 Chrome 浏览器上运行。但幸运的是,Travis 提供了在构建之前执行一些命令的机制。这应该在 YML 文件的before_script部分中完成。让我们调用必要的命令来安装 Chrome 并导出CHROME_BIN变量。将以下内容添加到你的.travis.yml文件中:

before_script: 
  - export CHROME_BIN=/usr/bin/google-chrome 
  - sudo apt-get update 
  - sudo apt-get install -y libappindicator1 fonts-liberation 
  - wget https://dl.google.com/linux/direct/google-chrome-
    stable_current_amd64.deb 
  - sudo dpkg -i google-chrome*.deb 

如你所见,为了执行安装和系统更新,我们必须使用sudo来调用命令。默认情况下,Travis 不允许你执行sudo命令,以防止不可信任的脚本造成意外损害。但你可以明确告诉 Travis 你的脚本使用了sudo,这意味着你知道自己在做什么。只需将以下行添加到你的.travis.yml文件中:

sudo: required 
dist: trusty  

现在你的整个.travis.yml文件应该如下所示:

//.travis.yml 
language: node_js 
**sudo: required 
dist: trusty** 
node_js: 
  - "5.11.0" 

before_script: 
  - export CHROME_BIN=/usr/bin/google-chrome 
  - sudo apt-get update 
  - sudo apt-get install -y libappindicator1 fonts-liberation 
  - wget https://dl.google.com/linux/direct/google-chrome-
    stable_current_amd64.deb 
  - sudo dpkg -i google-chrome*.deb 

尝试提交并检查你的 Travis 仪表板。

哦,不!它又失败了。这次,似乎是超时问题:

使用 Travis 进行持续集成设置

即使安装了 Chrome,测试仍然会由于超时而悄悄失败

为什么会发生这种情况?让我们回想一下当我们运行端到端测试时实际发生了什么。每个测试都会打开浏览器,然后执行点击、输入和其他操作来测试我们的用户界面。最后一句话的关键词是用户界面。如果我们需要测试用户界面,我们需要一个图形用户界面GUI)。Travis 虚拟镜像没有图形显示。因此,它们无法打开浏览器并在其中显示我们的用户界面。幸运的是,有一种叫做Xvfb - X 虚拟帧缓冲的好东西。

Xvfb 是一个显示服务器,实现了物理显示使用的协议。所有需要的图形操作都在内存中执行;因此,不需要物理显示。因此,我们可以运行一个 Xvfb 服务器,为我们的测试提供虚拟图形环境。如果您仔细阅读 Travis 文档,您会发现这正是它建议的运行需要 GUI 的测试的方法:docs.travis-ci.com/user/gui-and-headless-browsers/#Using-xvfb-to-Run-Tests-That-Require-a-GUI。因此,打开.travis.yml文件,并将以下内容添加到before_script部分:

  - export DISPLAY=:99.0 
  - sh -e /etc/init.d/xvfb start 

整个 YML 文件现在看起来像下面这样:

//.travis.yml 
language: node_js 
sudo: required 
dist: trusty 
node_js: 
  - "5.11.0" 

before_script: 
  - export CHROME_BIN=/usr/bin/google-chrome 
  - sudo apt-get update 
  - sudo apt-get install -y libappindicator1 fonts-liberation 
  - wget https://dl.google.com/linux/direct/google-chrome-
    stable_current_amd64.deb 
  - sudo dpkg -i google-chrome*.deb 
  - export DISPLAY=:99.0 
  - sh -e /etc/init.d/xvfb start 

提交并检查您的 Travis 仪表板。Pomodoro 应用程序已成功构建!

使用 Travis 设置持续集成

Pomodoro 应用程序构建成功!

然而,购物清单应用程序的构建失败了。请注意,Travis 甚至会为每个构建状态更改选项卡的标题颜色:

使用 Travis 设置持续集成

Travis 根据构建状态更改选项卡标题上的图标

购物清单应用程序的构建发生了什么?在端到端测试中有一步检查页面上是否存在Groceries标题。问题是,这个标题来自我们的后端服务器,应该使用npm run server命令运行。你还记得我们在第六章中实现它的吗,插件-用自己的砖头建造你的房子,使用了vue-resource插件?这意味着在构建应用程序之前,我们需要告诉 Travis 运行我们的小服务器。只需将以下行添加到购物清单应用程序的.travis.yml文件中:

- nohup npm run server & 

提交您的更改并检查 Travis 仪表板。构建通过了!一切都是绿色的,我们很高兴(至少我是,我希望成功的构建也能让你开心)。现在,如果我们能告诉世界我们的构建是通过的,那就太好了。我们可以通过将 Travis 按钮添加到我们的README.md文件中来实现这一点。这将使我们能够立即在项目的 GitHub 页面上看到构建状态。

在应用程序的 Travis 页面上点击构建通过按钮,从第二个下拉列表中选择Markdown选项,并将生成的文本复制到README.md文件中:

使用 Travis 设置持续集成

点击通过构建按钮,从第二个下拉菜单中选择 Markdown 选项,并将文本复制到 README.md 文件中

看看它在我们项目的 GitHub 页面的 README 文件中是多么漂亮:

使用 Travis 设置持续集成

Travis 按钮在 GitHub 页面上的项目的 README 文件中看起来真的很漂亮

现在我们的应用程序在每次提交时都会被检查,因此我们可以确保它们的质量,最终将它们部署到公共可访问的地方。

在开始部署过程之前,请在 Heroku(signup.heroku.com/dc)创建一个帐户并安装 Heroku Toolbelt(devcenter.heroku.com/articles/getting-started-with-nodejs#set-up)。

现在我们准备部署我们的项目。

部署番茄应用程序

让我们从在 Heroku 账户中添加新应用开始。在 Heroku 仪表板上点击创建新应用按钮。你可以创建自己的名称,也可以将名称输入字段留空,Heroku 会为你创建一个名称。我会将我的应用称为catodoro,因为它是有猫的番茄!

部署番茄应用程序

使用 Heroku 创建一个新应用

点击创建应用按钮,然后选择一个部署流水线来部署你的应用。选择 GitHub 方法,然后从建议的 GitHub 项目下拉菜单中选择我们想要部署的项目:

部署番茄应用程序

选择 GitHub 部署方法,并从 GitHub 项目中选择相应的项目

点击连接按钮后,你可能想要检查的两件事是 从主分支启用自动部署等待 CI 通过后再部署 选项:

部署番茄应用程序

勾选等待 CI 通过后再部署复选框,然后点击启用自动部署按钮

一切都准备好进行第一次部署,甚至可以单击Deploy Branch按钮,Heroku 将尝试执行构建,但是,如果您尝试在浏览器中打开应用程序,它将无法工作。如果您想知道原因,您应该始终查看执行此类操作时的运行日志。

检查日志

我希望您已经成功安装了 Heroku CLI(或 Heroku 工具包),现在您可以在命令行中运行heroku命令。让我们检查日志。在您的 shell 中运行heroku logs命令:

**heroku logs --app catodoro --tail** 

当 Heroku 尝试执行构建时,您将看到一个持续运行的日志。错误是npm ERR! missing script: start。我们在package.json文件中没有start脚本。

这是完全正确的。为了创建一个启动脚本,让我们首先尝试了解如何为生产构建和运行 Vue 应用程序。README 文件告诉我们需要运行npm run build命令。让我们在本地运行它并检查发生了什么:

检查日志

npm run build 命令的输出

因此,我们知道构建命令的结果会进入dist文件夹。我们还知道我们必须使用 HTTP 服务器从此文件夹中提供index.html文件。我们还知道我们必须在package.json文件的scripts部分中创建一个start脚本,以便 Heroku 知道如何运行我们的应用程序。

准备在 Heroku 上运行应用程序

通过检查日志文件,我们能够收集了大量信息。在继续部署应用程序的步骤之前,让我们在这里总结一下 Heroku 在运行应用程序之前的流程。

因此,Heroku 执行以下操作:

  • 运行npm install脚本以安装所有所需的依赖项(它检查package.json文件的dependencies部分中的依赖项)

  • package.json运行npm start脚本,并在已知的 web 地址上提供其结果

因此,根据这些信息和我们从日志和运行npm build脚本中收集到的信息,我们需要执行以下操作:

  • 告诉 Heroku 安装所有所需的依赖项;为此,我们需要将项目依赖项从package.json文件的devDependencies部分移动到dependencies部分,以便 Heroku 安装它们

  • 告诉 Heroku 在执行npm install后运行构建脚本;为此,我们需要在package.json文件中创建一个postinstall脚本,其中我们将调用npm run build命令。

  • 创建一个server.js文件,从dist文件夹中提供index.html文件

  • 提供 Heroku 运行server.js脚本的方法;为此,我们需要在package.json文件中创建一个start脚本来运行server.js脚本

首先,将package.json文件的devDependencies部分中除了与测试有关的依赖之外的所有依赖移动到dependencies部分中:

"dependencies": { 
  "autoprefixer": "⁶.4.0", 
  "babel-core": "⁶.0.0", 
  "babel-eslint": "⁷.0.0", 
  "babel-loader": "⁶.0.0", 
  "babel-plugin-transform-runtime": "⁶.0.0", 
  "babel-polyfill": "⁶.16.0", 
  "babel-preset-es2015": "⁶.0.0", 
  "babel-preset-stage-2": "⁶.0.0", 
  "babel-register": "⁶.0.0", 
  "chalk": "¹.1.3", 
  "connect-history-api-fallback": "¹.1.0", 
  "cross-spawn": "⁴.0.2", 
  "css-loader": "⁰.25.0", 
  "es6-promise": "⁴.0.5", 
  "eslint": "³.7.1", 
  "eslint-config-standard": "⁶.1.0", 
  "eslint-friendly-formatter": "².0.5", 
  "eslint-loader": "¹.5.0", 
  "eslint-plugin-html": "¹.3.0", 
  "eslint-plugin-promise": "².0.1", 
  "eslint-plugin-standard": "².0.1", 
  "eventsource-polyfill": "⁰.9.6", 
  "express": "⁴.13.3", 
  "extract-text-webpack-plugin": "¹.0.1", 
  "file-loader": "⁰.9.0", 
  "function-bind": "¹.0.2", 
  "html-webpack-plugin": "².8.1", 
  "http-proxy-middleware": "⁰.17.2", 
  "inject-loader": "².0.1", 
  "isparta-loader": "².0.0", 
  "json-loader": "⁰.5.4", 
  "lolex": "¹.4.0", 
  "opn": "⁴.0.2", 
  "ora": "⁰.3.0", 
  "semver": "⁵.3.0", 
  "shelljs": "⁰.7.4", 
  "url-loader": "⁰.5.7", 
  "vue": "².0.1", 
  "vuex": "².0.0", 
  "vue-loader": "⁹.4.0", 
  "vue-style-loader": "¹.0.0", 
  "webpack": "¹.13.2", 
  "webpack-dev-middleware": "¹.8.3", 
  "webpack-hot-middleware": "².12.2", 
  "webpack-merge": "⁰.14.1" 
}, 
"devDependencies": { 
  "chai": "³.5.0", 
  "chromedriver": "².21.2", 
  "karma": "¹.3.0", 
  "karma-coverage": "¹.1.1", 
  "karma-mocha": "¹.2.0", 
  "karma-phantomjs-launcher": "¹.0.0", 
  "karma-sinon-chai": "¹.2.0", 
  "karma-sourcemap-loader": "⁰.3.7", 
  "karma-spec-reporter": "0.0.26", 
  "karma-webpack": "¹.7.0", 
  "mocha": "³.1.0", 
  "nightwatch": "⁰.9.8", 
  "phantomjs-prebuilt": "².1.3", 
  "selenium-server": "2.53.1", 
  "sinon": "¹.17.3", 
  "sinon-chai": "².8.0" 
} 

现在让我们创建一个postinstall脚本,在其中我们将告诉 Heroku 运行npm run build脚本。在scripts部分中,添加postinstall脚本:

  "**scripts**": { 
    <...> 
    **"postinstall": "npm run build"** 
  }, 

现在让我们创建一个server.js文件,在其中我们将从dist目录中提供index.html文件。在项目文件夹中创建一个server.js文件,并添加以下内容:

// server.js 
var express = require('express'); 
var serveStatic = require('serve-static'); 
var app = express(); 
app.use(serveStatic(__dirname + '/dist')); 
var port = process.env.PORT || 5000; 
app.listen(port); 
console.log('server started '+ port); 

好的,现在我们只需要在package.json文件的scripts部分创建一个start脚本,然后我们就完成了!我们的start脚本应该只运行node server.js,所以让我们来做吧:

  "**scripts**": { 
    <...> 
    "postinstall": "npm run build", 
    **"start": "node server.js"** 
  }, 

提交您的更改,转到 Heroku 仪表板,然后点击Deploy Branch按钮。不要忘记检查运行日志!

哇哦!构建成功了!成功构建后,您被邀请点击View按钮;别害羞,点击它,您将看到您的应用程序在运行!

准备在 Heroku 上运行应用程序

番茄钟应用程序已成功部署到 Heroku

现在您可以在任何地方使用您的番茄钟应用程序。现在您也可以邀请您的朋友使用它,只需提供 Heroku 链接即可。

恭喜!您刚刚部署了您的 Vue 应用程序,每个人都可以使用它。多么美好啊!

部署购物清单应用程序

为了部署我们的购物清单应用程序,我们需要执行与番茄钟应用程序完全相同的步骤。

在您的 Heroku 仪表板上创建一个新应用程序,并将其连接到您的 GitHub 购物清单项目。之后,从番茄钟应用程序中复制server.js文件,处理package.json文件中的依赖关系,并创建postinstallstart脚本。

然而,我们还有一步要做。不要忘记我们的后端服务器,为购物清单提供 REST API。我们也需要运行它。

或者更好的是,如果我们可以只运行一个服务器来完成所有工作,为什么我们需要运行两个服务器呢?我们可以通过为其提供路由路径来将我们的 JSON 服务器与我们的 express 服务器集成以提供购物清单端点,比如api。打开server.js文件,在那里导入jsonServer依赖项,并告诉 express 应用程序使用它。因此,你的server.js文件将如下所示:

//server.js 
var express = require('express'); 
**var jsonServer = require('json-server');** 
var serveStatic = require('serve-static'); 
var app = express(); 

app.use(serveStatic(__dirname + '/dist')); 
**app.use('/api', jsonServer.router('server/db.json'));** 
var port = process.env.PORT || 5000; 
app.listen(port); 
console.log('server started '+ port); 

使用前一行,我们告诉我们的 express 应用程序使用jsonServer并在/api/端点上提供db.json文件。

我们还应该更改Vue资源中的端点地址。打开 API 文件夹中的index.js,并用api前缀替换localhost:3000

const ShoppingListsResource = Vue.resource('api/' + 'shoppinglists{/id}') 

我们还应该在dev-server.js中添加 JSON 服务器支持;否则,我们将无法以开发模式运行应用程序。因此,打开build/dev-server.js文件,导入jsonServer,并告诉 express 应用程序使用它:

//dev-server.js 
var path = require('path') 
var express = require('express') 
**var jsonServer = require('json-server')** 
<...> 
// compilation error display 
app.use(hotMiddleware) 

**// use json server 
app.use('/api', jsonServer.router('server/db.json'));** 
<...> 

尝试以开发模式运行应用程序(npm run dev)。一切正常。

现在你也可以从travis.yml文件中删除运行服务器的命令(- nohup npm run server &)。你也可以从package.json中删除服务器脚本。

在本地运行测试并检查它们是否失败。

我们几乎完成了。让我们在本地尝试我们的应用程序。

尝试在本地使用 Heroku

有时候要让事情运行起来需要很多次尝试和失败的迭代。我们尝试一些东西,提交,推送,尝试部署,看看是否起作用。我们意识到我们忘记了一些东西,提交,推送,尝试部署,看错误日志。一遍又一遍地做。这可能会非常耗时,因为网络上的事情需要时间!幸运的是,Heroku CLI 提供了一种在本地运行应用程序的方法,就像它已经部署到 Heroku 服务器上一样。你只需要在构建应用程序后立即运行heroku local web命令:

**npm run build 
heroku local web** 

试一下。

在浏览器中打开http://localhost:5000。是的,它起作用了!

尝试在本地使用 Heroku

使用 Heroku 本地 web 命令在本地运行应用程序。它起作用了!

现在让我们提交并推送更改。

现在你可以等待 Travis 成功构建并 Heroku 自动部署,或者你可以打开你的 Heroku 仪表板,点击Deploy Branch按钮。等一会儿。然后... 它起作用了!这是我们今天执行的两次部署的结果:

各自的 GitHub 存储库可以在github.com/chudaol/Pomodorogithub.com/chudaol/ShoppingList找到。

分叉,玩耍,测试,部署。此刻,您拥有增强,改进并向全世界展示这些应用程序所需的所有工具。感谢您与我一起经历这激动人心的旅程!

第九章:接下来是什么?

在上一章中,我们通过将应用程序部署到服务器并使其对外可用,使我们的应用程序上线。我们还保证了应用程序的持续集成和持续部署。这意味着每当我们提交对应用程序的更改时,它们将自动进行测试和部署。

看起来我们在这本书中的旅程已经结束了。但实际上,它才刚刚开始。尽管我们已经发现和学到了很多,但仍有很多工作要做!在本章中,我们将总结我们迄今为止学到的一切,看看我们还有什么需要学习,以及我们还可以做些什么来提升我们应用程序的酷炫程度。因此,在本章中,我们将做以下事情:

  • 总结我们迄今为止学到的一切

  • 列出后续事项

迄今为止的旅程

迄今为止,我们已经走过了一段很长的旅程,现在是时候总结我们所做的和所学到的。

在第一章使用 Vue.js 去购物中,我们与 Vue.js 有了第一次约会。我们谈论了 Vue.js 是什么,它是如何创建的,它的作用是什么,并看了一些基本示例。

在第二章基础知识-安装和使用中,我们深入了解了 Vue.js 的幕后情况。我们了解了 MVVM 架构模式,看到了 Vue.js 的工作原理,并接触了 Vue.js 的不同方面,如组件指令插件和应用程序状态。我们学习了安装 Vue.js 的不同方式,从使用简单的独立编译脚本开始,通过使用 CDN 版本、NPM 版本,然后使用 Vue.js 的开发版本,不仅可以使用它,还可以为其代码库做出贡献。我们学会了如何调试以及如何使用Vue-cli搭建 Vue.js 应用程序。我们甚至使用了符合 CSP 标准的 Vue 的简单 Chrome 应用程序。

在第三章组件-理解和使用中,我们深入了解了组件系统。我们学习了如何定义 Vue 组件,组件作用域的工作原理,以及组件之间的关系,我们开始在之前引导的应用程序中使用单文件组件。

在第四章中,反应性-将数据绑定到您的应用程序,我们深入研究了 Vue.js 的数据绑定和反应性。我们学习了如何使用指令、表达式和过滤器。我们将数据绑定引入了最初章节中开发的应用程序,并且由于 Vue.js 的反应性方式,使它们变得交互式。

在第五章中,Vuex-管理您的应用程序中的状态,我们学习了如何使用 Vuex 存储系统在 Vue 应用程序中管理全局状态。我们学习了如何使用状态、操作、获取器和突变来创建一个模块化和良好的应用程序结构,其中组件可以轻松地相互通信。我们将这些新知识应用到了我们在前几章中开发的应用程序中。

在第六章中,插件-用自己的砖块建造你的房子,我们学习了 Vue 插件如何与 Vue 应用程序合作。我们使用了现有的插件vue-resource,它帮助我们在浏览器刷新之间保存应用程序的状态。我们还为 Vue 应用程序创建了自己的插件,用于生成白噪声、棕噪声和粉红噪声。在这一点上,我们拥有了功能齐全的应用程序,具有相当不错的一套工作功能。

在第七章中,测试-是时候测试我们到目前为止所做的了!,我们学习了如何测试我们的 Vue 应用程序。我们学习了如何编写单元测试,以及如何使用 Selenium 驱动程序创建和运行端到端测试。我们了解了代码覆盖率以及如何在单元测试中伪造服务器响应。我们几乎用单元测试覆盖了我们的代码的 100%,并且我们看到 Selenium 驱动程序在运行端到端测试时的效果。

在第八章,“部署-上线时间!”中,我们最终将我们的应用程序暴露给了整个世界。我们将它们部署到 Heroku 云系统,现在它们可以从互联网存在的任何地方访问。更重要的是,我们使我们的部署过程完全自动化。每当我们将代码更改推送到master分支时,应用程序就会被部署!甚至更多。它们不仅在每次推送时部署,而且还会自动使用 Travis 持续集成系统进行测试。

因此,在这本书中,我们不仅学习了一个新的框架。我们运用我们的知识从头开始开发了两个简单但不错的应用程序。我们应用了最重要的 Vue 概念,使我们的应用程序具有响应性、快速、可维护和可测试。然而,这并不是结束。在写作本书期间,Vue 2.0 已经发布。它带来了一些新的可能性和一些新的东西需要学习和使用。

Vue 2.0

Vue 2.0 于 2016 年 9 月 30 日发布。查看 Evan You 在medium.com/the-vue-point/vue-2-0-is-here-ef1f26acf4b8#.ifpgtjlek的帖子。

在整本书中,我们使用了最新版本;然而,每当有必要时,我都试图参考 Vue 第一代的做法。实际上,API 几乎是相同的;有一些轻微的变化,一些已弃用的属性,但提供给最终用户的整个界面几乎没有改变。

然而,它几乎是从头开始重写的!当然,有一些代码部分几乎 100%被重用,但总体上,这是一个重大的重构,一些概念完全改变了。例如,渲染层被完全重写。如果早些时候,渲染引擎使用的是真实的 DOM,现在它使用了轻量级的虚拟 DOM 结构(github.com/snabbdom/snabbdom)。它的性能超群!查看以下的基准图表:

Vue 2.0

性能基准(数值越低越好)取自 https://medium.com/the-vue-point/vue-2-0-is-here-ef1f26acf4b8#.fjxegtv98

在这个新版本中还有另一个有趣的地方。如果你已经使用过第一代 Vue,并阅读过它并听过播客,你可能知道 Vue 和 React 之间的一个主要区别是 React Native(这个框架允许我们基于 React 构建原生应用程序)。Evan You 一直声称 Vue 只是一个用于 web 界面的微小层。现在,我们有新兴的Weex,一个将受 Vue 启发的组件渲染成原生应用程序的框架(github.com/alibaba/weex)。根据 Evan You 的说法,很快,“受 Vue 启发”的将变成“由 Vue 驱动”的!敬请期待。请继续关注。我想推荐这个令人惊叹的 Full Stack Radio 播客,Evan You 在其中谈到了 Vue 的新版本:www.fullstackradio.com/50

Vue 自其作为一个副产品的谦卑开始以来已经发展了很多。今天它是由社区资助的,在现实世界中被广泛采用,并且根据 stats.js.org 的统计数据,它在所有 JavaScript 库中拥有最强劲的增长趋势之一。我们相信 2.0 版本将进一步推动它。这是自 Vue 诞生以来最大的更新,我们很期待看到你用它构建的东西。- Evan Youhttps://medium.com/the-vue-point/vue-2-0-is-here-ef1f26acf4b8#.fjxegtv98)

考虑到这一点,如果你来自 Vue 1.0 时代,升级你的应用程序将不会很困难。查看迁移指南,vuejs.org/guide/migration.html,安装迁移助手,github.com/vuejs/vue-migration-helper,应用所有必要的更改,然后看看你的应用程序在那之后的表现如何。

重新审视我们的应用程序

让我们再次检查我们到目前为止做了什么。我们已经使用 Vue.js 开发了两个应用程序。让我们重新审视它们。

购物清单应用程序

我们在本书章节中开发的购物清单应用程序是一个允许以下操作的 web 应用程序:

  • 创建不同的购物清单

  • 向购物清单添加新项目并在购买后进行检查

  • 重命名购物清单并删除它们

我们的购物清单应用程序驻留在 Heroku 云平台上:shopping-list-vue.herokuapp.com/

它的代码托管在 GitHub 上:github.com/chudaol/ShoppingList

它与 Travis 持续集成:travis-ci.org/chudaol/ShoppingList

它的界面简单易懂:

购物清单应用程序

使用 Vue.js 开发的购物清单应用程序的界面

它仍然远非你每次去购物都会使用的东西,不是吗?

番茄钟应用程序

我们在本书中开发的番茄钟应用程序是一个 Web 应用程序,它在工作的番茄钟期间实现了白噪音和间隔时间显示美丽的猫的计时器。它允许以下操作:

  • 启动、暂停和停止应用程序

  • 在工作时听白噪音,有助于集中注意力的噪音

  • 静音和取消静音白噪音声音

  • 在空闲时间盯着小猫

我们的番茄钟应用程序也托管在 Heroku 云平台上:catodoro.herokuapp.com/

它的代码也托管在 GitHub 上:github.com/chudaol/Pomodoro

它还是在每次推送时使用 Travis 持续集成平台进行构建和测试:travis-ci.org/chudaol/Pomodoro

它的界面清晰易用。以下是它在 20 分钟工作的番茄钟间隔时间显示的内容:

番茄钟应用程序

工作中的番茄钟应用程序

当 5 分钟休息时间到来时,会出现以下内容:

番茄钟应用程序

间隔时间的番茄钟应用程序

它实际上相当可用,但仍然远非完美。

为什么这只是个开始?

在前一节中,我们总结了本书中开发的应用程序的功能。我们也同意(希望)它们仍然远非完美。远非完美的东西是我们想要改进的东西,因此它们给我们带来挑战和目的。实际上还有很多工作要做。我们的应用程序很好,但它们缺乏功能、风格、身份、UX 模式、扩展到其他平台等等。让我们看看我们还能做什么。

为我们的应用程序添加功能

我们的应用程序已经具有一些非常好的功能,但它们可以拥有更多。它们可以更具配置性。它们可以更加灵活。它们可以更加友好的 UI/UX。让我们逐个查看它们,并列出可以添加的功能列表。这将是你的家庭作业。

购物清单应用

在浏览器中打开我们的购物清单应用程序并查看它。您可以向其中添加清单和项目。您可以删除项目和清单。但是每个打开应用程序的人都可以做同样的事情。这意味着我们必须为每个人提供自己的购物清单应用程序的方式,这只有通过身份验证机制才可能。

还有一些用户体验问题。如果我们可以内联更改购物清单的名称,为什么要在页脚的输入字段中更改它呢?实际上,当我们学习如何在 Vue 应用程序中实现数据绑定时,购物清单名称编辑在输入字段中是我们实现的第一件事情。所以,当时是有道理的,但现在它可以并且应该得到改进。

另一件事与已删除的项目有关。没有清除它们的方法。如果我们有一个很长的项目列表,即使我们删除它们,除非我们删除整个购物清单,否则它们将永远存在。应该有一种方法来清除清单上已选项目的方式。

我们可以应用的另一个美观变化与样式有关。不同的清单可能有不同的背景颜色,不同的字体颜色,甚至可能有不同的字体样式和大小。因此,以下是购物清单应用的改进列表:

  • 实现身份验证机制

  • 实现内联名称编辑

  • 实现清除已选项目

  • 实现配置不同购物清单样式的机制,如背景颜色、文字颜色、字体大小和样式

您还可以为项目实现类别,并为每个类别添加图标。作为灵感,您可以查看 Splitwise 应用程序www.splitwise.com/。当您开始添加项目时,项目的图标是通用的。一旦您输入了有意义的内容,图标就会更改,如下面的屏幕截图所示:

购物清单应用

Splitwise 应用程序的屏幕截图可以为图标类别提供灵感:它会根据您在输入字段中输入的内容进行调整

尝试为我们的购物清单应用程序实现这种分类。这将是一个非常好的和强大的奖励!

番茄钟应用程序

在浏览器中打开我们的番茄钟应用程序并尝试使用它。这很好,毫无疑问。它简单易用。但是,一些额外的配置可能会为其带来一些额外的功能。例如,为什么我要工作 20 分钟?也许我想要 15 分钟的工作番茄钟。或者我想要更长的工作番茄钟,比如 25 或 30 分钟。它肯定应该是可配置的。

让我们仔细检查维基百科上的番茄钟技术描述,看看我们是否漏掉了什么:en.wikipedia.org/wiki/Pomodoro_Technique

我很确定我们是。检查一下基本原则:

“四个番茄钟后,休息更长时间(15-30 分钟),将您的勾号计数重置为零,然后转到步骤 1。”
--https://en.wikipedia.org/wiki/Pomodoro_Technique

啊哈!四个番茄钟后应该发生一些事情。更长的间隔,更多时间盯着猫(或者做任何你想做的事情)。嗯,也许能够配置这段时间会很好!

还有一件重要的事情。和任何人一样,努力工作后,我想看到一些进展。如果我们的番茄钟应用程序能够显示一些关于我们能够集中精力和工作的时间的统计数据,这不是很好吗?为此,我们可以收集一些统计数据,并在我们的番茄钟计时器中显示它们。

另外,将这些统计数据存储起来并能够在一段时间内进行可视化,比如一周、一个月、一年,这会很好吧?这就导致我们需要实现一个存储机制。这个存储应该为每个用户存储统计数据,因此,也需要一个身份验证机制。

让我们想想我们美丽的白色、棕色和粉色噪音。目前,我们只播放在我们的App.vue中硬编码的棕色噪音:

<template>
 <div id="app" class="container" **v-noise="'brown'"**>
 </div>
</template> 

我们不应该能够在噪音之间切换并选择我们最喜欢的吗?因此,我们已经确定了要添加到应用程序配置中的另一项内容。现在就够了;让我们把这些都列在清单上:

  • 实现身份验证机制

  • 实现一个存储机制——它应该收集有关工作时间的统计数据,并将它们存储在某种持久层中

  • 实现统计数据显示机制——它应该获取存储的统计数据并以一种漂亮干净的方式显示出来(例如,图表)

  • 为番茄钟应用程序添加配置机制。这个配置应该允许以下操作:

  • 配置番茄钟工作时间

  • 配置休息间隔时间

  • 在可配置的工作番茄数量之后配置一个长的休息时间(默认为 4 个)

  • 配置工作间隔期间播放的首选噪音

正如你所看到的,你还有一些工作要做。好在你已经有一个可用的番茄钟计时器应用程序,可以在改进时使用!

美化我们的应用程序

目前两个应用程序都相当灰暗。只有番茄钟计时器应用程序在屏幕上出现猫时才会变得多彩一点。为它们添加一些设计会很好。让它们变得独特,赋予它们自己的特色;你为它们努力工作了这么久,显然它们值得一些漂亮的衣服。让我们想想我们可以用样式做些什么。

标志

从标志开始。一个好的标志定义了你的产品并使其独特。至少我可以帮你设计番茄钟应用程序的标志的想法。我有一个叫 Carina 的非常好的朋友为我设计了一个番茄,我尽力在上面加了一只小猫。看看吧。你可以直接使用它,或者只是作为发展你自己想法的参考。实际上,你的想象力没有极限!

标志

番茄钟应用程序的标志的想法

为购物清单应用程序想一个漂亮的标志。它可以是什么?一只装杂货的袋子?一个复选框?只是首字母——SL?同样,没有限制。我希望在存储库的分支中看到你漂亮的标志。等不及了!

标识和设计

我们的应用程序确实需要一些独特的设计。使用一些 UX 技术为它们开发一个漂亮的标识指南。考虑颜色、字体以及页面上元素应该如何组合,以便为我们的用户提供独特的用户友好体验。

动画和过渡

动画和过渡是为应用程序带来生机的强大机制。然而,它们不能被滥用。考虑它们何时何地是有意义的。例如,悬停在购物清单标题上可能会导致一些突出显示,购物清单项目在被选中时可以进行一些微小的弹跳,更改购物清单标题的过程也可以以某种方式突出显示,等等。番茄钟应用程序可以在每个状态转换时更改其背景颜色。它还可以意识到一天中的时间并相应地着色背景。机会数不胜数。发挥你的创造力,利用 Vue 的力量实现你的想法。

将我们的应用程序扩展到其他设备

我们的两个应用程序都是 Web 应用程序。对于番茄钟应用程序来说,如果我们整天都在电脑上工作并使用 Web,这可能没问题,但对于购物清单应用程序来说可能有点不舒服。你去购物时不会带着笔记本电脑。当然,你可以在家里填写购物清单,然后在超市打开手机浏览器,但这可能会很慢,使用起来也不太好。使用 Weex(github.com/alibaba/weex)将我们的 Web 应用程序带到移动设备上。这两个应用程序也可以扩展为 Google Chrome 应用程序,就像我们在第二章中学到的那样,基础知识-安装和使用。将你的工作扩展到每一个设备上。我期待着检查你的工作。

总结

这是本书的最后一章。老实说,我对此感到有点难过。我和你在一起的时间真的很愉快。我知道我不认识你,但我觉得我认识你。我和你交谈,有时我觉得你也在和我交谈。到目前为止开发的一切,我不能说都是我开发的;我觉得我们一直在一起工作。

实际上,这是一种非常有趣的感觉,因为当你阅读这本书时(对我来说,这是未来),我同时处于现在和未来。而你现在处于你的现在,同时又在过去和我交谈。我喜欢书籍和技术建立的联系方式,不仅在人与人之间建立联系,还在不同的时间间隔之间建立联系。这太神奇了。

我真的希望你能像我一样成为 Vue.js 的粉丝。

我真的希望你能至少改进我们迄今为止开发的一个应用程序,并向我展示。如果你需要帮助,我会很乐意帮助你。不要犹豫给我发邮件chudaol@gmail.com

谢谢你一直陪伴着我,希望很快能在下一本书中见到你!

第十章:练习解决方案

第一章的练习

在第一章的结尾,有以下练习:

注意

我们在前几章中构建的番茄钟毫无疑问非常棒,但仍然缺少一些不错的功能。它可以提供的一个非常好的功能是在休息时间显示来自thecatapi.com/的随机小猫。你能实现这个吗?当然可以!但请不要把休息时间和工作时间搞混了!我几乎可以肯定,如果你盯着小猫而不是工作,你的项目经理是不会喜欢的 😃

让我们解决这个问题。

查看 Pomodoro 的代码jsfiddle.net/chudaol/b6vmtzq1/

检查thecatapi.com/网站。

让我们首先添加一个带有指向猫 API 的图像的 Bootstrap 元素:

<div id="app" class="container">
  <...>
  **<div class="well">
    <img :src="’ http://thecatapi.com/api/images/get?**
 **type=gif&size=med’" />
  <div>**
</div>

如果你打开页面,你会发现图像总是可见的。这不是我们想要的,我们希望它只在我们的 Pomodoro 休息间隔时可见。你已经知道如何做了。有几种方法可以实现这一点;让我们使用类绑定方法,并在状态为工作时绑定一个隐藏的类:

<div class="well" **:class="{ 'hidden': pomodoroState === 'work' }**">
  <img :src="'http://thecatapi.com/api/
    images/get?type=gif&size=med'" />
</div>

现在,如果你打开页面,你会发现图像只在工作的 Pomodoro 完成后出现。

然而,问题在于我们休息的所有时间,图像都是一样的。如果我们每隔,比如,10 秒更新一次,那就太好了。

让我们为此目的使用缓存破坏机制。如果我们将一些属性附加到我们的 URL 并每隔 10 秒更改它,URL 将改变,因此我们将获得另一只随机的猫。让我们向我们的 Vue 应用程序添加一个timestamp变量,并在_tick函数内更改它:

<...>
new Vue({
  el: "#app",
  data: {
    <...>
    **timestamp: 0**
  },
  <...>
  methods: {
    <...>
    _tick: function () {
      //update timestamp that is used in image src
      **if (this.second % 10 === 0) {
        this.timestamp = new Date().getTime();
      }**
      <...>
    }
  }
});

时间戳创建和更新后,我们可以在图像源 URL 中使用它:

<div class="well" :class="{ 'hidden': pomodoroState === 'work' }">
  <img :src="**'http://thecatapi.com/api/images/get?
    type=gif&size=med&ts=' + timestamp"** />
</div>

就是这样!在这个 JSFiddle 中检查整个代码jsfiddle.net/chudaol/4hnbt0pd/2/

第二章的练习

增强 MathPlugin

用三角函数(正弦、余弦和正切)增强我们的MathPlugin

实际上,这只是添加缺失的指令并在其中使用Math对象的函数。打开VueMathPlugin.js并添加以下内容:

//VueMathPlugin.js
export default {
  install: function (Vue) {
    Vue.directive('square', function (el, binding) {
      el.innerHTML = Math.pow(binding.value, 2);
    });
    Vue.directive('sqrt', function (el, binding) {
      el.innerHTML = Math.sqrt(binding.value);
    });
    **Vue.directive('sin', function (el, binding) {
      el.innerHTML = Math.sin(binding.value);
    });
    Vue.directive('cos', function (el, binding) {
      el.innerHTML = Math.cos(binding.value);
    });
    Vue.directive('tan', function (el, binding) {
      el.innerHTML = Math.tan(binding.value);
    });**
  }
};

你可以在 HTML 文件中检查这个指令是如何工作的:

//index.html 
<div id="app">
  <input v-model="item"/>
  <hr>
  <div><strong>Square:</strong> <span v-square="item"></span></div>
  <div><strong>Root:</strong> <span v-sqrt="item"></span></div> **<div><strong>Sine:</strong> <span v-sin="item"></span></div>
  <div><strong>Cosine:</strong> <span v-cos="item"></span></div>
  <div><strong>Tangent:</strong> <span v-tan="item"></span></div>**
</div>

就是这样!

创建 Pomodoro 计时器的 Chrome 应用程序

请结合使用符合 SCP 标准的 Vue.js 版本和我们在第一章中创建的简单番茄钟应用程序的解决方案。检查chrome-app-pomodoro文件夹中的代码。

第三章练习

练习 1

当我们使用简单组件重写购物清单应用程序时,我们失去了应用程序的功能。这个练习建议使用事件发射系统来恢复功能。

在本节中,我们最终得到的代码看起来与chapter3/vue-shopping-list-simple-components文件夹中的代码类似。

为什么它不起作用?检查开发工具的错误控制台。它显示如下内容:

**[Vue warn]: Property or method "addItem" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option.**
**(found in component <add-item-component>)**

啊哈!这是因为在add-item-template中,我们调用了不属于这个组件的addItem方法。这个方法属于父组件,当然,子组件没有访问权限。我们该怎么办?让我们发出事件!我们已经知道如何做了。所以,我们不需要做太多事情。实际上,我们只需要做三件小事:

  • addItem方法附加到add-item-component中,我们将在其中发出一个事件,并将这个组件的newItem属性传递给它。

  • 修改/简化父组件的addItem方法。现在它只需要接收一个文本并将其添加到其items属性中。

  • 在主标记中,使用v-on修饰符和事件的名称将组件的调用绑定到addItem方法,每次事件被发出时都会调用它。

让我们首先将addItem方法添加到add-item-component中。每次点击添加按钮或按下Enter键时都会调用它。这个方法应该检查newItem属性,如果包含文本,就应该发出一个事件。让我们把这个事件叫做add。因此,我们组件的 JavaScript 代码现在应该如下所示:

//add item component
Vue.component('add-item-component', {
  template: '#add-item-template',
  data: function () {
    return {
      newItem: ''
    }
  },
  **methods: {
    addItem: function () {
      var text;

      text = this.newItem.trim();
      if (text) {
        this.$emit('add', this.newItem);
        this.newItem = '';
      }
    }
  }**
});

当发出add事件时,一定要以某种方式调用主组件的addItem方法。让我们通过在add-item-component的调用中附加v-on:add修饰符来将add事件绑定到addItem

<add-item-component **v-on:add="addItem"** :items="items">
</add-item-component>

好吧。正如你所看到的,这种方法几乎与主组件的addItem方法之前所做的事情相同。它只是不将newItem推送到items数组中。让我们修改主组件的addItem方法,使其只接收已处理的文本并将其推入物品数组中:

new Vue({
  el: '#app',
  data: data,
  methods: { **addItem: function (text) {
      this.items.push({
        text: text,
        checked: false
      });
    }** }
});

我们完成了!这个练习的完整解决方案可以在附录/第三章/vue-shopping-list-simple-components文件夹中找到。

练习 2

在第三章的使用单文件组件重写购物清单应用程序部分,组件-理解和使用,我们很好地改变了使用单文件组件的购物清单应用程序,但还有一些事情没有完成。我们有两个缺失的功能:向物品列表添加物品和更改标题。

为了实现第一个功能,我们必须从AddItemComponent中发出一个事件,并在主App.vue组件中的add-item-component调用上附加v-on修饰符,就像我们在处理简单组件的情况下所做的那样。你基本上只需复制并粘贴代码。

更改标题功能也是如此。我们还应该发出一个input事件,就像我们在简单组件示例中所做的那样。

不要忘记向App.vue组件添加样式,使其看起来与以前一样。

附录/第三章/shopping-list-single-file-components文件夹中检查完整的代码。

总结

在本章中,您学会了如何使我们的应用程序对每个人都可用。您还学会了如何使用 Heroku 与 GitHub 存储库集成部署它们。您还学会了如何在每次提交和推送时自动执行此操作。我们还使用 Travis 在每次部署时进行自动构建。现在我们的应用程序正在进行全面测试,并在每次提交更改时自动重新部署。恭喜!

你可能认为这是旅程的终点。不,不是。这只是开始。在下一章中,我们将看到您可以学到什么,以及您可以用 Pomodoro 和购物清单应用程序做些什么好事。和我在一起!

posted @ 2024-05-16 12:10  绝不原创的飞龙  阅读(6)  评论(0编辑  收藏  举报