今天我们来分享一个Vue开发知识点:Vue插槽综合指南。
组件是现代 Web 应用程序开发的核心。每个应用程序都由许多组件组成,这些组件平滑地拼接在一起,以便作为一个整体工作。这些组件需要最大程度地灵活和可重用,以允许在不同的情况下甚至在不同的应用程序中使用它们。许多框架用来满足这些要求的主要机制之一——尤其是 Vue——被称为“插槽”。
插槽是一种功能强大且通用的内容分发和组合机制。您可以将槽视为可定制的模板(例如,类似于 PHP 模板),您可以在不同的地方使用它们,针对不同的用例,产生不同的效果。例如,在像Vuetify这样的 UI 框架中,槽用于制作通用组件,例如警报组件【https://vuetifyjs.com/en/api/v-alert/#slots】。在这些类型的组件中,插槽用作默认内容和任何附加/可选内容(例如图标、图像等)的占位符。
插槽允许您向特定组件添加任何结构、样式和功能。通过使用插槽,开发人员可以大大减少单个组件中使用的 props 数量,使组件更加干净和易于管理。
在本教程中,我们将探索如何在 Vue 3 的上下文中利用插槽的力量。让我们开始吧。
插槽的基本用法
基本上,Vue 提供两种插槽:简单插槽和作用域插槽。让我们从简单的开始。考虑以下示例:
const app = Vue.createApp({})app.component('primary-button', { template: ` <button> <slot>OK</slot> </button>`})app.mount('#app')
在这里,我们有一个主要的按钮组件。我们希望按钮的文本是可定制的,因此我们使用slot
元素内的button
组件为文本添加占位符。我们还需要一个默认(后备)通用值,以防我们不提供自定义值。Vue 使用我们放入slot
组件中的所有内容作为默认插槽内容。所以我们只是将文本“OK”放在组件中。现在我们可以像这样使用组件:
<div id="app"> <primary-button></primary-button> </div>
JS代码:
const app = Vue.createApp({}) app.component('primary-button', { template: ` <button> <slot>OK</slot> </button>` }) app.mount('#app')
结果是一个带有文本“OK”的按钮,因为我们没有提供任何值。但是如果我们想创建一个带有自定义文本的按钮怎么办?在这种情况下,我们在组件实现中提供自定义文本,如下所示:
<div id="app"> <primary-button>Subscribe</primary-button></div>
在这里,Vue 采用自定义的“订阅”文本并使用它而不是默认文本。
正如你所看到的,即使在这个简单的例子中,我们在如何呈现我们的组件方面也获得了很大的灵活性。但这只是冰山一角。让我们看一个更复杂的例子。
构建每日报价组件
现在,我们将构建一个显示当天报价的报价组件。这是代码:
const app = Vue.createApp({}) app.component('quote', { template: ` <article> <h2>The quote of the day says:</h2> <p class="quote-text"> <slot></slot> </p> </article>`})app.mount('#app')
<div id="app"> <quote> <div class="quote-box"> "Creativity is just connecting things." <br><br> - Steve Jobs </div> </quote></div>
.quote-box { background-color: lightgreen; width: 300px; padding: 5px 10px;}.quote-text { font-style: italic;}
在这个例子中,我们创建了一个内容不变的标题标题,然后我们在一个段落中放置了一个槽组件,其内容将根据当天的引用而变化。当组件被渲染时,Vue 将显示来自引用组件的标题,然后是我们放在引用标签中的内容。还要注意引用创建和实现中使用的 CSS 类。我们可以根据需要以两种方式设置组件的样式。
DEMO:
<div id="app"> <quote> <div class="quote-box"> "Creativity is just connecting things." <br><br> - Steve Jobs </div> </quote> </div> //css .quote-box { background-color: lightgreen; width: 300px; padding: 5px 10px; } .quote-text { font-style: italic; } //js const app = Vue.createApp({}) app.component('quote', { template: ` <article> <h2>The quote of the day says:</h2> <p class="quote-text"> <slot></slot> </p> </article>` })
我们的当天报价组件工作正常,但我们仍然需要手动更新报价。让我们使用Fav Quotes API使其动态化:
<div id="app"> <quote> <button v-if="show == false" @click="showQuote">Show Quote of the Day</button> <div v-if="show" class="quote-box"> {{ quoteOfTheDay.quote.body }} <br><br> - {{ quoteOfTheDay.quote.author }} </div> </quote> </div> const app = Vue.createApp({ data() { return { quoteOfTheDay: null, show: false }; }, methods: { showQuote() { axios.get('https://favqs.com/api/qotd').then(result => { this.quoteOfTheDay = result.data this.show = true }); } } }) ... app.mount('#app')
在这里,我们使用Axios调用“每日报价”API 端点,然后使用返回的 JSON 对象中的body
和author
属性来填充报价。所以我们不再需要手动添加报价;它是自动为我们完成的。
<div id="app" v-cloak> <quote> <button v-if="show == false" @click="showQuote">Show Quote of the Day</button> <div v-if="show" class="quote-box"> {{ quoteOfTheDay.quote.body }} <br><br> - {{ quoteOfTheDay.quote.author }} </div> </quote> </div> .quote-box { background-color: lightgreen; width: 300px; padding: 5px 10px; } .quote-text { font-style: italic; } [v-cloak] { display: none; } const app = Vue.createApp({ data() { return { quoteOfTheDay: null, show: false }; }, methods: { showQuote() { axios.get('https://favqs.com/api/qotd').then(result => { this.quoteOfTheDay = result.data this.show = true }); } } }) app.component('quote', { template: ` <article> <h2>The quote of the day says:</h2> <p class="quote-text"> <slot></slot> </p> </article>` }) app.mount('#app')
使用多个插槽
尽管单个插槽可以非常强大,但在许多情况下这还不够。在实际场景中,我们通常需要多个插槽来完成这项工作。幸运的是,Vue 允许我们根据需要使用任意数量的插槽。让我们看看如何通过构建一个简单的卡片组件来使用多个插槽。
构建基本卡片组件
我们将构建一个包含三个部分的卡片组件:页眉、正文和页脚:
const app = Vue.createApp({}) app.component('card', { template: ` <div class="container"> <header> <slot name="header"></slot> </header> <main> <slot></slot> </main> <footer> <slot name="footer"></slot> </footer> </div>` }) app.mount('#app')
<div id="app"> <card> <template v-slot:header> <h2>Card Header Title</h2> </template> <template v-slot:default> <p> Lorem ipsum leo risus, porta ac consectetur ac, vestibulum at eros. Donec id elit non mi porta gravida at eget metus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras mattis consectetur purus sit amet fermentum. </p> </template> <template v-slot:footer> <a href="#">Save</a> - <a href="#">Edit</a> - <a href="#">Delete</a> </template> </card> </div>
为了使用多个插槽,我们必须为每个插槽提供一个名称。唯一的例外是默认插槽。因此,在上面的示例中,我们name
为页眉和页脚插槽添加了一个属性。未提供名称的插槽被视为默认插槽。
当我们使用card
组件时,我们需要使用带有插槽名称指令的template
元素v-slot
:v-slot:[slot-name]
。
<div id="app"> <card> <template v-slot:header> <h2>Card Header Title</h2> </template> <template v-slot:default> <p> Lorem ipsum leo risus, porta ac consectetur ac, vestibulum at eros. Donec id elit non mi porta gravida at eget metus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras mattis consectetur purus sit amet fermentum. </p> </template> <template v-slot:footer> <a href="#">Save</a> - <a href="#">Edit</a> - <a href="#">Delete</a> </template> </card> </div> //css .container { width: 300px; } //js const app = Vue.createApp({}) app.component('card', { template: ` <div class="container"> <header> <slot name="header"></slot> </header> <main> <slot></slot> </main> <footer> <slot name="footer"></slot> </footer> </div>` }) app.mount('#app')
注意:该v-slot
指令有一个简写,它使用特殊符号#
后跟插槽名称。因此,例如v-slot:header
,我们可以编写,而不是#header
。
命名插槽也可以与第三方组件一起使用,我们将在下一节中看到。
在布尔玛的卡片组件中使用命名插槽
让我们使用Bulma 的 Card 组件并稍微调整一下:
<div id="app"> <card> <template v-slot:header> <p class="card-header-title"> Card Header Title </p> </template> <template v-slot:default> <p> Lorem ipsum leo risus, porta ac consectetur ac, vestibulum at eros. Donec id elit non mi porta gravida at eget metus. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Cras mattis consectetur purus sit amet fermentum. </p> </template> <template v-slot:footer> <a href="#" class="card-footer-item">Save</a> <a href="#" class="card-footer-item">Edit</a> <a href="#" class="card-footer-item">Delete</a> </template> </card> </div> //css .container { width: 300px; } //js const app = Vue.createApp({}) app.component('card', { template: ` <div class="container"> <div class="card"> <header class="card-header"> <slot name="header"></slot> </header> <main class="card-content"> <slot></slot> </main> <footer class="card-footer"> <slot name="footer"></slot> </footer> </div> </div>` }) app.mount('#app')
在这里,我们使用 Bulma Card 组件中的类作为基础骨架,并为每个部分(页眉、内容、页脚)添加一个插槽。然后,当我们添加内容时,一切都被正确地构建。
使用作用域插槽
我们通过使用多个插槽向前迈出了一大步,但真正的马力来自于作用域插槽。它们允许父级访问子级数据,这为我们提供了很多机会。让我们在实践中看看它们。
构建多用途列表组件
为了展示作用域插槽的强大功能,我们将构建一个多用途列表组件,当我们需要列出一些数据时,该组件可用于不同的场景:
const app = Vue.createApp({ data() { return { tasks: ['Reading a book', 'Buying vegetables', 'Going for a walk'] } }})app.component('list', { props: ['items', 'name'], template: ` <h3>{{ name }}</h3> <ul> <li v-for="item in items"> <slot :item="item"></slot> </li> </ul>`})app.mount('#app')
<div id="app"> <list name="My tasks list:" :items='tasks' v-slot="{ item: task }"> <span>{{ task }}</span> </list></div>
在这里,我们list
使用items
和name
道具创建了一个组件。在name
将被用来提供一个标题列表和items
将举行,我们希望列表中的数据。在模板中,我们name
向列表上方的标题添加了一个道具,然后我们使用v-for
指令来呈现每个项目。为了将子项的数据公开给父项,我们将项目绑定为插槽属性 ( <slot :item="item"></slot>
)。
在父级中,我们有一系列任务。当我们使用列表时,我们提供了一个名称道具,然后将它绑定items
到tasks
数组,然后我们使用它v-slot
来访问子数据。
注意:slot 中所有绑定的 props 都称为 slot props,我们可以通过 using 暴露它们v-slot:[slot-name]="slotProps"
,然后我们像这样使用单个 props: {{ slotProps.item }}
。但是在这个例子中,我使用了object destructuring,这是一种更优雅、更直接的获取对象属性的方式。它还允许您重命名对象属性(就像我所做的那样,重命名item
为task
),这对于不同类型的列表更加灵活。
我们的列表组件的目的是用于不同的场景。因此,假设我们想将其用作购物清单。这是如何做到的:
const app = Vue.createApp({ data() { return { products: [ {name: 'Tomatoes', quantity: '4'}, {name: 'Cucumbers', quantity: '2'}, {name: 'Red onion', quantity: '1'}, ] } }})...app.mount('#app')
<div id="app"> <list name="My shopping list:" :items='products' v-slot="{ item: product }"> <span>{{ product.quantity }} {{ product.name }}</span> </list></div>
在这里,我们有一个产品列表,其中每个项目都有一个name
和一个quantity
属性。list
除了这里的列表项有两个属性之外,我们使用的组件几乎与前面的示例相同。
如您所见,列表组件可以轻松适应不同的列表用例。
老虎机与道具
在了解插槽(并实现其功能)之前,许多开发人员主要使用 props 进行内容分发。但是当我们有一个复杂的组件时,道具的数量会急剧增加。在这种情况下,插槽可以代替道具的使用,使组件实现更加清晰。
为了说明上面的观点,我们将以Tailwind CSS 中的一个例子为例,它只使用 props 来创建一个vacation-card
组件:
const app = Vue.createApp({})app.component('vacation-card', { props: ['url', 'img', 'imgAlt', 'eyebrow', 'title', 'pricing'], template: ` <div class="m-5 shadow-md w-80"> <img class="rounded" :src="img" :alt="imgAlt"> <div class="p-2"> <div> <div class="text-xs text-gray-600 uppercase font-bold">{{ eyebrow }}</div> <div class="font-bold text-gray-700 leading-snug"> <a :href="url" class="hover:underline">{{ title }}</a> </div> <div class="mt-2 text-sm text-gray-600">{{ pricing }}</div> </div> </div> </div>`})app.mount('#app')
<div id="app"> <vacation-card url="/vacations/cancun" img="https://images.unsplash.com/photo-1452784444945-3f422708fe5e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=512&q=80" imgAlt="Beach in Cancun" eyebrow="Private Villa" title="Relaxing All-Inclusive Resort in Cancun" pricing="$299 USD per night" > </vacation-card></div>
如您所见,为了提供所有需要的数据/内容,该vacation-card
组件使用了六个道具。这绝对使它可重用,但也难以维护。我们可以通过使用 slot 而不是一些 props 来生成一个更清晰和可维护的版本。
让我们用插槽重写组件:
const app = Vue.createApp({})app.component('vacation-card', { props: ['url', 'img', 'imgAlt'], template: ` <div class="m-5 shadow-md w-80"> <img class="rounded" :src="img" :alt="imgAlt"> <div class="p-2"> <div> <div class="text-xs text-gray-600 uppercase font-bold"><slot name="eyebrow"></slot></div> <div class="font-bold text-gray-700 leading-snug"> <a :href="url" class="hover:underline"><slot name="title"></slot></a> </div> <div class="mt-2 text-sm text-gray-600"><slot name="pricing"></slot></div> </div> </div> </div>`})app.mount('#app')
<div id="app"> <vacation-card url="/vacations/cancun" img="https://images.unsplash.com/photo-1452784444945-3f422708fe5e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=512&q=80" imgAlt="Beach in Cancun" > <template v-slot:eyebrow> <p>Private Villa</p> </template> <template v-slot:title> <p>Relaxing All-Inclusive Resort in Cancun</p> </template> <template v-slot:pricing> <p><mark>$299 USD per night</mark></p> </template> </vacation-card></div>
看个DEMO:
<div id="app"> <vacation-card url="/vacations/cancun" img="https://images.unsplash.com/photo-1452784444945-3f422708fe5e?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=512&q=80" imgAlt="Beach in Cancun" > <template v-slot:eyebrow> <p>Private Villa</p> </template> <template v-slot:title> <p>Relaxing All-Inclusive Resort in Cancun</p> </template> <template v-slot:pricing> <p><mark>$299 USD per night</mark></p> </template> </vacation-card> </div> //js const app = Vue.createApp({}) app.component('vacation-card', { props: ['url', 'img', 'imgAlt'], template: ` <div class="m-5 shadow-md w-80"> <img class="rounded" :src="img" :alt="imgAlt"> <div class="p-2"> <div> <div class="text-xs text-gray-600 uppercase font-bold"><slot name="eyebrow"></slot></div> <div class="font-bold text-gray-700 leading-snug"> <a :href="url" class="hover:underline"><slot name="title"></slot></a> </div> <div class="mt-2 text-sm text-gray-600"><slot name="pricing"></slot></div> </div> </div> </div>` }) app.mount('#app')
在这里,我们将道具减少到三个。我们只为元数据保留道具,并为实际内容使用命名槽,我认为这更合乎逻辑。
以下是我对使用 props 与 slot 的一般考虑。
在以下情况下使用道具:
父组件只能将数据向下传递给子组件
父组件无法控制数据的呈现方式,也无法自定义子组件
你有一个明确的设计,变量的数量很少,组件很简单
您需要提供元数据或某种配置
在以下情况下使用插槽:
您需要将数据从孩子传递给父母
父级可以决定数据将如何呈现
您不仅要传递数据,还要传递复杂的 HTML 标记、特定功能,甚至其他组件
您希望从父组件自定义子组件的多功能性
需要更大的灵活性来定制组件,组件很大,涉及的变量很多
底线:为了获得最佳效果,结合道具和插槽!
探索更多插槽用例
在这个阶段,您应该会看到插槽的强大功能和灵活性,但还有更多有用的方法来使用它们。现在让我们探索它们。
使用插槽重用功能
插槽不仅可以提供内容/结构,还可以提供功能。让我们看看这个:
const app = Vue.createApp({ data() { return { counter: 1 } }, methods: { increment() { this.counter++ } }})app.component('double-counter', { props: ['counter'], template: ` <p> <slot :double="double"></slot> </p>`, computed: { double() { return this.counter * 2 } }})app.mount('#app')
<div id="app"> <double-counter :counter="counter"> <template v-slot:default="{ double }"> <h2>{{ counter }} x 2 = {{ double }}</h2> <button @click="increment"> Increment counter by 1 </button> </template> </double-counter></div>
在这里,我们创建了一个double-counter
将计数器值加倍的组件。为此,我们创建了一个计算属性double
,通过将其值绑定为槽属性来向父级公开该属性。计算器采用counter
道具的值并将其加倍。
在父级中,我们有一个counter
数据属性和一个将increment()
其加一的方法。当我们使用 时double-counter
,我们绑定了 prop counter
,然后我们公开了double
计算属性。在表达式中,我们使用counter
and double
。当我们单击按钮时,counter
增加 1 并且计算属性使用新的加倍值重新计算。例如,如果counter
prop 设置为 3,则加倍的值将是 6 ( 3 x 2 = 6
)。
看个DEMO:
<div id="app"> <double-counter :counter="counter"> <template v-slot:default="{ double }"> <h2>{{ counter }} x 2 = {{ double }}</h2> <button @click="increment"> Increment counter by 1 </button> </template> </double-counter> </div> //js const app = Vue.createApp({ data() { return { counter: 1 } }, methods: { increment() { this.counter++ } } }) app.component('double-counter', { props: ['counter'], template: ` <p> <slot :double="double"></slot> </p>`, computed: { double() { return this.counter * 2 } } }) app.mount('#app')
我们可以让这个组件更加灵活。让我们调整它以将计数器乘以任何自定义值:
const app = Vue.createApp({ data() { return { counter: 1, by: 4 } }, methods: { increment() { this.counter++ } }})app.component('multiply-counter', { props: ['counter', 'by'], template: ` <p> <slot :multiply="multiply"></slot> </p>`, computed: { multiply() { return this.counter * this.by } }})app.mount('#app')
<div id="app"> <multiply-counter :by="by" :counter="counter"> <template v-slot:default="{ multiply }"> <h2>{{ counter }} x {{ by }} = {{ multiply }}</h2> <button @click="increment"> Increment counter by 1 </button> </template> </multiply-counter></div>
在这里,我们添加了一个 prop, by
,它设置了乘法数,并将double
计算属性更改为multiply
,它将 乘以counter
给定的数。
在父级中,我们添加by
data 属性并将其绑定到by
prop。所以现在,如果我们将by
data 属性设置为 3 并且设置counter
为 4,结果将是4 x 3 = 12
。
看个DEMO:
<div id="app"> <multiply-counter :by="by" :counter="counter"> <template v-slot:default="{ multiply }"> <h2>{{ counter }} x {{ by }} = {{ multiply }}</h2> <button @click="increment"> Increment counter by 1 </button> </template> </multiply-counter> </div> //js const app = Vue.createApp({ data() { return { counter: 1, by: 4 } }, methods: { increment() { this.counter++ } } }) app.component('multiply-counter', { props: ['counter', 'by'], template: ` <p> <slot :multiply="multiply"></slot> </p>`, computed: { multiply() { return this.counter * this.by } } }) app.mount('#app')
在无渲染组件中使用插槽
使用插槽的另一种强大方法是将它们放在无渲染组件中。无渲染组件没有template
元素。它有一个渲染函数,可以暴露单个作用域插槽。让我们创建一个多用途列表的无渲染版本:
const app = Vue.createApp({ data() { return { products: [ {name: 'Tomatoes', quantity: '4'}, {name: 'Cucumbers', quantity: '2'}, {name: 'Red onion', quantity: '1'}, ] } } }) app.component('renderless-list', { props: ['items', 'name'], render() { return this.$slots.default({ items: this.items, name: this.name }); } }) app.mount('#app')
<div id="app"> <renderless-list name="My shopping list:" :items="products"> <template v-slot:default="{name, items: products}"> <h3>{{ name }}</h3> <ul> <li v-for="product in products" :key="product.name"> {{ product.quantity }} {{ product.name }} </li> </ul> </template> </renderless-list> </div>
在这里,我们创建了一个renderless-list
带name
和items
道具的组件。它还在渲染函数中公开了一个作用域插槽。
然后,在父级中,我们以与多用途列表组件类似的方式使用它——除了这一次,内容结构在父级中定义,这为我们提供了更大的灵活性,我们将在下一个示例中看到。
注意:在 Vue 3 中,this.$scopedSlots
已删除并this.$slots
改为使用。此外,this.$slots
将插槽公开为函数。有关更多信息,请参阅插槽统一。
这个组件的真正力量在于我们不受限制如何呈现内容。让我们看看呈现与表格相同的内容是多么容易:
<div id="app"> <renderless-list name="My shopping list:" :items="products"> <template v-slot:default="{name, items: products}"> <h3>{{ name }}</h3> <table> <tr> <th>Quantity</th> <th>Product</th> </tr> <tr v-for="product in products" :key="product.name"> <td>{{ product.quantity }}</td> <td>{{ product.name }}</td> </tr> </table> </template> </renderless-list></div>
table, th, td { border: 1px solid black; border-collapse: collapse; padding: 5px;}
在这里,我们使用相同的功能,但我们定义了不同的内容结构。
<div id="app"> <renderless-list name="My shopping list:" :items="products"> <template v-slot:default="{name, items: products}"> <h3>{{ name }}</h3> <table> <tr> <th>Quantity</th> <th>Product</th> </tr> <tr v-for="product in products" :key="product.name"> <td>{{ product.quantity }}</td> <td>{{ product.name }}</td> </tr> </table> </template> </renderless-list> </div> //css table, th, td { border: 1px solid black; border-collapse: collapse; padding: 5px; } //js const app = Vue.createApp({ data() { return { products: [ {name: 'Tomatoes', quantity: '4'}, {name: 'Cucumbers', quantity: '2'}, {name: 'Red onion', quantity: '1'}, ] } } }) app.component('renderless-list', { props: ['items', 'name'], render() { return this.$slots.default({ items: this.items, name: this.name }); } }) app.mount('#app')
结论
这就是插槽的所有功能和灵活性。正如我们所见,它们可用于各种用例,这使我们能够生产高度可重用且用途广泛的组件。当与 props 结合使用时,slots 为我们提供了创建复杂组件和应用程序所需的一切。
网友评论文明上网理性发言 已有1人参与
发表评论:
评论列表