这是关于 JavaScript 框架 Vue.js 的五部分系列文章的第二部分。 在这一部分中,我们将介绍组件、Props 和 Slots。 这并非旨在成为一份完整的指南,而是一份关于基础知识的概述,旨在帮助您快速上手,从而了解 Vue.js 并理解该框架提供的功能。

组件和数据传递
如果您熟悉 React 或 Angular2,那么组件和传递状态的概念对您来说并不陌生。 如果您不熟悉,让我们一起了解一些主要概念。
无论大小,网站通常都由不同的部分组成,将它们抽象成更小的部分可以使它们易于构建、推理、重用,并使我们的代码更易读。 我们可以将它分解成如下所示的组件,而不是在冗长、多方面页面中深入挖掘所有标记。
<header></header>
<aside>
<sidebar-item v-for="item in items"></sidebar-item>
</aside>
<main>
<blogpost v-for="post in posts"></blogpost>
</main>
<footer></footer>
这是一个简化的示例,但是您可以看到,当您开始构建站点的结构时,这种类型的组合有多么有用。 如果您作为维护人员深入研究此代码,则无需花费太多时间即可了解应用程序的结构或查找每个部分的位置。
Vue 允许我们通过几种不同的方式创建组件。 让我们从简单到复杂地进行操作,请记住,复杂的示例最符合普通 Vue 应用程序的外观。
new Vue({
el: 'hello',
template: '<h1>Hello World!</h1>'
});
这可以工作,但并不十分有用,因为它只能使用一次,我们还没有将信息传递给不同的组件。 从父组件传递数据到子组件的一种方法称为 **props**。
这是我能想到的最简单的示例,因此非常清楚。 请记住,HTML 中的 :text
是 Vue 绑定的快捷方式。 我们上次在关于指令的部分中介绍过这一点。 绑定可以用于各种事情,但在这种情况下,它使我们无需将状态放置在八字胡模板中,例如 {{ message }}
。
在下面的代码中,Vue.component
是 **组件**,new Vue
被称为 **实例**。 您可以在一个应用程序中拥有多个实例。 通常,我们将拥有一个实例和多个组件,因为实例是主应用程序。
Vue.component('child', {
props: ['text'],
template: `<div>{{ text }}<div>`
});
new Vue({
el: "#app",
data() {
return {
message: 'hello mr. magoo'
}
}
});
<div id="app">
<child :text="message"></child>
</div>
查看演示.
现在,我们可以在应用程序中根据需要多次重用此组件。
<div id="app">
<child :text="message"></child>
<child :text="message"></child>
</div>
查看演示.
我们还可以为 props 添加验证,这类似于 React 中的 PropTypes
。 这很好,因为它具有自文档功能,如果它不是我们期望的类型,则会在开发模式下返回错误。
Vue.component('child', {
props: {
text: {
type: String,
required: true
}
},
template: `<div>{{ text }}<div>`
});
在下面的示例中,我以开发模式加载 Vue,并故意将无效类型传递到我们的 prop 验证中。 您可以在控制台中看到错误。(它还有助于让您知道可以使用 Vue 的开发者工具以及在哪里找到它们)。
Vue.component('child', {
props: {
text: {
type: Boolean,
required: true
}
},
template: `<div>{{ text }}<div>`
});
查看演示 带有验证的简单 props,作者 Sarah Drasner (@sdras) 在 CodePen 上发布。
对象应作为工厂函数返回,您甚至可以将其作为自定义验证器函数传递,这非常好,因为您可以根据业务、输入或其他逻辑检查值。 有关如何使用每种类型的详细说明,请参阅 此处的指南。
您也不必一定将数据通过 props 传递给子组件,您可以根据需要使用状态或静态值。
Vue.component('child', {
props: {
count: {
type: Number,
required: true
}
},
template: `<div class="num">{{ count }}</div>`
})
new Vue({
el: '#app',
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++;
},
decrement() {
this.count--;
}
}
})
<div id="app">
<h3>
<button @click="increment">+</button>
Adjust the state
<button @click="decrement">-</button>
</h3>
<h2>This is the app state: <span class="num">{{ count }}</span></h2>
<hr>
<h4><child count="1"></child></h4>
<p>This is a child counter that is using a static integer as props</p>
<hr>
<h4><child :count="count"></child></h4>
<p>This is the same child counter and it is using the state as props</p>
</div>
查看演示 使用和不使用 props 的子组件,作者 Sarah Drasner (@sdras) 在 CodePen 上发布。
区别在于您是否正在传递属性并将其绑定。
不使用状态
<child count="1"></child>
对比
使用状态
<child :count="count"></child>
到目前为止,我们一直在使用字符串在子组件中创建内容,当然,如果您使用 babel 来处理所有浏览器中的 ES6(我强烈建议这样做),则可以使用 模板字面量 来避免可能难以阅读的字符串连接。
Vue.component('individual-comment', {
template:
`<li> {{ commentpost }} </li>`,
props: ['commentpost']
});
new Vue({
el: '#app',
data: {
newComment: '',
comments: [
'Looks great Julianne!',
'I love the sea',
'Where are you at?'
]
},
methods: {
addComment: function () {
this.comments.push(this.newComment)
this.newComment = ''
}
}
});
<ul>
<li
is="individual-comment"
v-for="comment in comments"
v-bind:commentpost="comment"
></li>
</ul>
<input
v-model="newComment"
v-on:keyup.enter="addComment"
placeholder="Add a comment"
>
查看演示 cd81de1463229a9612dca7559dd666e0,作者 Sarah Drasner (@sdras) 在 CodePen 上发布。
这更有用一些,但是即使借助模板字面量,我们仍然对要放入该字符串中的内容数量有限制。 最终,在此评论表单中,我们将希望拥有照片和作者的姓名,并且您可能已经猜到,包含所有这些信息会变得多么拥挤。 我们也不会在该字符串中获得任何有用的语法高亮显示。
考虑到所有这些因素,让我们创建一个模板。 我们将一些常规 HTML 包裹在特殊的 script 标记中,并使用 id 来引用它以创建组件。 当我们有很多文本和元素时,您可以看到这更易读。
<!-- This is the Individual Comment Component -->
<script type="text/x-template" id="comment-template">
<li>
<img class="post-img" :src="commentpost.authorImg" />
<small>{{ commentpost.author }}</small>
<p class="post-comment">"{{ commentpost.text }}"</p>
</li>
</script>
Vue.component('individual-comment', {
template: '#comment-template',
props: ['commentpost']
});
查看演示 使用 Vue.js 的照片应用程序帖子,作者 Sarah Drasner (@sdras) 在 CodePen 上发布。
Slots
这样好多了。 但是,当我们有两个具有细微差异的组件(内容或样式偏差)时会发生什么? 我们可以通过 props 将所有不同的内容和样式传递到组件中,并在每次使用时切换所有内容,或者我们可以分叉组件本身并创建它们的多个版本。 但是如果我们可以重用组件并使用相同的数据或功能填充它们,那就太好了。 这就是 Slots 发挥作用的地方。
假设我们有一个使用相同 <app-child>
组件两次的主应用程序实例。 在每个子组件内部,我们希望某些内容保持一致,而某些内容则需要更改。 对于我们希望保持一致的内容,我们将使用标准的 p 标记,而对于我们希望切换的内容,我们将放置一个空的 <slot></slot>
标记。
<script type="text/x-template" id="childarea">
<div class="child">
<slot></slot>
<p>It's a veritable slot machine!<br>
Ha ha aw</p>
</div>
</script>
然后,在应用程序实例中,我们可以在 <app-child>
组件标记内传递内容,它将自动填充 Slots。
<div id="app">
<h2>We can use slots to populate content</h2>
<app-child>
<h3>This is slot number one</h3>
</app-child>
<app-child>
<h3>This is slot number two</h3>
<small>I can put more info in, too!</small>
</app-child>
</div>
查看演示.
您也可以在 Slots 中使用默认内容。 如果在 Slot 本身中,而不是编写 <slot></slot>
,您可以使用以下内容填充它:
<slot>我是一些默认文本</slot>
在您使用其他内容填充 Slot 之前,将使用该默认文本,这非常有用! 高五。
您还可以使用命名 Slots。 如果要在组件中使用两个 Slots,可以通过添加名称属性 <slot name="headerinfo"></slot>
来区分它们,我们可以通过编写 <h1 slot="headerinfo">我将填充 headerinfo Slot!</h1>
来访问该特定 Slot。 这非常有用。 如果您有多个已命名的 Slot 和一个未命名的 Slot,Vue 会将命名内容放入命名 Slot 中,其余内容将用于填充剩余的未命名 Slot。
以下是一个示例,说明我的意思。
这是一个示例子模板。
<div id="post">
<main>
<slot name="header"></slot>
<slot></slot>
</main>
</div>
这是父组件的示例。
<app-post>
<h1 slot="header">This is the main title</h1>
<p>I will go in the unnamed slot!</p>
</app-post>
渲染后的内容。
<main>
<h1>This is the main title</h1>
<p>I will go in the unnamed slot!</p>
</main>
就我个人而言,如果我一次使用多个 Slot,我会为所有 Slot 命名,以便其他维护人员清楚地了解内容放置的位置,但 Vue 提供如此灵活的 API 也是一件好事。
Slots 示例
或者,我们可以为不同的组件分配特定的样式,并将所有内容保留在同一组件中,从而快速轻松地更改某些内容的外观。 在下面的葡萄酒标签制作器中,其中一个按钮将根据用户选择切换组件和颜色,瓶子和标签以及文本的背景都会切换,同时保持内容稳定。
const app = new Vue({
...
components: {
'appBlack': {
template: '#black'
}
}
});
主 Vue 应用程序 HTML
<component :is="selected">
...
<path class="label" d="M12,295.9s56.5,5,137.6,0V409S78.1,423.6,12,409Z" transform="translate(-12 -13.8)" :style="{ fill: labelColor }"/>
...
</component>
<h4>Color</h4>
<button @click="selected ='appBlack', labelColor = '#000000'">Black Label</button>
<button @click="selected ='appWhite', labelColor = '#ffffff'">White Label</button>
<input type="color" v-model="labelColor" defaultValue="#ff0000">
白色组件 HTML
<script type="text/x-template" id="white">
<div class="white">
<slot></slot>
</div>
</script>
(这是一个更大的演示,因此如果您在单独的窗口/选项卡中使用它,可能会更有意义)
查看演示.

现在,我们将所有 SVG 图像数据放在主应用程序中,但它实际上放置在每个组件中的 <slot>
内。 这使我们能够切换内容片段或根据用法以不同的方式设置样式,这是一个非常好的功能。 您可以看到,我们通过创建一个更改组件“selected”值的按钮,允许用户决定他们将使用哪个组件。
现在,我们将所有内容都放在一个 Slot 中,但我们也可以使用多个 Slots,如果需要,可以通过命名来区分它们。
<!-- main vue app instance -->
<app-comment>
<p slot="comment">{{ comment.text }}</p>
</app-comment>
<!-- individual component -->
<script type="text/x-template" id="comment-template">
<div>
<slot name="comment"></slot>
</div>
</script>
我们可以轻松地在具有相同引用 Slots 的不同组件之间切换,但是当我们想要能够来回切换但保留每个组件的各个状态时会发生什么? 目前,当我们在黑色和白色之间切换时,模板会切换,内容保持不变。 但是,也许我们遇到了这样的情况:我们希望黑色标签与白色标签完全不同。 您可以使用一个名为 <keep-alive></keep-alive>
的特殊组件将其包装起来,该组件会在您切换时保留状态。
看看上面示例的偏差——创建一个黑色标签,然后创建一个不同的白色标签,并在它们之间切换。你会看到每个标签的状态都保留了下来,并且彼此不同。
<keep-alive>
<component :is="selected">
...
</component>
</keep-alive>

查看 Sarah Drasner 在 CodePen 上发布的笔 Vue Wine Label Maker- with keep-alive(@sdras)。
我非常喜欢 API 的这个功能。
这一切都很好,但为了简单起见,我们一直将所有内容都放在一两个文件中。如果我们能够将组件分离到不同的文件中,并在需要时导入它们,那么在构建网站时,组织结构会更好,而且这确实是 Vue 中真正的开发方式,所以我们接下来就来了解一下。在下一部分中,我们将讨论 Vue-cli、构建流程以及用于状态管理的 Vuex!
我正式被 Vue.js 迷住了。
简单、简洁、易于理解、不碍事、速度快、文档写得很好……
对吧?!
顺便说一句,感谢 Sarah 的系列文章。
在我看来,Vue 确实像是 THE 后起之秀——是对那些笨重、充满样板代码、配置一天却只能被范式束缚的框架的一种令人耳目一新的改变……
也许我有点夸张了?
Sarah,你太棒了!
这些教程读起来就像黄油一样顺滑。谢谢!
太棒了!很高兴它对您有用!
很棒的文章,Sarah!
之前一直在使用 VueJS,并在生产环境中使用过它。
不过,在这篇文章中,我了解到了非常酷的
<keep-alive>
标签,以及“默认内容”和“命名”的slots
.. 太酷了。开始重构吧!
鼓舞人心的文章——谢谢!
也许您想使用 而不是 来包装模板代码。
https://mdn.org.cn/en-US/docs/Web/HTML/Element/template
感谢您的概述!
这是在线演示
http://cody1991.github.io/learn/Vue2/tutorial/intro-to-vue/demo/dist/index.html#/wine
这是我的在线源代码
https://github.com/cody1991/learn/blob/gh-pages/Vue2/tutorial/intro-to-vue/demo/src/components/part-2-wine-label-maker.vue
完成葡萄酒演示后,我将
放在组件周围,但它就像之前一样,没有保存每个状态.. 为什么?
谢谢。
嗨,喜欢这篇文章。只是在第一个组件示例中发现了一个错别字——
{{ text }}
缺少关闭斜杠“/”。