框架实战指南-传递子组件

76 阅读6分钟

本文是系列文章的一部分:框架实战指南 - 基础知识

正如我们之前提到的,在 DOM 中,HTML 元素彼此之间存在关系

例如,以下内容:

<div>	<ul>		<li>One</li>		<li>Two</li>		<li>Three</li>	</ul></div>

将构建以下 DOM 树:

image.png 这就是 DOM 如何将节点构造为父节点和子节点。请注意, 是如何<li>明显地位于<ul>标签下方的,而不是像下面这样的语法:

<!-- This isn't correct HTML to do what we want -->
<div	ul="        li='One'        li='Two'        li='Three'    "/>

虽然上面的内容看起来很奇怪而且违反直觉,但让我们看看如果每个元素都是一个专用组件,我们如何使用迄今为止创建的方法定义相同的列表:

<!-- ListItem.vue -->
<script setup>const props = defineProps(["name"]);</script><template>	<li>{{ props.name }}</li></template>
<!-- List.vue --><script setup>import ListItem from "./ListItem.vue";</script><template>	<ul>		<ListItem name="One" />		<ListItem name="Two" />		<ListItem name="Three" />	</ul></template>
<!-- Container.vue --><script setup>import List from "./List.vue";</script><template>	<div>		<List />	</div></template>

这和那个奇怪的伪嵌套 HTML 语法非常相似。另一种更接近 DOM 的组件使用语法可能如下所示:

<Component>	<OtherComponent /></Component>

出现这种不匹配的原因是,如果我们看一下组件的定义方式,我们会深入地而不是广泛地构建我们以前的组件。

image.png

这就是仅使用 HTML 构建应用程序和使用前端框架构建应用程序之间的区别;虽然 DOM 通常被认为是一维的,但实际上有两个维度,通过框架以更细粒度的方式构建此树的能力,它们可以更彻底地暴露出来。

让我们使用一个听起来很熟悉的功能将组件树移回广度优先:传递子项。

通过基础组件

在我们探索使用框架传递子项之前,让我们首先考虑一下我们想要这样做的潜在用例。

例如,假设您希望button每次点击时都产生“按下”效果。然后,当您再次点击它时,它会取消点击。这可能看起来像这样:

<!-- ToggleButton.vue --><script setup>import { ref } from "vue";const pressed = ref(false);const props = defineProps(["text"]);function togglePressed() {	pressed.value = !pressed.value;}</script><template>	<button		@click="togglePressed()"		:style="			pressed				? 'background-color: black; color: white'				: 'background-color: white; color: black'		"		type="button"		:aria-pressed="pressed"	>		{{ props.text || "Test" }}	</button></template>
<!-- ToggleButtonList.vue --><script setup>import ToggleButton from "./ToggleButton.vue";</script><template>	<ToggleButton text="Hello world!" />	<ToggleButton text="Hello other friends!" /></template>

这里,我们传入text一个字符串属性来赋值文本。但是不行!如果我们想span在 里面添加 来button添加粗体文本怎么办?毕竟,如果传入Hello, <span>world</span>!,它不会渲染span,而是将 渲染<span>为文本。

image.png

相反,让我们允许父级ToggleButton传递一个模板,然后将其呈现到组件中

在 Vue-land 中,该slot标签用于将子项传递到组件的模板。

<!-- ToggleButton.vue --><script setup>import { ref } from "vue";const pressed = ref(false);function togglePressed() {	pressed.value = !pressed.value;}</script><template>	<button		@click="togglePressed()"		:style="			pressed				? 'background-color: black; color: white'				: 'background-color: white; color: black'		"		type="button"		:aria-pressed="pressed"	>		<slot></slot>	</button></template>
<!-- ToggleButtonList.vue --><script setup>import ToggleButton from "./ToggleButton.vue";</script><template>	<ToggleButton>		Hello <span style="font-weight: bold">world</span>!	</ToggleButton>	<ToggleButton>Hello other friends!</ToggleButton></template>

因为slot它是 Vue 的内置组件,所以我们不需要从vue包中导入它。

在这里,我们可以将span和其他元素作为子元素ToggleButton直接传递给我们的组件。**

将其他框架功能与组件子项一起使用

然而,由于这些模板拥有框架的全部功能,这些子模板也拥有了超能力!让我们for在子模板中添加一个循环,向所有的朋友问好:

<!-- ToggleButtonList.vue --><script setup>import ToggleButton from "./ToggleButton.vue";import RainbowExclamationMark from "./RainbowExclamationMark.vue";const friends = ["Kevin,", "Evelyn,", "and James"];</script><template>	<ToggleButton		>Hello <span v-for="friend of friends">{{ friend }} </span>!</ToggleButton	>	<ToggleButton		>Hello other friends		<RainbowExclamationMark />	</ToggleButton></template>
<!-- RainbowExclamationMark.vue --><template>	<span>!</span></template><!-- "scoped" means the styles only apply to this component --><style scoped>span {	font-size: 3rem;	background: linear-gradient(		180deg,		#fe0000 16.66%,		#fd8c00 16.66%,		33.32%,		#ffe500 33.32%,		49.98%,		#119f0b 49.98%,		66.64%,		#0644b3 66.64%,		83.3%,		#c22edc 83.3%	);	background-size: 100%;	-webkit-background-clip: text;	-webkit-text-fill-color: transparent;	-moz-background-clip: text;}</style>

正如您所见,我们可以使用我们内部的任何功能children- 甚至是其他组件!

感谢 Sarah Fossheim 提供的指南,教您如何添加像感叹号一样的剪切背景文本!

命名子组件

虽然传递一组元素本身很有用,但许多组件要求有多个可以传递的数据“槽”。

例如,以下拉组件为例:

让我们构建这个下拉组件

这个下拉组件有两个潜在的地方,传递元素会很有帮助:

<Dropdown>	<DropdownHeader>Let's build this dropdown component</DropdownHeader>	<DropdownBody>		These tend to be useful for FAQ pages, hidden contents, and more!	</DropdownBody></Dropdown>

让我们使用“命名子项”通过与上述类似的 API 来构建此组件。

与 Angular 的查询工作方式类似,Vue 允许您将组件ng-content[select]传递给投影和命名的内容。name``slot

<!-- Dropdown.vue --><script setup>const props = defineProps(["expanded"]);const emit = defineEmits(["toggle"]);</script><template>	<button		@click="emit('toggle')"		:aria-expanded="expanded"		aria-controls="dropdown-contents"	>		{{ props.expanded ? "V" : ">" }} <slot name="header" />	</button>	<div id="dropdown-contents" role="region" :hidden="!props.expanded">		<slot />	</div></template>
<!-- App.vue --><script setup>import { ref } from "vue";import Dropdown from "./Dropdown.vue";const expanded = ref(false);</script><template>	<Dropdown :expanded="expanded" @toggle="expanded = !expanded">		<template v-slot:header>Let's build this dropdown component</template>		These tend to be useful for FAQ pages, hidden contents, and more!	</Dropdown></template>

这里我们可以看到slot正在查询一个模板槽。的标题元素模板header可以满足此查询。App``template

v-slot也有一个简写形式#,类似于 的v-bind简写形式:。使用这个简写形式,我们可以将App组件修改为如下所示:

<!-- App.vue --><script setup>import { ref } from "vue";import Dropdown from "./Dropdown.vue";const expanded = ref(false);</script><template>	<Dropdown :expanded="expanded" @toggle="expanded = !expanded">		<template #header>Let's build this dropdown component</template>		These tend to be useful for FAQ pages, hidden contents, and more!	</Dropdown></template>

此下拉组件的简单版本实际上已作为 HTML 标签内置于浏览器中<details><summary>我们自己构建此组件主要是为了学习,仅供实验之用。在实际生产环境中,强烈建议使用这些内置元素。

使用传递的子项构建表

现在我们已经熟悉了如何将子项传递给组件,让我们将其应用到我们为文件托管应用程序构建的组件之一:我们的文件“列表”。

image.png

虽然这确实<ul>构成了一个文件列表,但实际上数据有两个维度:向下和向右。这使得这个“列表”实际上更像是一个“表格”。因此,将无序列表 ( ) 和列表项 ( <li>) 元素用于这个特定的 UI 元素实际上有点误解。

让我们先创建一个 HTMLtable元素。一个普通的 HTML 表格可能看起来像这样:

<table>	<thead>		<tr>			<th>Name</th>		</tr>	</thead>	<tbody>		<tr>			<td>Movies</td>		</tr>	</tbody></table>

其中th充当标题数据项,并td充当给定行和列上的数据位。

让我们重构文件列表以使用这个 DOM 布局:

<!-- File.vue --><script setup>import { ref } from "vue";import FileDate from "./FileDate.vue";const props = defineProps(["fileName", "href", "isSelected", "isFolder"]);const emit = defineEmits(["selected"]);const inputDate = ref(new Date());// ...</script><template>	<tr		@click="emit('selected')"		:aria-selected="props.isSelected"		:style="			props.isSelected				? { backgroundColor: 'blue', color: 'white' }				: { backgroundColor: 'white', color: 'blue' }		"	>		<td>			<a :href="props.href" style="color: inherit">{{ props.fileName }}</a>		</td>		<td v-if="props.isFolder">Type: Folder</td>		<td v-else>Type: File</td>		<td><FileDate v-if="!props.isFolder" :inputDate="inputDate" /></td>	</tr></template>
<!-- FileTableBody --><!-- This was previously called "FileList" --><script setup>import File from "./File.vue";// ...const filesArray = [	{		fileName: "File one",		href: "/file/file_one",		isFolder: false,	},	{		fileName: "File two",		href: "/file/file_two",		isFolder: false,	},	{		fileName: "File three",		href: "/file/file_three",		isFolder: false,	},];</script><template>	<tbody>		<template v-for="file in filesArray">			<File				v-if="!file.isFolder"				:fileName="file.fileName"				:href="file.href"				:isFolder="file.isFolder"				:isSelected="false"			/>		</template>	</tbody></template>
<!-- FileTable.vue --><script setup>import FileTableBody from "./FileTableBody.vue";</script><template>	<table>		<FileTableBody />	</table></template>

现在我们有了一个显式FileTable组件,让我们看看是否可以使用替换组件对其进行更多样式设置FileTableContainer,该组件使用传递的子项来设置底层table元素的样式。

<!-- FileTableContainer.vue --><template>	<table		style="			color: #3366ff;			border: 2px solid #3366ff;			border-spacing: 0;			padding: 0.5rem;		"	>		<slot></slot>	</table></template>
<!-- FileTable.vue --><script setup>import FileTableContainer from "./FileTableContainer.vue";import FileTableBody from "./FileTableBody.vue";</script><template>	<FileTableContainer><FileTableBody /></FileTableContainer></template>

挑战

让我们将本章的挑战作为上一节中构建的表格的延续。注意,我们之前的表格只有文件本身,没有文件头。让我们通过添加第二组可以传递的子项来改变这种情况,如下所示:

<table>	<FileHeader />	<FileList /></table>
<!-- FileTableContainer.vue --><template>	<table		style="			color: #3366ff;			border: 2px solid #3366ff;			border-spacing: 0;			padding: 0.5rem;		"	>		<thead>			<slot name="header"></slot>		</thead>		<slot></slot>	</table></template>
<!-- FileTable.vue --><script setup>import FileTableContainer from "./FileTableContainer.vue";import FileTableBody from "./FileTableBody.vue";</script><template>	<FileTableContainer>		<template #header>			<tr>				<th>Name</th>				<th>File Type</th>				<th>Date</th>			</tr>		</template>		<FileTableBody />	</FileTableContainer></template>