Vue2-设计模式最佳实践-二-

54 阅读30分钟

Vue2 设计模式最佳实践(二)

原文:zh.annas-archive.org/md5/6E739FB94554764B9B3B763043E30DA8

译者:飞龙

协议:CC BY-NC-SA 4.0

第五章:与 Vue.js 组件进行安全通信

在现代 Web 应用程序中,注意到组件驱动的架构并不需要花费太多精力。在短时间内,开发需求发生了变化,Web 从一个简单的文档查看器发展为承载具有显着庞大代码库的复杂应用程序。因此,能够创建可重用的组件使我们作为前端开发人员的生活变得更加轻松,因为我们可以将核心功能封装到单一块中,减少总体复杂性,实现更好的关注点分离,协作和可扩展性。

在本章中,我们将把前面的概念应用到我们的 Vue 应用程序中。在本章结束时,您将实现:

  • 创建自己的 Vue 组件的能力

  • 对单文件组件的更深入理解

  • 创建特定于每个组件的样式的能力

  • 能够在本地和全局注册组件,并理解选择其中一个的原因

  • 使用 props 在父子组件之间进行通信的能力

  • 使用全局事件总线在整个应用程序中进行通信的能力

  • 使用插槽使您的组件更加灵活

让我们从您的第一个 Vue 组件开始。

您的第一个 Vue 组件

事实证明,我们一直在 Vue 应用程序中使用组件!使用webpack-simple模板,我们支持单文件组件SFC),它本质上只是一个带有.vue扩展名的模板、脚本和样式标签:

# Create a new Vue project
$ vue init webpack-simple vue-component-1

# Navigate to directory
$ cd vue-component-1

# Install dependencies
$ npm install

# Run application
$ npm run dev

由于我们正在使用 Visual Studio Code 的 Vetur 扩展,我们可以输入scaffold并按Tab键,然后创建一个可以在项目中使用的 SFC。如果我们用一个空组件覆盖App.vue,根据我们当前的定义,它将如下所示:

就是这样!有点。我们仍然需要向我们的组件添加一些功能,并且如果我们要创建一个新文件(即不使用默认的App.vue组件),则需要在某个地方注册它以供使用。让我们通过在src/components/FancyButton.vue下创建一个新文件来看看这个过程:

<template>
 <button>
  {{buttonText}}
 </button>
</template>

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

<style>
 button {
  border: 1px solid black;
  padding: 10px;
 }
</style>

我们的FancyButton组件只是一个说'Hello World!'的按钮,并带有一点点样式。立即,我们需要考虑可以做些什么来使其更具可扩展性:

  • 允许在此组件上输入以更改按钮文本

  • 当我们为button元素设置样式(甚至如果我们添加了类),我们需要一种方法来阻止样式泄漏到应用程序的其他部分

  • 注册此组件,以便可以在整个应用程序中全局使用

  • 注册此组件,以便可以在组件内部本地使用

  • 还有更多!

让我们从最简单的开始,注册组件,以便在我们的应用程序中使用。

全局注册组件

我们可以使用以下接口创建组件并全局注册它们:Vue.component(name: string, options: Object<VueInstance>)。虽然不是必需的,但在命名我们的组件时,遵循 W3C 自定义元素规范设置的命名约定很重要(www.w3.org/TR/custom-elements/#valid-custom-element-name),即全部小写并且必须包含连字符。

在我们的main.js文件中,让我们首先从适当的路径导入FancyButton组件,然后注册它:

import FancyButton from './components/FancyButton.vue';

之后,我们可以使用Vue.component注册组件,可以在main.js中看到加粗的结果代码如下:

import Vue from 'vue';
import App from './App.vue';
import FancyButton from './components/FancyButton.vue';

Vue.component('fancy-button', FancyButton);

new Vue({
  el: '#app',
  render: h => h(App)
});

塔达!我们的组件现在已经全局注册了。现在...我们如何在App.vue组件内部使用它呢?好吧,记得我们指定的标签吗?我们只需将其添加到template中,如下所示:

<template>
 <fancy-button/>
</template>

这是我们辛苦工作的结果(放大到 500%):

作用域样式

太棒了!如果我们添加另一个按钮元素会发生什么?因为我们直接用 CSS 为button元素设置了样式:

<template>
  <div>
    <fancy-button></fancy-button>
    <button>I'm another button!</button>
  </div>
</template>

如果我们转到浏览器,我们可以看到我们创建的每个按钮:

哦哦!这个其他按钮不是fancy-button,那么为什么它会得到样式?幸运的是,阻止样式泄漏到组件外部很简单,我们只需要在style标签中添加scoped属性:

<style scoped>
 button {
 border: 1px solid black;
 padding: 10px;
 }
</style>

scoped属性不是 Vue 默认的一部分,这来自我们的 Webpack vue-loader。您会注意到,在添加此属性后,按钮样式仅适用于我们的fancy-button组件。如果我们看一下以下截图中两个按钮之间的区别,我们可以看到一个只是一个按钮,另一个是使用随机生成的数据属性为按钮设置样式。这可以阻止浏览器在这种情况下将样式应用于两个按钮元素。

在 Vue 中使用作用域 CSS 时,请记住组件内创建的规则不会在整个应用程序中全局访问:

在本地注册组件

我们也可以在应用程序内部局部注册我们的组件。这可以通过将其添加到我们的 Vue 实例中来实现,例如,让我们将main.js中的全局注册注释掉,然后导航到App.vue

// Vue.component('fancy-button', FancyButton);

在将任何代码添加到我们的应用程序组件之前,请注意,我们的按钮现在已经消失,因为我们不再全局注册它。要在本地注册这个,我们需要首先导入组件,类似于之前的操作,然后将其添加到实例中的component对象中:

<template>
 <div>
 <fancy-button></fancy-button>
 <button>I'm another button!</button>
 </div>
</template>

<script>
import FancyButton from './components/FancyButton.vue';

export default {
 components: {
 FancyButton
 }
}
</script>

<style>

</style>

我们的按钮现在再次出现在屏幕上。在决定注册组件的位置时,考虑它们在整个项目中可能需要被多频繁使用。

组件通信

现在我们有了创建可重用组件的能力,这使我们能够在项目中封装功能。为了使这些组件可用,我们需要让它们能够相互通信。我们首先要看的是组件属性的单向通信(称为“props”)。

组件通信的目的是保持我们的功能分布、松散耦合,并从而使我们的应用程序更容易扩展。为了实现松散耦合,您不应尝试在子组件中引用父组件的数据,而应仅使用props传递。让我们看看如何在我们的FancyButton上创建一个改变button文本的属性:

<template>
 <button>
  {{buttonText}}
 </button>
</template>

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

<style scoped>
 button {
 border: 1px solid black;
 padding: 10px;
 }
</style>

请注意,我们能够在模板中绑定到buttonText值,因为我们创建了一个包含每个组件属性的字符串或对象值的props数组。设置这个可以通过连字符形式作为组件本身的属性,这是必需的,因为 HTML 是不区分大小写的:

<template>
 <fancy-button button-text="I'm set using props!"></fancy-button>
</template>

这给我们带来了以下结果:

配置属性值

我们还可以通过将属性值设置为对象来进一步配置它们。这使我们能够定义默认值、类型、验证器等。让我们用我们的buttonText属性来做这个:

export default {
 props: {
  buttonText: {
   type: String,
   default: "Fancy Button!",
   required: true,
   validator: value => value.length > 3
  }
 },
}

首先,我们确保只能将 String 类型传递到此属性中。我们还可以检查其他类型,例如:

  • 数组

  • 布尔值

  • 函数

  • 数字

  • 对象

  • 字符串

  • 符号

根据 Web 组件的良好实践,向 props 发送原始值是一种良好的实践。

在底层,这是针对属性运行instanceof运算符,因此它也可以针对构造函数类型运行检查,如下面的屏幕截图所示:

与此同时,我们还可以使用数组语法检查多种类型:

export default {
 props: {
  buttonText: {
   type: [String, Number, Cat],
  }
 },
}

接下来,我们将默认文本设置为FancyButton!,这意味着默认情况下,如果未设置该属性,它将具有该值。我们还将required设置为true,这意味着每次创建FancyButton时,都必须包含buttonText属性。

目前这是一个术语上的矛盾(即默认值和必需性),但有时您可能希望在属性不是必需的情况下设置默认值。最后,我们将为此添加一个验证函数,以指定每次设置此属性时,它的字符串长度必须大于三。

我们如何知道属性验证失败了?在开发模式下,我们可以检查开发控制台,应该会有相应的错误。例如,如果我们忘记在组件上添加buttonText属性:

自定义事件

我们取得了很大的进展。我们现在有一个可以接受输入、可以全局或局部注册、具有作用域样式、验证等功能的组件。现在我们需要让它具有向其父组件发送事件的能力,以便在FancyButton按钮被点击时进行通信,这是通过编辑$emit事件的代码来实现的:

<template>
 <button 
  @click.prevent="clicked">
  {{buttonText}}
 </button>
</template>

<script>
export default {
 props: {
  buttonText: {
   type: String,
   default: () => {
     return "Fancy Button!" 
   },
   required: true,
   validator: value => value.length > 3
  }
 },
 methods: {
  clicked() {
   this.$emit('buttonClicked');
  }
 }
}
</script>

在我们的示例中,我们将clicked函数附加到按钮的点击事件上,这意味着每当它被选中时,我们就会发出buttonClicked事件。然后我们可以在App.vue文件中监听此事件,将我们的元素添加到 DOM 中:

<template>
  <fancy-button 
   @buttonClicked="eventListener()" 
   button-text="Click 
   me!">
  </fancy-button>
</template>

<script>
import FancyButton from './components/FancyButton.vue';

export default {
  components: {
    'fancy-button': FancyButton
  },
  methods: {
    eventListener() {
      console.log("The button was clicked from the child component!");
    }
  }
}
</script>

<style>

</style>

请注意,此时我们正在使用@buttonClicked="eventListener()"。这使用v-on事件在事件被触发时调用eventListener()函数,随后将消息记录到控制台。我们现在已经演示了在两个组件之间发送和接收事件的能力。

发送事件值

为了使事件系统更加强大,我们还可以将值传递给我们的另一个组件。让我们在FancyButton组件中添加一个输入框(也许我们需要重新命名它或考虑将输入分离成自己的组件!):

<template>
 <div>
  <input type="text" v-model="message">
  <button 
  @click.prevent="clicked()">
   {{buttonText}}
  </button>
 </div>
</template>

<script>
export default {
 data() {
  return {
   message: ''
  };
 },
 // Omitted
}

接下来要做的是在我们的$emit调用中传递消息值。我们可以在clicked方法中这样做:

 methods: {
  clicked() {
   this.$emit('buttonClicked', this.message);
  }
 }

此时,我们可以将事件作为eventListener函数的参数来捕获:

<template>
 <fancy-button @buttonClicked="eventListener($event)" button-text="Click me!"></fancy-button>
</template>

此时要做的最后一件事也是匹配函数的预期参数:

 eventListener(message) {
  console.log(`The button was clicked from the child component with this message: ${message}`);
 }

然后我们应该在控制台中看到以下内容:

我们现在有能力在父子组件之间真正发送事件,以及我们可能想要发送的任何数据。

事件总线

当我们想要创建一个应用程序范围的事件系统(即,不仅限于父子组件),我们可以创建所谓的事件总线。这允许我们通过一个单一的 Vue 实例“管道”所有事件,从而实现超出父子组件通信的可能。除此之外,对于那些不想使用第三方库如Vuex,或者处理不多动作的小型项目来说,这也是有用的。让我们创建一个新的示例项目来演示它:

# Create a new Vue project
$ vue init webpack-simple vue-event-bus

# Navigate to directory
$ cd vue-event-bus

# Install dependencies
$ npm install

# Run application
$ npm run dev

首先,在src文件夹中创建一个EventsBus.js。从这里,我们可以导出一个新的 Vue 实例,我们可以像以前一样使用$emit来发出事件:

import Vue from 'vue';

export default new Vue();

接下来,我们可以创建两个组件,ShoppingInputShoppingList。这将允许我们输入新项目,并在购物清单上显示输入项目的列表,从我们的ShoppingInput组件开始:

<template>
 <div>
  <input v-model="itemName">
  <button @click="addShoppingItem()">Add Shopping Item</button>
 </div>
</template>

<script>
import EventBus from '../EventBus';

export default {
 data() {
  return {
   itemName: ''
  }
 },
 methods: {
  addShoppingItem() {
   if(this.itemName.length > 0) {
    EventBus.$emit('addShoppingItem', this.itemName)
    this.itemName = "";
   }
  }
 },
}
</script>

这个组件的关键是,我们现在导入EventBus并使用$emit,而不是使用this,将我们的应用程序事件系统从基于组件变为基于应用程序。然后,我们可以使用$on来监视任何组件中的更改(以及随后的值)。让我们用下一个组件ShoppingList来看一下:

<template>
 <div>
  <ul>
   <li v-for="item in shoppingList" :key="item">
    {{item}}
   </li>
  </ul>
 </div>
</template>

<script>
import EventBus from '../EventBus';
export default {
 props: ['shoppingList'],
 created() {
  EventBus.$on('addShoppingItem', (item) => {
   console.log(`There was an item added! ${item}`);
  })
 }
}
</script>

看看我们的ShoppingList组件,我们可以看到$on的使用,这允许我们监听名为addShoppingItem的事件(与我们发出的相同事件名称,或者您想要监听的任何其他事件)。这将返回该项,然后我们可以将其记录到控制台或在此时执行任何其他操作。

我们可以将所有这些放在我们的App.vue中:

<template>
 <div>
  <shopping-input/>
  <shopping-list :shoppingList="shoppingList"/>
 </div>
</template>

<script>
import ShoppingInput from './components/ShoppingInput';
import ShoppingList from './components/ShoppingList';
import EventBus from './EventBus';

export default {
 components: {
  ShoppingInput,
  ShoppingList
 },
 data() {
  return {
   shoppingList: []
  }
 },
 created() {
  EventBus.$on('addShoppingItem', (itemName) => {
   this.shoppingList.push(itemName);
  })
 },
}

我们定义了两个组件,并在创建的生命周期钩子内监听addShoppingItem事件。就像以前一样,我们得到了itemName,然后我们可以将其添加到我们的数组中。我们可以将数组传递给另一个组件作为 prop,比如ShoppingList,以在屏幕上呈现。

最后,如果我们想要停止监听事件(完全或每个事件),我们可以使用$off。在App.vue内,让我们创建一个新的按钮来进一步展示这一点:

<button @click="stopListening()">Stop listening</button>

然后我们可以这样创建stopListening方法:

methods: {
 stopListening() {
  EventBus.$off('addShoppingItem')
 }
},

如果我们想要停止监听所有事件,我们可以简单地使用:

EventBus.$off();

到目前为止,我们已经创建了一个事件系统,可以让我们与任何组件进行通信,而不受父/子关系的影响。我们可以通过EventBus发送事件并监听事件,从而更灵活地处理组件数据。

插槽

当我们组合组件时,我们应该考虑它们将如何被我们自己和团队使用。使用插槽允许我们动态地向组件添加具有不同行为的元素。让我们通过创建一个新的示例项目来看看它的作用:

# Create a new Vue project
$ vue init webpack-simple vue-slots

# Navigate to directory
$ cd vue-slots

# Install dependencies
$ npm install

# Run application
$ npm run dev

然后,我们可以继续创建一个名为Messagesrc/components/Message.vue)的新组件。我们可以为这个组件添加一些特定的内容(比如下面的h1),以及一个slot标签,我们可以用它来从其他地方注入内容:

<template>
 <div>
   <h1>I'm part of the Message component!</h1>
   <slot></slot>
 </div>
</template>

<script>
export default {}
</script>

如果我们在App.vue内注册了我们的组件,并将其放置在我们的模板内,我们就可以像这样在component标签内添加内容:

<template>
 <div id="app">
   <message>
     <h2>What are you doing today?</h2>
   </message>
   <message>
     <h2>Learning about Slots in Vue.</h2>
   </message>
 </div>
</template>

<script>
import Message from './components/Message';

export default {
 components: {
  Message
 }
}
</script>

此时,message标签内的所有内容都被放置在Message组件内的slot中:

注意,每次声明Message组件时,我们都会看到"I'm part of the Message component!",这表明即使我们向这个空间注入内容,我们仍然可以每次显示特定于组件的模板信息。

默认值

虽然我们可以向插槽中添加内容,但我们可能希望添加默认内容,以便在我们没有自己添加任何内容时显示。这意味着我们不必每次都添加内容,如果需要的话,我们可以在特定情况下覆盖它。

我们如何向我们的插槽添加默认行为?这很简单!我们只需要在slot标签之间添加我们的元素,就像这样:

<template>
 <div>
  <h1>I'm part of the Message component!</h1>
  <slot>
   <h2>I'm a default heading that appears <em>only</em> when no slots 
   have been passed into this component</h2>
   </slot>
 </div>
</template>

因此,如果我们添加另一个message元素,但这次没有任何标记,我们会得到以下结果:

<template>
 <div id="app">
  <message>
   <h2>What are you doing today?</h2>
  </message>
  <message>
   <h2>Learning about Slots in Vue.</h2>
  </message>
  <message></message>
 </div>
</template>

现在,如果我们转到浏览器,我们可以看到我们的消息如预期般显示:

命名插槽

我们还可以通过命名插槽进一步进行。假设我们的message组件希望同时有datemessageText输入,其中一个是插槽,另一个是组件的属性。我们使用这个的情况可能是,也许我们想以不同的方式显示日期,添加不同的信息,或者根本不显示它。

我们的消息组件变成了:

<template>
 <div>
  <slot name="date"></slot>
  <h1>{{messageText}}</h1>
 </div>
</template>

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

请注意我们在slot标签上的name="date"属性。这使我们能够在运行时动态地将我们的内容放在正确的位置。然后我们可以构建一个小型的聊天系统来展示这一点,让我们确保在继续之前在我们的项目中安装了moment

$ npm install moment --save

你可能还记得在第四章中使用momentVue.js 指令,我们还将重用之前创建的Date管道。让我们升级我们的App.vue,包含以下内容:

<template>
 <div id="app">

  <input type="text" v-model="message">
  <button @click="sendMessage()">+</button>

  <message v-for="message in messageList" :message-text="message.text" :key="message">
   <h2 slot="date">{{ message.date | date }}</h2>
  </message>
 </div>
</template>

<script>
import moment from 'moment';
import Message from './components/Message';

const convertDateToString = value => moment(String(value)).format('MM/DD/YYYY');

export default {
 data() {
  return {
   message: '',
   messageList: []
  }
 },
 methods: {
  sendMessage() {
   if ( this.message.length > 0 ) {
    this.messageList.push({ date: new Date(), text: this.message });
    this.message = ""
   }
  }
 },
 components: {
  Message
 },
 filters: {
  date: convertDateToString
 }
}
</script>

这里发生了什么?在我们的模板中,我们正在遍历我们的messageList,每次添加新消息时都会创建一个新的消息组件。在组件标签内部,我们期望messageText会出现(因为我们将其作为 prop 传递,并且标记是在 Message 组件内部定义的),但我们还动态添加了日期使用slot

如果我们从 h2 中删除slot="date"会发生什么?日期还会显示吗?不会。这是因为当我们只使用命名插槽时,没有其他地方可以添加插槽。只有当我们将我们的Message组件更改为接受一个未命名插槽时,它才会出现,如下所示:

<template>
 <div>
  <slot name="date"></slot>
  <slot></slot>
  <h1>{{messageText}}</h1>
 </div>
</template>

总结

本章使我们有能力创建可重用的组件,这些组件可以相互通信。我们已经看到了如何可以在整个项目中全局注册组件,或者在特定实例中本地注册组件,从而给我们带来了灵活性和适当的关注点分离。我们已经看到了这种强大的功能,从简单属性的添加到复杂验证和默认值的例子。

在下一章中,我们将研究如何创建更好的 UI。我们将更多地关注指令,比如在表单、动画和验证的上下文中使用v-model

第六章:创建更好的 UI

过渡和动画是在我们的应用程序中创建更好用户体验的好方法。由于有很多不同的选项和用例,它们可以使应用程序的感觉得以或败。我们将在本章中进一步探讨这个概念。

我们还将使用名为Vuelidate的第三方库来进行表单验证。这将允许我们创建随着应用程序规模而扩展的表单。我们还将获得根据表单状态更改 UI 的能力,以及显示有用的验证消息来帮助用户。

最后,我们将看看如何使用render函数和 JSX 来使用 Vue 组合用户界面。虽然这并不适用于每种情况,但在某些情况下,您可能希望充分利用模板中的 JavaScript,并使用功能组件模型创建智能/表现组件。

到本章结束时,您将拥有:

  • 学习了 CSS 动画

  • 创建自己的 CSS 动画

  • 使用Animate.css创建交互式 UI,工作量很小

  • 调查并创建自己的 Vue 过渡

  • 利用Vuelidate在 Vue 中验证表单

  • 使用render函数作为模板驱动 UI 的替代方案

  • 使用 JSX 来组合类似于 React 的 UI

让我们首先了解为什么我们应该关心项目中的动画和过渡。

动画

动画可以用来吸引特定 UI 元素的注意,并通过使其生动起来来改善用户的整体体验。当没有明确的开始状态和结束状态时,应该使用动画。动画可以自动播放,也可以由用户交互触发。

CSS 动画

CSS 动画不仅是强大的工具,而且在项目中使用它们只需要很少的知识就可以轻松维护。

将它们添加到界面中可以是捕获用户注意力的直观方法,它们也可以用于轻松指向用户特定的元素。动画可以定制和自定义,使它们成为各种项目中许多用例的理想选择。

在深入研究 Vue 过渡和其他动画可能性之前,我们应该了解如何进行基本的 CSS3 动画。让我们创建一个更详细地查看这一点的简单项目:

# Create a new Vue project
$ vue init webpack-simple vue-css-animations

# Navigate to directory
$ cd vue-css-animations

# Install dependencies
$ npm install

# Run application
$ npm run dev

App.vue中,我们可以首先创建以下样式:

<style>
button {
 background-color: transparent;
 padding: 5px;
 border: 1px solid black;
}

h1 {
 opacity: 0;
}

@keyframes fade {
 from { opacity: 0; }
 to { opacity: 1; }
}

.animated {
 animation: fade 1s;
 opacity: 1;
}
</style>

如您所见,没有什么特别的。我们使用@keyframes命名为fade来声明 CSS 动画,基本上给 CSS 两个我们希望元素处于的状态-opacity: 1opacity: 0。它并没有说明这些关键帧持续多长时间或是否重复;这一切都在animated类中完成。我们在将类添加到元素时应用fade关键帧为1;与此同时,我们添加opacity: 1以确保在动画结束后它不会消失。

我们可以通过利用v-bind:class根据toggle的值动态添加/删除类来组合这些:

<template>
 <div id="app">
  <h1 v-bind:class="{ animated: toggle }">I fade in!</h1>
  <button @click="toggle = !toggle">Toggle Heading</button>
 </div> 
</template>

<script>
export default {
 data () {
  return {
   toggle: false
  }
 }
}
</script>

很好。现在我们可以根据Boolean值淡入一个标题。但如果我们能做得更好呢?在这种特殊情况下,我们可以使用过渡来实现类似的结果。在更详细地查看过渡之前,让我们看看我们可以在项目中使用 CSS 动画的其他方式。

Animate.css

Animate.css是一种很好的方式,可以轻松地将不同类型的动画实现到项目中。这是由 Daniel Eden 创建的开源 CSS 库(daneden.me/),它为我们提供了"即插即用"的 CSS 动画。

在将其添加到任何项目之前,前往daneden.github.io/animate.css/预览不同的动画样式。有许多不同的动画可供选择,每种都提供不同的默认动画。这些可以进一步定制,我们稍后将在本节中详细讨论。

继续运行以下命令在我们的终端中创建一个游乐项目:

 Create a new Vue project
$ vue init webpack-simple vue-animate-css

# Navigate to directory
$ cd vue-animate-css

# Install dependencies
$ npm install

# Run application
$ npm run dev

设置项目后,继续在所选的编辑器中打开index.html文件。在<head>标签内,添加以下样式表:

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.5.2/animate.min.css">

这是项目中需要的样式表引用,以使Animate.css在项目中起作用。

使用 Animate.css

现在我们在项目中有了Animate.css,我们可以将App.vue更改为具有以下template

<template>
 <h1 class="animated fadeIn">Hello Vue!</h1>
</template>

在添加任何动画之前,我们首先需要添加 animated 类。接下来,我们可以从Animate.css库中选择任何动画;我们选择了fadeIn作为示例。然后可以将其替换为其他动画,如bounceInLeftshakerubberBand等等!

我们可以将之前的示例转换为基于布尔值的绑定类值,但过渡可能更有趣。

过渡

过渡效果是通过从一个特定状态开始,然后过渡到另一个状态并在中间插值数值来实现的。过渡不能在动画中涉及多个步骤。想象一对窗帘从打开到关闭:第一个状态将是打开的位置,而第二个状态将是关闭的位置。

Vue 有自己的标签来处理过渡,称为<transition><transition-group>。这些标签是可定制的,可以很容易地与 JavaScript 和 CSS 一起使用。实际上,并不一定需要有transition标签来使过渡生效,因为你只需将状态变量绑定到可见属性,但标签通常提供更多控制和潜在更好的结果。

让我们来看看之前的toggle示例,并创建一个使用transition的版本:

<template>
 <div id="app">
  <transition name="fadeIn"
  enter-active-class="animated fadeIn"
  leave-active-class="animated fadeOut">
   <h1 v-if="toggle">I fade in and out!</h1>
  </transition>
  <button @click="toggle = !toggle">Toggle Heading</button>
 </div> 
</template>

<script>
export default {
 data () {
  return {
   toggle: false
  }
 }
}
</script>

让我们更详细地看看各个部分的运作方式。

我们将元素包裹在<transition>标签中,当<h1>进入 DOM 时,它会应用animated fadeInenter-active-class。这是通过v-if指令触发的,因为toggle变量最初设置为false。单击按钮会切换我们的布尔值,触发过渡并应用适当的 CSS 类。

过渡状态

每个进入/离开过渡都会应用最多六个类,这些类由进入场景时的过渡、过程中和离开场景时的过渡组成。第一组(v-enter-*)指的是最初进入然后移出的过渡,而第二组(v-leave-*)指的是结束过渡最初进入然后移出:

名称描述
v-enter这是进入的起始状态。在元素插入后的一帧后被移除。
v-enter-activeenter-activeenter的活动状态。它在整个活动阶段都是活动的,并且只有在过渡或动画结束后才会被移除。该状态还管理进一步的指令,如延迟、持续时间等。
v-enter-to这是进入的最后状态,在元素插入后的一帧后添加,与v-enter被移除的时间相同。一旦过渡/动画结束,enter-to就会被移除。
v-leave这是离开的起始状态。一旦离开过渡被触发,就会在一帧后被移除。
v-leave-activeleave-activeleave的活动状态。在整个离开阶段都是活动的,只有在过渡或动画结束时才会被移除。
v-leave-to离开的最后状态,在离开触发后的一帧后添加,与v-leave同时移除。当过渡/动画结束时,leave-to也会被移除。

每个enterleave过渡都有一个前缀,在表中显示为v的默认值,因为过渡本身没有名称。当将 enter 或 leave 过渡添加到项目中时,理想情况下应该应用适当的命名约定,以充当唯一标识符。如果您计划在项目中使用多个过渡,这可以帮助,并且可以通过简单的赋值操作完成:

<transition name="my-transition">

表单验证

在本书中,我们已经看过了各种不同的捕获用户输入的方式,比如v-model。我们将使用一个名为Vuelidate的第三方库来根据特定规则进行模型验证。让我们通过在终端中运行以下命令来创建一个示例项目:

# Create a new Vue project
$ vue init webpack-simple vue-validation

# Navigate to directory
$ cd vue-validation

# Install dependencies
$ npm install

# Install Vuelidate
$ npm install vuelidate

# Run application
$ npm run dev

什么是 Vuelidate?

Vuelidate是一个开源的轻量级库,帮助我们使用各种验证上下文进行模型验证。验证可以被功能组合,并且它也可以很好地与其他库(如MomentVuex等)配合使用。由于我们已经在项目中使用npm install vuelidate安装了它,现在我们需要在main.js中将其注册为插件。

import Vue from 'vue';
import Vuelidate from 'vuelidate';
import App from './App.vue';

Vue.use(Vuelidate);

new Vue({
  el: '#app',
  validations: {},
  render: h => h(App),
});

将空验证对象添加到我们的主 Vue 实例中,可以在整个项目中引导 Vuelidate 的$v。这样我们就可以使用$v对象来获取关于表单当前状态的信息,跨越所有组件的 Vue 实例。

使用 Vuelidate

让我们创建一个基本表单,允许我们输入firstNamelastNameemailpassword。这将允许我们使用Vuelidate添加验证规则,并在屏幕上可视化它们:

<template>
  <div>
    <form class="form" @submit.prevent="onSubmit">
      <div class="input">
        <label for="email">Email</label>
        <input 
        type="email" 
        id="email" 
        v-model.trim="email">
      </div>
      <div class="input"> 
        <label for="firstName">First Name</label>
        <input 
        type="text"
        id="firstName" 
        v-model.trim="firstName">
      </div>
      <div class="input">
        <label for="lastName">Last Name</label>
        <input 
        type="text" 
        id="lastName" 
        v-model.trim="lastName">
      </div>
      <div class="input">
        <label for="password">Password</label>
        <input 
        type="password" 
        id="password" 
        v-model.trim="password">
      </div>
      <button type="submit">Submit</button>
    </form>
  </div>
</template>
<script>
export default {
  data() {
    return {
      email: '',
      password: '',
      firstName: '',
      lastName: '',
    };
  },
  methods: {
    onSubmit(){
    }
  },
}
</script>

这里涉及很多内容,让我们一步一步来分解:

  1. 我们正在创建一个新的表单,使用@submit.prevent指令,这样当表单提交时页面不会重新加载,这与在表单上调用 submit 并在事件上使用preventDefault是一样的。

  2. 接下来,我们将在每个表单输入元素中添加v-model.trim,以便修剪任何空白并将输入捕获为变量

  3. 我们在数据函数中定义这些变量,以便它们是响应式的

  4. submit按钮被定义为type="submit",这样当点击它时,表单的submit函数就会运行

  5. 我们正在创建一个空白的onSubmit函数,很快就会创建它

现在我们需要添加@input事件,并在每个input元素上调用touch事件,绑定到数据属性v-model,并为字段提供验证,如下所示:

<div class="input">
  <label for="email">Email</label>
  <input 
  type="email" 
  id="email" 
  @input="$v.email.$touch()"
  v-model.trim="email">
</div>
<div class="input"> 
  <label for="firstName">First Name</label>
  <input 
  type="text"
  id="firstName" 
  v-model.trim="firstName"
  @input="$v.firstName.$touch()">
</div>
<div class="input">
  <label for="lastName">Last Name</label>
  <input 
  type="text" 
  id="lastName" 
  v-model.trim="lastName"
  @input="$v.lastName.$touch()">
</div>
<div class="input">
  <label for="password">Password</label>
  <input 
  type="password" 
  id="password" 
  v-model.trim="password"
  @input="$v.password.$touch()">
</div>

然后,通过从Vuelidate导入它们并添加与表单元素对应的validations对象,将验证添加到我们的 Vue 实例中。

Vuelidate将使用相同的名称与我们的data变量绑定,如下所示:

import { required, email } from 'vuelidate/lib/validators';

export default {
 // Omitted
  validations: {
    email: {
      required,
      email,
    },
    firstName: {
      required,
    },
    lastName: {
      required,
    },
    password: {
      required,
    }
  },
}

我们只需导入所需的电子邮件验证器并将其应用于每个模型项。这基本上确保了我们所有的项目都是必需的,并且电子邮件输入与电子邮件正则表达式匹配。然后,我们可以通过添加以下内容来可视化表单和每个字段的当前状态:

 <div class="validators">
  <pre>{{$v}}</pre>
 </div>

然后,我们可以添加一些样式来显示右侧的验证和左侧的表单:

<style>
.form {
 display: inline-block;
 text-align: center;
 width: 49%;
}
.validators {
 display: inline-block;
 width: 49%;
 text-align: center;
 vertical-align: top;
}
.input {
 padding: 5px;
}
</style>

如果一切都按计划进行,我们应该会得到以下结果:

显示表单错误

我们可以使用$invalid布尔值来显示消息或更改表单字段的外观和感觉。让我们首先添加一个名为error的新类,它在输入字段周围添加了red border

<style>
input:focus {
  outline: none;
}
.error {
  border: 1px solid red;
}
</style>

然后,我们可以在字段无效且已触摸时有条件地应用此类,使用v-bind:class

<div class="input">
  <label for="email">Email</label>
  <input 
  :class="{ error: $v.email.$error }"
  type="email" 
  id="email" 
  @input="$v.email.$touch()"
  v-model.trim="email">
</div>
<div class="input"> 
  <label for="firstName">First Name</label>
  <input 
  :class="{ error: $v.firstName.$error }"
  type="text"
  id="firstName" 
  v-model.trim="firstName"
  @input="$v.firstName.$touch()">
</div>
<div class="input">
  <label for="lastName">Last Name</label>
  <input 
  :class="{ error: $v.lastName.$error}"
  type="text" 
  id="lastName" 
  v-model.trim="lastName"
  @input="$v.lastName.$touch()">
</div>
<div class="input">
  <label for="password">Password</label>
  <input 
  :class="{ error: $v.password.$error }"
  type="password" 
  id="password" 
  v-model.trim="password"
  @input="$v.password.$touch()">
</div>

这样,每当字段无效或有效时,我们就会得到以下结果:

随后,如果是这种情况,我们可以显示错误消息。这可以通过多种方式来完成,具体取决于您想要显示的消息类型。让我们以email输入为例,当email字段具有无效的电子邮件地址时显示错误消息:

<div class="input">
  <label for="email">Email</label>
  <input 
  :class="{ error: $v.email.$error }"
  type="email" 
  id="email" 
  @input="$v.email.$touch()"
  v-model.trim="email">

  <p class="error-message" v-if="!$v.email.email">Please enter a valid email address</p>
</div>

// Omitted
<style>
.error-message {
 color: red;
}
</style>

从我们的$v对象的表示中,我们可以看到当字段具有有效的电子邮件地址时,电子邮件布尔值为 true,如果不是,则为 false。虽然这检查电子邮件是否正确,但它并不检查字段是否为空。让我们添加另一个基于required验证器的检查这一点的错误消息:

 <p class="error-message" v-if="!$v.email.email">Please enter a valid email address.</p>
 <p class="error-message" v-if="!$v.email.required">Email must not be empty.</p>

如果我们愿意,甚至可以更进一步,创建自己的包装组件,用于呈现每个字段的各种错误消息。让我们填写剩下的错误消息,以及检查表单元素是否已被触摸(即$dirty):

<div class="input">
  <label for="email">Email</label>
  <input 
  :class="{ error: $v.email.$error }"
  type="email" 
  id="email" 
  @input="$v.email.$touch()"
  v-model.trim="email">

  <div v-if="$v.email.$dirty">
    <p class="error-message" v-if="!$v.email.email">Please enter a 
    valid email address.</p>
    <p class="error-message" v-if="!$v.email.required">Email must not 
    be empty.</p>
  </div>

</div>
<div class="input"> 
  <label for="firstName">First Name</label>
  <input 
  :class="{ error: $v.firstName.$error }"
  type="text"
  id="firstName" 
  v-model.trim="firstName"
  @input="$v.firstName.$touch()">

  <div v-if="$v.firstName.$dirty">
    <p class="error-message" v-if="!$v.firstName.required">First Name 
  must not be empty.</p>
  </div>
</div>
<div class="input">
  <label for="lastName">Last Name</label>
  <input 
  :class="{ error: $v.lastName.$error}"
  type="text" 
  id="lastName" 
  v-model.trim="lastName"
  @input="$v.lastName.$touch()">

  <div v-if="$v.lastName.$dirty">
    <p class="error-message" v-if="!$v.lastName.required">Last Name 
   must not be empty.</p>
  </div>
</div>
<div class="input">
  <label for="password">Password</label>
  <input 
  :class="{ error: $v.password.$error }"
  type="password" 
  id="password" 
  v-model.trim="password"
  @input="$v.password.$touch()">

  <div v-if="$v.password.$dirty">
    <p class="error-message" v-if="!$v.password.required">Password must 
  not be empty.</p>
  </div>
</div>

密码验证

在创建用户帐户时,密码往往会被输入两次,并符合最小长度。让我们添加另一个字段和一些更多的验证规则来强制执行这一点:

import { required, email, minLength, sameAs } from 'vuelidate/lib/validators';

export default {
 // Omitted
  data() {
    return {
      email: '',
      password: '',
      repeatPassword: '',
      firstName: '',
      lastName: '',
    };
  },
  validations: {
    email: {
      required,
      email,
    },
    firstName: {
      required,
    },
    lastName: {
      required,
    },
    password: {
      required,
      minLength: minLength(6),
    },
    repeatPassword: {
      required,
      minLength: minLength(6),
      sameAsPassword: sameAs('password'),
    },
  },
}

我们已经完成了以下工作:

  1. repeatPassword字段添加到我们的数据对象中,以便它可以保存重复的密码

  2. Vuelidate导入了minLengthsameAs验证器

  3. password验证器的minLength添加为6个字符

  4. 添加了sameAs验证器来强制repeatPassword应遵循与password相同的验证规则

现在我们已经有了适当的密码验证,我们可以添加新字段并显示任何错误消息:

<div class="input">
 <label for="email">Email</label>
 <input 
 :class="{ error: $v.email.$error }"
 type="email" 
 id="email" 
 @input="$v.email.$touch()"
 v-model.trim="email">

 <div v-if="$v.email.$dirty">
 <p class="error-message" v-if="!$v.email.email">Please enter a valid email address.</p>
 <p class="error-message" v-if="!$v.email.required">Email must not be empty.</p>
 </div>

</div>
<div class="input"> 
 <label for="firstName">First Name</label>
 <input 
 :class="{ error: $v.firstName.$error }"
 type="text"
 id="firstName" 
 v-model.trim="firstName"
 @input="$v.firstName.$touch()">

 <div v-if="$v.firstName.$dirty">
 <p class="error-message" v-if="!$v.firstName.required">First Name must not be empty.</p>
 </div>
</div>
<div class="input">
 <label for="lastName">Last Name</label>
 <input 
 :class="{ error: $v.lastName.$error}"
 type="text" 
 id="lastName" 
 v-model.trim="lastName"
 @input="$v.lastName.$touch()">

 <div v-if="$v.lastName.$dirty">
 <p class="error-message" v-if="!$v.lastName.required">Last Name must not be empty.</p>
 </div>
</div>
<div class="input">
 <label for="password">Password</label>
 <input 
 :class="{ error: $v.password.$error }"
 type="password" 
 id="password" 
 v-model.trim="password"
 @input="$v.password.$touch()">

 <div v-if="$v.password.$dirty">
 <p class="error-message" v-if="!$v.password.required">Password must not be empty.</p>
 </div>
</div>
<div class="input">
 <label for="repeatPassword">Repeat Password</label>
 <input 
 :class="{ error: $v.repeatPassword.$error }"
 type="password" 
 id="repeatPassword" 
 v-model.trim="repeatPassword"
 @input="$v.repeatPassword.$touch()">

 <div v-if="$v.repeatPassword.$dirty">
 <p class="error-message" v-if="!$v.repeatPassword.sameAsPassword">Passwords must be identical.</p>

 <p class="error-message" v-if="!$v.repeatPassword.required">Password must not be empty.</p>
 </div>
</div>

表单提交

接下来,如果表单无效,我们可以禁用我们的“提交”按钮:

<button :disabled="$v.$invalid" type="submit">Submit</button>

我们还可以在 JavaScript 中使用this.$v.$invalid来获取此值。以下是一个示例,演示了如何检查表单是否无效,然后根据我们的表单元素创建用户对象:

methods: {
  onSubmit() {
    if(!this.$v.$invalid) {
      const user = { 
        email: this.email,
        firstName: this.firstName,
        lastName: this.lastName,
        password: this.password,
        repeatPassword: this.repeatPassword
      }

      // Submit the object to an API of sorts
    }
  },
},

如果您希望以这种方式使用您的数据,您可能更喜欢设置您的数据对象如下:

data() {
  return {
    user: {
      email: '',
      password: '',
      repeatPassword: '',
      firstName: '',
      lastName: '',
    }
  };
},

我们现在已经创建了一个具有适当验证的表单!

渲染/功能组件

我们将改变方向,从验证和动画转向考虑使用功能组件和渲染函数来提高应用程序性能。您可能也会听到它们被称为“呈现组件”,因为它们是无状态的,只接收数据作为输入属性。

到目前为止,我们只声明了组件的标记,使用了template标签,但也可以使用render函数(如src/main.js中所示):

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

new Vue({
  el: '#app',
  render: h => h(App)
})

h来自超文本,它允许我们用 JavaScript 创建/描述 DOM 节点。在render函数中,我们只是渲染App组件,将来我们会更详细地看这个。Vue 创建了一个虚拟 DOM,使得处理实际 DOM 变得更简单(以及在处理大量元素时提高性能)。

渲染元素

我们可以用以下对象替换我们的App.vue组件,该对象接受一个render对象和hyperscript,而不是使用template

<script>
export default {
 render(h) {
  return h('h1', 'Hello render!')
 }
}
</script>

然后渲染一个带有文本节点'Hello render!'的新h1标签,这就是所谓的VNode虚拟节点),复数形式为VNodes虚拟 DOM 节点),它描述了整个树。现在让我们看看如何在ul中显示一个项目列表:

  render(h){
    h('ul', [
      h('li', 'Evan You'),
      h('li', 'Edd Yerburgh'),
      h('li', 'Paul Halliday')
    ])
 }

重要的是要意识到,我们只能用超文本渲染一个根节点。这个限制对我们的模板也是一样的,所以我们预期将我们的项目包裹在一个div中,就像这样:

render(h) {
 return h('div', [
  h('ul', [
   h('li', 'Evan You'),
   h('li', 'Edd Yerburgh'),
   h('li', 'Paul Halliday')
  ])
 ])
}

属性

我们还可以向我们渲染的项目传递样式元素和各种其他属性。以下是一个使用style对象来将每个项目的颜色更改为red的示例:

 h('div', [
  h('ul', { style: { color: 'red' } }, [
   h('li', 'Evan You'),
   h('li', 'Edd Yerburgh'),
   h('li', 'Paul Halliday')
  ])
 ])

正如你可以想象的那样,我们可以添加尽可能多的style属性,以及我们期望的额外选项,比如propsdirectiveson(点击处理程序)等。让我们看看如何映射元素以渲染带有props的组件。

组件和 props

让我们在components/ListItem.vue下创建一个ListItem组件,其中有一个 prop,name。我们将在我们的li的位置渲染这个组件,并在包含各种names的数组上进行映射。请注意,我们还向我们的 Vue 实例添加了functional: true选项;这告诉 Vue 这纯粹是一个呈现组件,它不会有任何自己的状态:

<script>
export default {
 props: ['name'],
 functional: true
}
</script>

在我们的render函数中,h通常也被称为createElement,因为我们在 JavaScript 上下文中,我们能够利用数组操作符,如mapfilterreduce等。让我们用map替换静态名称,用动态生成的组件:

import ListItem from './components/ListItem.vue';

export default {
 data() {
  return {
   names: ['Evan You', 'Edd Yerburgh', 'Paul Halliday']
  }
 },
 render(createElement) {
  return createElement('div', [
   createElement('ul',
    this.names.map(name => 
     createElement(ListItem, 
      {props: { name: name } })
     ))
   ])
 }
}

我们需要做的最后一件事是向我们的组件添加一个render函数。作为第二个参数,我们能够访问上下文对象,这使我们能够访问propsoptions。在这个例子中,我们假设name prop 总是存在且不是nullundefined

export default {
 props: ['name'],
 functional: true,
 render(createElement, context) {
  return createElement('li', context.props.name)
 }
}

再次,我们现在有一个包含作为prop传递的项目的元素列表:

JSX

虽然这是一个很好的思考练习,但在大多数情况下,模板更优越。也许有时您想在组件内部使用render函数,在这种情况下,使用 JSX 可能更简单。

让我们通过在终端中运行以下命令将 JSX 的 babel 插件添加到我们的项目中:

**$ npm i -D babel-helper-vue-jsx-merge-props babel-plugin-syntax-jsx babel-plugin-transform-vue-jsx** 

然后我们可以更新我们的.babelrc以使用新的插件:

{
 "presets": [
 ["env", { "modules": false }],
 "stage-3"
 ],
 "plugins": ["transform-vue-jsx"]
}

这使我们能够重写我们的render函数,以利用更简单的语法:

render(h) {
 return (
  <div>
   <ul>
    { this.names.map(name => <ListItem name={name} />) }
   </ul>
  </div>
 )
}

这更具有声明性,而且更容易维护。在底层,它被转译为以前的hyperscript格式与 Babel 一起。

总结

在本章中,我们学习了如何在 Vue 项目中利用 CSS 动画和过渡。这使我们能够使用户体验更流畅,并改善我们应用程序的外观和感觉。

我们还学习了如何使用render方法构建我们的 UI;这涉及使用 HyperScript 创建 VNodes,然后使用 JSX 进行更清晰的抽象。虽然您可能不想在项目中使用 JSX,但如果您来自 React 背景,您可能会觉得更舒适。

第七章:HTTP 和 WebSocket 通信

在本章中,我们将看看如何使用HTTP与服务器端 API 进行接口交互。我们将使用HTTP GETPOSTPUTPATCHDELETE创建一个应用程序,以及创建一个利用Socket.io库的内存实时聊天应用程序,利用 WebSockets。

在本章结束时,您将知道如何:

  • 使用json-server创建模拟数据库 API

  • 使用Axios创建 HTTP 请求

  • 使用 WebSockets 和Socket.io进行客户端之间的实时通信

HTTP

让我们首先创建一个新的 Vue.js 项目,作为我们的游乐场项目。在终端中输入以下内容:

# Create a new Vue project
$ vue init webpack-simple vue-http

# Navigate to directory
$ cd vue-http
# Install dependencies
$ npm install

# Run application
$ npm run dev

在 JavaScript 中有许多创建 HTTP 请求的方法。我们将使用Axios库在项目中使用简化的基于 promise 的方法。让我们通过在终端中输入以下内容来安装它:

# Install Axios to our project
$ npm install axios --save

我们现在有了创建 HTTP 请求的能力;我们只需要一个 API 来指向Axios。让我们创建一个模拟 API。

安装 JSON 服务器

为了创建一个模拟 API,我们可以使用json-server库。这允许我们通过在项目内创建一个db.json文件来快速全局启动。它有效地创建了一个 GET,POST,PUT,PATCH 和 DELETE API,并将数据存储在一个文件中,附加到我们的原始 JSON 文件中。

我们可以通过在终端中运行以下命令来安装它:

# Install the json-server module globally
$ npm install json-server -g

由于我们添加了-g标志,我们将能够在整个终端中全局访问json-server模块。

接下来,我们需要在项目的根目录下创建我们的db.json文件。您可以根据需要对数据集进行创意处理;我们只是简单地有一份我们可能感兴趣的课程列表:

{
  "courses": [
    {
      "id": 1,
      "name": "Vue.js Design Patterns"
    },
    {
      "id": 2,
      "name": "Angular: From Beginner to Advanced"
    },
    {
      "id": 3,
      "name": "Cross Platform Native Applications with Fuse"
    }
  ]
}

然后我们可以通过在终端中运行以下命令来运行我们的数据库:

# Run the database based on our db.json file
$ json-server db.json --watch

如果我们一切顺利,我们应该能够通过http://localhost:3000访问我们的数据库,如下成功消息所示:

太棒了。我们已经准备好了,现在我们可以获取课程列表。

HTTP GET

我们需要做的第一件事是将Axios导入到我们的App.vue组件中。在这种情况下,我们还可以设置一个ROOT_URL,因为我们只会寻找/courses端点:

<script>
import axios from 'axios'
export default {
  data() {
    return {
      ROOT_URL: 'http://localhost:3000/courses',
      courses: []
    }
  }
}
</script>

这样我们就能够钩入created()这样的生命周期钩子,并调用一个从我们的 API 请求课程的方法:

export default {
  data() {
    return {
      ROOT_URL: 'http://localhost:3000/courses',
      courses: []
    }
  },
  created() {
    this.getCourseList();
  },
  methods: {
    getCourseList() {
      axios
        .get(this.ROOT_URL)
        .then(response => {
          this.courses = response.data;
        })
        .catch(error => console.log(error));
    }
  }
}

这里发生了什么?我们调用了getCoursesList函数,该函数向我们的http://localhost:3000/courses端点发出了 HTTPGET请求。然后,它要么将课程数组设置为数据(也就是说,我们的db.json中的所有内容),要么仅仅在出现错误时记录错误。

然后,我们可以使用v-指令在屏幕上显示这个:

<template>
  <div class="course-list">
    <h1>Courses</h1>
    <div v-for="course in courses" v-bind:key="course.id">
      <p>
        {{course.name}}
      </p> 
    </div>
  </div>
</template>

再加上一点样式,我们得到:

<style>
.course-list {
  background-color: rebeccapurple;
  padding: 10px;
  width: 50%;
  text-align: center;
  margin: 0 auto;
  color: white;
}
</style>

让我们继续进行 HTTP POST!

HTTP POST

我们可以在courseName div后面添加一个输入框和button,允许用户向他们的学习列表中输入一个新的课程:

<div>
 <input type="text" v-model="courseName" placeholder="Course name"> 
 <button @click="addCourse(courseName)">Add</button>
</div>

这要求我们将courseName变量添加到我们的data对象中:

data() {
 return {
  ROOT_URL: 'http://localhost:3000/courses/',
  courses: [],
  courseName: '',
 };
},

然后,我们可以创建一个名为addCourse的类似方法,该方法以courseName作为参数:

methods: {
// Omitted
 addCourse(name) {
  axios
   .post(this.ROOT_URL, { name })
   .then(response => {
     this.courses.push(response.data);
     this.courseName = ''; 
   })
   .catch(error => console.log(error));
 }
}

您可能会注意到它与之前的 HTTP 调用非常相似,但这次我们使用的是.post而不是.get,并传递了一个具有name键和值的对象。

发送 POST 请求后,我们使用this.courses.push(response.data)来更新客户端数组,因为虽然服务器端(我们的客户端db.json文件)已更新,但客户端状态没有更新。

HTTP PUT

接下来,我们想要做的是能够更改列表中的项目。也许在提交项目时我们犯了一个错误,因此我们想要编辑它。让我们添加这个功能。

首先,让我们告诉 Vue 跟踪我们何时正在编辑课程。用户编辑课程的意图是每当他们点击课程名称时;然后我们可以将编辑布尔值添加到我们的data对象中:

data() {
 return {
  ROOT_URL: 'http://localhost:3000/courses/',
  courses: [],
  courseName: '',
  editing: false,
 };
},

然后我们的模板可以更改以反映这一点:

<template>
 <div class="course-list">
  <h1>Courses</h1>
  <div v-for="course in courses" v-bind:key="course.id">
   <p @click="setEdit(course)" v-if="!editing">
   {{course.name}}
   </p>
  <div v-else>
   <input type="text" v-model="course.name">
   <button @click="saveCourse(course)">Save</button>
  </div> 
  </div>
  <div v-if="!editing">
  <input type="text" v-model="courseName" placeholder="Course name"> 
  <button @click="addCourse(courseName)">Add</button>
  </div>
 </div>
</template>

这里到底发生了什么?嗯,我们已经将我们的courseName更改为只在我们不编辑时显示(也就是说,我们没有点击课程名称)。相反,使用v-else指令,我们显示一个输入框和button,允许我们保存新的CourseName

此时,我们还隐藏了添加课程按钮,以保持简单。

代码如下所示:

setEdit(course) {
 this.editing = !this.editing;
},
saveCourse(course) {
 this.setEdit();
 axios
 .put(`${this.ROOT_URL}/${course.id}`, { ...course })
 .then(response => {
 console.log(response.data);
 })
 .catch(error => console.log(error));
}

在这里,我们在指向所选课程的端点上使用了我们的axios实例上的.put方法。作为数据参数,我们使用了展开操作符{ ...course }来解构课程变量以与我们的 API 一起使用。

之后,我们只是将结果记录到控制台。当我们将"Vue.js Design Patterns"字符串编辑为简单地说Vue.js时,它看起来是这样的:

耶!我们要看的最后一件事是 DELETE 和从我们的数据库中删除项目。

HTTP DELETE

为了从我们的列表中删除项目,让我们添加一个button,这样当用户进入编辑模式(通过点击一个项目)时,他们可以删除那个特定的课程:

<div v-else>
  <input type="text" v-model="course.name">
  <button @click="saveCourse(course)">Save</button>
  <button @click="removeCourse(course)">Remove</button>
</div> 

我们的removeCourse函数如下:

removeCourse(course) {
  axios
    .delete(`${this.ROOT_URL}/${course.id}`)
    .then(response => {
      this.setEdit();
      this.courses = this.courses.filter(c => c.id != course.id);
    })
    .catch(error => console.error(error));
},

我们调用axios.delete方法,然后过滤我们的courses列表,除了我们删除的课程之外的每个课程。然后更新我们的客户端状态,并使其与数据库一致。

在本章的这一部分中,我们根据我们的 REST API 创建了一个简单的“我想学习的课程”列表。它当然可以被抽象为多个组件,但由于这不是应用程序的核心重点,我们只是在一个组件中完成了所有操作。

接下来,让我们使用 Node 和Socket.io制作一个实时聊天应用程序。

使用 Node 和 Socket.io 制作实时聊天应用程序

在本节中,我们将使用 Node 和Socket.io创建一个实时聊天应用程序。我们将使用 Node.js 和 Express 框架编写少量代码,但它都是您所熟悉和喜爱的 JavaScript。

在您的终端中运行以下命令以创建一个新项目:

# Create a new Vue project
$ vue init webpack-simple vue-chat

# Navigate to directory
$ cd vue-chat

# Install dependencies
$ npm install

# Run application
$ npm run dev

然后我们可以创建一个服务器文件夹,并初始化一个package.json,用于服务器特定的依赖项,如下所示:

# Create a new folder named server
$ mkdir server

# Navigate to directory
$ cd server

# Make a server.js file
$ touch server.js

# Initialise a new package.json
$ npm init -y

# Install dependencies
$ npm install socket.io express --save

什么是 Socket.io?

在我们之前的例子中,如果我们想要从服务器获取新数据,我们需要发出另一个 HTTP 请求,而使用 WebSockets,我们可以简单地拥有一个一致的事件监听器,每当事件被触发时就会做出反应。

为了在我们的聊天应用程序中利用这一点,我们将使用Socket.io。这是一个客户端和服务器端的库,允许我们快速轻松地使用 WebSockets。它允许我们定义和提交事件,我们可以监听并随后执行操作。

服务器设置

然后,我们可以使用 Express 创建一个新的 HTTP 服务器,并通过在server.js中添加以下内容来监听应用程序连接:

const app = require('express')();
const http = require('http').Server(app);
const io = require('socket.io')(http);
const PORT = 3000;

http.listen(PORT, () => console.log(`Listening on port: ${PORT}`));

io.on('connection', socket => {
  console.log('A user connected.');
});

如果我们在server文件夹内的终端中运行node server.js,我们应该会看到消息“Listening on port: 3000”。这意味着一旦我们在客户端应用程序中实现Socket.io,我们就能够监视每当有人连接到应用程序时。

客户端连接

为了捕获客户端连接,我们需要在 Vue 应用程序中安装Socket.io。我们还将使用另一个名为vue-socket.io的依赖项,在 Vue 应用程序中为我们提供更流畅的实现。

在终端中运行以下命令,确保你在根目录下(即不在server文件夹中):

# Install socket.io-client and vue-socket.io
$ npm install socket.io-client vue-socket.io --save

设置 Vue 和 Socket.io

让我们转到我们的main.js文件,这样我们就可以注册Socket.ioVue-Socket.io插件。你可能还记得如何在之前的章节中做到这一点:

import Vue from 'vue';
import App from './App.vue';
import SocketIo from 'socket.io-client';
import VueSocketIo from 'vue-socket.io';

export const Socket = SocketIo(`http://localhost:3000`);

Vue.use(VueSocketIo, Socket);

new Vue({
  el: '#app',
  render: h => h(App),
});

在上述代码块中,我们导入必要的依赖项,并创建对我们当前运行在端口3000上的 Socket.io 服务器的引用。然后我们使用Vue.use添加 Vue 插件。

如果我们做的一切都正确,我们的客户端和服务器应该在彼此交流。我们应该在终端中看到以下内容:

确定连接状态

现在我们已经添加了 Vue-Socket.io 插件,我们可以在 Vue 实例内部访问 sockets 对象。这使我们能够监听特定事件,并确定用户是否连接或断开 WebSocket 连接。

App.vue中,让我们在屏幕上显示一条消息,如果我们与服务器连接/断开连接:

<template>
  <div>
    <h1 v-if="isConnected">Connected to the server.</h1>
    <h1 v-else>Disconnected from the server.</h1>
  </div>
</template>

<script>
export default {
  data() {
    return {
      isConnected: false,
    };
  },
  sockets: {
    connect() {
      this.isConnected = true;
    },
    disconnect() {
      this.isConnected = false;
    },
  },
};
</script>

除了 sockets 对象之外,这里不应该有太多新的东西。每当我们连接到 socket 时,我们可以在connect()钩子内运行任何代码,disconnect()也是一样。我们只是翻转一个布尔值,以便在屏幕上显示不同的消息,使用v-ifv-else指令。

最初,我们得到了 Connected to the server,因为我们的服务器正在运行。如果我们在终端窗口中使用CTRL + C停止服务器,我们的标题将更改以反映我们不再具有 WebSocket 连接的事实。以下是结果:

创建连接状态栏

让我们用这个概念玩一些游戏。我们可以创建一个 components 文件夹,然后创建一个名为ConnectionStatus.vue的新组件。在这个文件中,我们可以创建一个状态栏,当用户在线或离线时向用户显示:

<template>
  <div>
    <span v-if="isConnected === true" class="bar connected">
      Connected to the server.
    </span>
    <span v-else class="bar disconnected">
      Disconnected from the server.
    </span>
  </div>
</template>

<script>
export default {
  props: ['isConnected'],
};
</script>

<style>
.bar {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  text-align: center;
  padding: 5px;
}

.connected {
  background: greenyellow;
  color: black;
}

.disconnected {
  background: red;
  color: white;
}
</style>

虽然我们当前应用程序中只有一个屏幕,但我们可能希望在多个组件中使用这个组件,所以我们可以在main.js中全局注册它:

import App from './App.vue';
import ConnectionStatus from './components/ConnectionStatus.vue';

Vue.component('connection-status', ConnectionStatus);

然后,我们可以编辑我们的 App.vue 模板以使用此组件,并将当前连接状态作为 prop 传递:

<template>
  <div>
    <connection-status :isConnected="isConnected" />
  </div>
</template>

这是我们的结果:

接下来,我们可以创建一个导航栏组件,使我们的用户界面更完整。

导航栏

导航栏组件除了简单显示我们应用程序的名称外,不会有太多用途。您可以更改此功能,以包括其他功能,例如登录/注销、添加新的聊天频道或任何其他特定于聊天的用户操作。

让我们在 components 文件夹中创建一个名为 Navbar.vue 的新组件:

<template>
  <div v-once>
    <nav class="navbar">
      <span>Socket Chat</span>
    </nav>
  </div>
</template>

<script>
export default {};
</script>

<style>
.navbar {
  background-color: blueviolet;
  padding: 10px;
  margin: 0px;
  text-align: center;
  color: white;
}
</style>

您可能会注意到在这个 div 上添加了 v-once 指令。这是我们第一次看到它,但由于这个组件完全是静态的,我们可以告诉 Vue 不要监听任何更改,只渲染一次。

然后,我们必须删除 HTML body 内部的任何默认填充或边距。在根目录中创建一个名为 styles.css 的文件,其中包含这些属性:

body {
 margin: 0px;
 padding: 0px;
}

然后,我们可以像这样将其添加到我们的 index.html 文件中:

<head>
 <meta charset="utf-8">
 <title>vue-chat</title>
 <link rel="stylesheet" href="styles.css">
</head>

接下来,我们需要全局注册此组件。如果您觉得可以的话,请尝试在 main.js 中自行完成。

这要求我们导入 Navbar 并像这样注册它:

import Navbar from './components/Navbar.vue'

Vue.component('navigation-bar', Navbar);

然后我们可以将其添加到我们的 App.vue 文件中:

<template>
  <div>
    <navigation-bar />
    <connection-status :isConnected="isConnected" />
  </div>
</template>

接下来,让我们创建我们的 MessageList 组件来保存消息列表。

消息列表

通过创建一个接受消息数组的 prop 的新组件,我们可以在屏幕上显示消息列表。在 components 文件夹中创建一个名为 MessageList.vue 的新组件:

<template>
 <div>
  <span v-for="message in messages" :key="message.id">
  <strong>{{message.username}}: </strong> {{message.message}}
  </span>
 </div>
</template>

<script>
export default {
 props: ['messages'],
};
</script>

<style scoped>
div {
 overflow: scroll;
 height: 150px;
 margin: 10px auto 10px auto;
 padding: 5px;
 border: 1px solid gray;
}
span {
 display: block;
 padding: 2px;
}
</style>

这个组件非常简单;它只是使用 v-for 指令遍历我们的 messages 数组。我们使用适当的 prop 将消息数组传递给这个组件。

不要将此组件全局注册,让我们在 App.vue 组件内部特别注册它。在这里,我们还可以向 messages 数组添加一些虚拟数据:

import MessageList from './components/MessageList.vue';

export default {
 data() {
  return {
   isConnected: false,
   messages: [
    {
     id: 1,
     username: 'Paul',
     message: 'Hey!',
    },
    {
     id: 2,
     username: 'Evan',
     message: 'How are you?',
    },
   ],
  };
 },
 components: {
 MessageList,
},

然后我们可以将 message-list 组件添加到我们的模板中:

 <div class="container">
  <message-list :messages="messages" />
 </div>

我们根据数据对象中找到的消息数组将消息作为 prop 传递。我们还可以添加以下样式:

<style>
.container {
 width: 300px;
 margin: 0 auto;
}
</style>

这样做将使我们的消息框居中显示在屏幕上,并限制 width 以进行演示。

我们正在取得进展!这是我们的消息框:

接下来呢?嗯,我们仍然需要能够向我们的列表中添加消息的功能。让我们接下来处理这个。

向列表添加消息

在 components 文件夹中创建一个名为MessageForm.vue的新组件。这将用于将消息输入到列表中。

我们可以从以下开始:

<template>
  <form @submit.prevent="sendMessage">
    <div>
      <label for="username">Username:</label>
      <input type="text" name="username" v-model="username">
    </div>
    <div>
      <label for="message">Message:</label>
      <textarea name="message" v-model="message"></textarea>
    </div>
    <button type="submit">Send</button>
  </form>
</template>

<script>
export default {
  data() {
    return {
      username: '',
      message: '',
    };
  },
};
</script>

<style>
input,
textarea {
  margin: 5px;
  width: 100%;
}
</style>

这本质上允许我们捕获用户对所选usernamemessage的输入。然后我们可以使用这些信息在sendMessage函数中向我们的Socket.io服务器发送数据。

通过将@submit.prevent添加到我们的表单而不是@submit,我们确保覆盖了提交表单的默认行为;这是必要的,否则我们的页面会重新加载。

让我们去注册我们的表单在App.vue中,即使我们还没有连接任何操作:

import MessageList from './components/MessageList.vue';

export default {
 // Omitted
 components: {
   MessageList,
   MessageForm,
 },
}

然后我们可以将其添加到我们的模板中:

<template>
  <div>
    <navigation-bar />
    <div class="container">
      <message-list :messages="messages" />
      <message-form />
    </div>
    <connection-status :isConnected="isConnected" />
  </div>
</template>

现在我们的应用程序看起来是这样的:

使用 Socket.io 进行服务器端事件

为了发送新消息,我们可以在我们的server.js文件中监听名为chatMessage的事件。

这可以在我们的原始连接事件内完成,确保我们按 socket 逐个 socket 地监听事件:

io.on('connection', socket => {
  console.log('A user connected.');

  socket.on('chatMessage', message => {
    console.log(message);
  })
});

如果我们从客户端发送chatMessage事件,那么它应该随后在我们的终端内记录出这条消息。让我们试一试!

因为我们对server.js文件进行了更改,所以我们需要重新启动 Node 实例。在运行server.js的终端窗口中按下CTRL + C,然后再次运行 node server.js

Nodemon

或者,您可能希望使用一个名为nodemon的模块,在进行任何更改时自动执行此操作。

在您的终端内运行以下命令:

# Install nodemon globally
$ npm install nodemon -g

然后我们可以运行:

# Listen for any changes to our server.js file and restart the server
$ nodemon server.js

太好了!让我们回到我们的MessageForm组件并创建sendMessage函数:

methods: {
 sendMessage() {
   this.socket.emit('chatMessage', {
     username: this.username,
     message: this.message,
   });
 },
},

此时点击发送还没有将消息添加到数组中,但它确实在我们的终端内显示了发送的消息!让我们来看一下:

事实证明,我们不必写太多代码来利用我们的 WebSockets。让我们回到App.vue组件并向我们的 sockets 对象添加一个名为chatMessage的函数。注意这与事件名称相同,这意味着每次触发此事件时我们都可以运行特定的方法:

export default {
// Omitted
 sockets: {
  connect() {
   this.isConnected = true;
  },
  disconnect() {
   this.isConnected = false;
  },
  chatMessage(messages) {
   this.messages = messages;
  },
 },
}

我们的客户端代码现在已经连接并监听chatMessage事件。问题在于我们的服务器端代码目前没有向客户端发送任何内容!让我们通过在 socket 内部发出一个事件来解决这个问题:

const app = require('express')();
const http = require('http').Server(app);
const io = require('socket.io')(http);
const PORT = 3000;

http.listen(PORT, () => console.log(`Listening on port: ${PORT}`));

const messages = [];

const emitMessages = () => io.emit('chatMessage', messages);

io.on('connection', socket => {
  console.log('A user connected.');

  emitMessages(messages);

  socket.on('chatMessage', message => {
    messages.push(message);

    emitMessages(messages);
  });
});

我们使用一个名为 messages 的数组将消息保存在内存中。每当客户端连接到我们的应用程序时,我们也会向下游发送这些消息(所有先前的消息都将显示)。除此之外,每当数组中添加新消息时,我们也会将其发送给所有客户端。

如果我们打开两个 Chrome 标签,我们应该能够进行自我导向的对话!

然后我们可以在另一个标签页中与自己交谈!

总结

在本章中,我们学习了如何使用Axios库和json-server在 Vue 中创建 HTTP 请求。这使我们能够与第三方 API 进行交互,并增强我们的 Vue 应用程序。

我们还学习了如何使用 WebSockets 和Socket.io创建一个更大的应用程序。这使我们能够与连接到我们的应用程序的其他客户端进行实时通信,从而实现更多的可能性。

我们已经走了很长的路!为了真正利用 Vue,我们需要掌握路由器并了解高级状态管理概念。这将在接下来的章节中讨论!