五、Vue.js 组件的安全通信
在注意到现代 web 应用中的组件驱动体系结构之前,您不需要看太远。开发需求在短时间内发生了变化,web 从一个简单的文档查看器转变为托管具有大量代码库的复杂应用。因此,创建可重用组件的能力使我们作为前端开发人员的生活更加轻松,因为我们可以将核心功能封装到单个块中,从而降低总体复杂性,更好地分离关注点、协作和可伸缩性。
在本章中,我们将学习前面的概念,并将其应用到我们的 Vue 应用中。在本章结束时,您将实现:
- 创建自己的 Vue 组件的能力
- 更好地理解单个文件组件
- 创建特定于每个组件的样式的能力
- 能够在本地和全局注册组件,并理解为什么选择一个而不是另一个
- 使用道具在父组件和子组件之间进行通信的能力
- 使用全局事件总线跨应用进行通信的能力
- 使用插槽使组件更加灵活的能力
让我们先看看您的第一个 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
当我们为 VisualStudio 代码使用 Vetur 扩展时,我们可以键入scaffold
并点击选项卡,然后创建一个可在项目内部使用的 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 自定义元素规范(中的命名约定非常重要 https://www.w3.org/TR/custom-elements/#valid-自定义元素名称,即所有小写字母,且必须包含连字符。
在我们的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
,为什么它会得到样式?谢天谢地,阻止样式泄漏到组件之外很简单,我们只需将scoped
属性添加到style
标记中:
<style scoped>
button {
border: 1px solid black;
padding: 10px;
}
</style>
默认情况下,作用域属性不是 Vue 的一部分,它来自我们的网页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
传递该数据。让我们来看看在我们的 To1 T1 上制作一个属性,它改变了 Ty2 T2 文本:
<template>
<button>
{{buttonText}}
</button>
</template>
<script>
export default {
props: ['buttonText'],
}
</script>
<style scoped>
button {
border: 1px solid black;
padding: 10px;
}
</style>
注意,当我们自己创建了一个包含每个组件属性的字符串或对象值的props
数组时,我们如何能够绑定到模板中的buttonText
值。可以将 kebab case 设置为组件本身的属性,这是必需的,因为 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
}
},
}
首先,我们要确保只能将字符串类型传递到此属性。我们还可以对照其他类型进行检查,例如:
- 大堆
- 布尔值
- 作用
- 数字
- 对象
- 一串
- 象征
根据 web 组件良好实践,向道具发送原始值是一种良好实践。
在引擎盖下,这是针对属性运行instanceof
操作符,因此它还可以针对构造函数类型运行检查,如以下屏幕截图所示:
同时,我们还可以使用数组语法检查多种类型:
export default {
props: {
buttonText: {
type: [String, Number, Cat],
}
},
}
接下来,我们将默认文本设置为FancyButton!
,这意味着默认情况下,如果未设置此属性,它将具有该值。我们还将 required 设置为 true,这意味着无论何时创建一个FancyButton
都必须包含buttonText
属性。
这在术语(即默认值和必需值)上是矛盾的,但有时您希望在不需要属性的情况下使用默认值。最后,我们将向其添加一个验证函数,以指定任何时候设置此属性时,其字符串长度必须大于 3。
我们如何知道属性验证是否失败?在开发模式下,我们可以检查我们的开发控制台,我们应该有一个相应的错误。例如,如果我们忘记在组件上添加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();
接下来,我们可以创建两个组件,ShoppingInput
和ShoppingList
。这将允许我们输入一个新项目,并在我们的购物清单上显示一个输入项目列表,从我们的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
而不是使用它,将应用的事件系统从基于组件更改为基于应用。然后,我们可以使用$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
,然后我们可以将它添加到我们的数组中。我们可以将数组作为道具传递给另一个组件,例如要在屏幕上渲染的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
然后我们可以继续创建一个名为Message
(src/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
组件的每个声明,这表明即使我们将内容注入这个空间,我们仍然可以每次显示特定于该组件的模板信息。
默认值
虽然我们可以将内容添加到插槽中,但我们可能希望添加默认内容,以显示我们自己没有添加任何内容。这意味着我们不必每次都添加内容,如果我们愿意,我们可以在这种情况下覆盖它。
如何将默认行为添加到插槽中?那很简单!我们需要做的就是在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
组件需要date
和messageText
输入,其中一个是插槽,另一个是组件的属性。我们的用例是,也许我们希望以不同的方式显示日期,添加不同的信息位,或者甚至不显示它。
我们的消息组件变成:
<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
您可能还记得在第 4 章、Vue.js 指令中使用了moment
,我们还将重用之前创建的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
(我们将其作为道具传递,标记在消息组件内部定义),但我们也使用slot
动态添加日期:
如果我们从 h2 中删除slot="date"
,会发生什么?日期还显示吗?不。这是因为当我们只使用命名插槽时,没有其他位置可以添加插槽。只有当我们将Message
组件更改为接受一个未命名的插槽时,才会出现如下情况:
<template>
<div>
<slot name="date"></slot>
<slot></slot>
<h1>{{messageText}}</h1>
</div>
</template>
总结
本章为我们提供了创建可相互通信的可重用组件的能力。我们已经研究了如何在整个项目中全局注册组件,或者在特定实例中本地注册组件,从而为我们提供灵活性和适当的关注点分离。从添加简单属性到复杂的验证和默认值,我们已经看到了它的强大功能。
在下一章中,我们将研究如何创建更好的 UI。我们将在表单、动画和验证的上下文中更多地研究指令,如v-model
。
版权属于:月萌API www.moonapi.com,转载请注明出处