跳转到内容

插槽

本页面假设您已经阅读了组件基础知识。如果您对组件是新手,请首先阅读。

插槽内容和出口

我们已经了解到组件可以接受属性,这些属性可以是任何类型的JavaScript值。但是模板内容怎么办?在某些情况下,我们可能想要将一个模板片段传递给子组件,并让子组件在其自己的模板内渲染该片段。

例如,我们可能有一个支持以下使用的<FancyButton>组件

template
<FancyButton>
  Click me! <!-- slot content -->
</FancyButton>

<FancyButton>的模板看起来像这样

template
<button class="fancy-btn">
  <slot></slot> <!-- slot outlet -->
</button>

<slot>元素是一个插槽出口,它指示父组件提供的插槽内容应渲染的位置。

slot diagram

并且最终的渲染DOM

html
<button class="fancy-btn">Click me!</button>

使用插槽,<FancyButton>负责渲染外部的<button>(及其华丽的样式),而内部内容由父组件提供。

另一种理解插槽的方式是通过将其与JavaScript函数进行比较

js
// parent component passing slot content
FancyButton('Click me!')

// FancyButton renders slot content in its own template
function FancyButton(slotContent) {
  return `<button class="fancy-btn">
      ${slotContent}
    </button>`
}

插槽内容不仅限于文本。它可以是以任何有效模板内容。例如,我们可以传递多个元素,甚至其他组件

template
<FancyButton>
  <span style="color:red">Click me!</span>
  <AwesomeIcon name="plus" />
</FancyButton>

通过使用插槽,我们的<FancyButton>变得更加灵活和可重用。现在我们可以在不同的位置使用它,并且具有不同的内部内容,但所有这些都具有相同的时尚样式。

Vue组件的插槽机制灵感来源于原生Web组件<slot>元素,但它具有我们稍后将要看到的附加功能。

渲染范围

插槽内容可以访问父组件的数据作用域,因为它是在父组件中定义的。例如

template
<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>

这里两个{{ message }}插值将渲染相同的内容。

插槽内容无法访问子组件的数据。Vue模板中的表达式只能访问其定义的作用域,这与JavaScript的词法作用域一致。换句话说

父模板中的表达式仅能访问父作用域;子模板中的表达式仅能访问子作用域。

备用内容

有时为插槽指定备用(即默认)内容非常有用,以便在没有提供内容时进行渲染。例如,在<SubmitButton>组件中

template
<button type="submit">
  <slot></slot>
</button>

我们可能希望文本“提交”在父组件没有提供任何插槽内容的情况下渲染在<button>内。要将“提交”作为备用内容,我们可以将其放置在<slot>标签之间

template
<button type="submit">
  <slot>
    Submit <!-- fallback content -->
  </slot>
</button>

现在,当我们在父组件中使用<SubmitButton>,没有为插槽提供内容时

template
<SubmitButton />

这将渲染备用内容,“提交”

html
<button type="submit">Submit</button>

但如果我们提供内容

template
<SubmitButton>Save</SubmitButton>

那么提供的内容将被渲染

html
<button type="submit">Save</button>

命名插槽

有时在单个组件中具有多个插槽出口很有用。例如,在具有以下模板的<BaseLayout>组件中

template
<div class="container">
  <header>
    <!-- We want header content here -->
  </header>
  <main>
    <!-- We want main content here -->
  </main>
  <footer>
    <!-- We want footer content here -->
  </footer>
</div>

在这些情况下,<slot>元素具有一个特殊属性name,可以用来为不同的插槽分配一个唯一的ID,以便确定内容应该渲染的位置

template
<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

没有name<slot>出口隐式具有“默认”名称。

在父组件中使用<BaseLayout>时,我们需要一种方法来传递多个插槽内容片段,每个片段针对不同的插槽出口。这就是命名插槽的作用。

要传递命名插槽,我们需要使用带有v-slot指令的<template>元素,然后传递插槽名称作为v-slot的参数

template
<BaseLayout>
  <template v-slot:header>
    <!-- content for the header slot -->
  </template>
</BaseLayout>

v-slot有一个专门的简写#,所以<template v-slot:header>可以缩短为<template #header>。把它想象成“在子组件的‘header’插槽中渲染此模板片段”。

named slots diagram

以下是使用简写语法将内容传递给所有三个插槽的代码

template
<BaseLayout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <template #default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</BaseLayout>

当一个组件接受默认插槽和命名插槽时,所有顶级非<template>节点都将隐式地视为默认插槽的内容。因此,上面的代码也可以写成

template
<BaseLayout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <!-- implicit default slot -->
  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</BaseLayout>

现在,所有在<template>元素内部的内容都将传递给相应的插槽。最终渲染的HTML将是

html
<div class="container">
  <header>
    <h1>Here might be a page title</h1>
  </header>
  <main>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </main>
  <footer>
    <p>Here's some contact info</p>
  </footer>
</div>

再次,使用JavaScript函数的类比可能有助于您更好地理解命名插槽

js
// passing multiple slot fragments with different names
BaseLayout({
  header: `...`,
  default: `...`,
  footer: `...`
})

// <BaseLayout> renders them in different places
function BaseLayout(slots) {
  return `<div class="container">
      <header>${slots.header}</header>
      <main>${slots.default}</main>
      <footer>${slots.footer}</footer>
    </div>`
}

条件插槽

有时,您可能需要根据插槽是否存在来渲染某些内容。

您可以使用 $slots 属性与 v-if 结合使用来实现这一点。

以下示例中,我们定义了一个具有三个条件插槽的 Card 组件:headerfooterdefault。当存在头部/尾部/默认插槽时,我们想要将它们包裹起来以提供额外的样式。

template
<template>
  <div class="card">
    <div v-if="$slots.header" class="card-header">
      <slot name="header" />
    </div>
    
    <div v-if="$slots.default" class="card-content">
      <slot />
    </div>
    
    <div v-if="$slots.footer" class="card-footer">
      <slot name="footer" />
    </div>
  </div>
</template>

在Playground中尝试它

动态插槽名称

动态指令参数 也可以用于 v-slot,允许定义动态插槽名称。

template
<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>

  <!-- with shorthand -->
  <template #[dynamicSlotName]>
    ...
  </template>
</base-layout>

请注意,表达式受到 动态指令参数语法约束 的限制。

作用域插槽

渲染作用域 所述,插槽内容无法访问子组件的状态。

然而,在某些情况下,如果插槽的内容可以同时使用父作用域和子作用域中的数据,那么这可能很有用。为了实现这一点,我们需要一种方法,让子组件在渲染时将数据传递给插槽。

实际上,我们可以这样做——我们可以像传递 prop 给组件一样将属性传递给插槽出口。

template
<!-- <MyComponent> template -->
<div>
  <slot :text="greetingMessage" :count="1"></slot>
</div>

在使用单个默认插槽与使用命名插槽接收 prop 时有所不同。我们将首先通过在子组件标签上直接使用 v-slot 来展示如何使用单个默认插槽接收 prop。

template
<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>

scoped slots diagram

子组件传递给插槽的 prop 可以作为相应 v-slot 指令的值使用,可以在插槽内的表达式中访问。

您可以将作用域插槽视为传递给子组件的函数。然后子组件调用它,并将 prop 作为参数传递。

js
MyComponent({
  // passing the default slot, but as a function
  default: (slotProps) => {
    return `${slotProps.text} ${slotProps.count}`
  }
})

function MyComponent(slots) {
  const greetingMessage = 'hello'
  return `<div>${
    // call the slot function with props!
    slots.default({ text: greetingMessage, count: 1 })
  }</div>`
}

实际上,这与作用域插槽的编译方式非常接近,以及您如何在使用手动 渲染函数 时使用作用域插槽。

注意 v-slot="slotProps" 与插槽函数签名的匹配。就像函数参数一样,我们可以在 v-slot 中使用解构。

template
<MyComponent v-slot="{ text, count }">
  {{ text }} {{ count }}
</MyComponent>

命名作用域插槽

命名作用域插槽的工作方式类似——插槽 prop 可以作为 v-slot 指令的值访问:v-slot:name="slotProps"。当使用简写时,它看起来像这样

template
<MyComponent>
  <template #header="headerProps">
    {{ headerProps }}
  </template>

  <template #default="defaultProps">
    {{ defaultProps }}
  </template>

  <template #footer="footerProps">
    {{ footerProps }}
  </template>
</MyComponent>

将 prop 传递给命名插槽

template
<slot name="header" message="hello"></slot>

请注意,插槽的 name 不会包含在 prop 中,因为它被保留——所以生成的 headerProps 将是 { message: 'hello' }

如果您正在将命名插槽与默认作用域插槽混合使用,则需要为默认插槽使用显式的 <template> 标签。直接在组件上放置 v-slot 指令会导致编译错误。这是为了避免对默认插槽的 prop 作用域的任何歧义。例如

template
<!-- <MyComponent> template -->
<div>
  <slot :message="hello"></slot>
  <slot name="footer" />
</div>
template
<!-- This template won't compile -->
<MyComponent v-slot="{ message }">
  <p>{{ message }}</p>
  <template #footer>
    <!-- message belongs to the default slot, and is not available here -->
    <p>{{ message }}</p>
  </template>
</MyComponent>

使用显式的 <template> 标签为默认插槽有助于明确 message prop 不会在其它插槽内部可用。

template
<MyComponent>
  <!-- Use explicit default slot -->
  <template #default="{ message }">
    <p>{{ message }}</p>
  </template>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</MyComponent>

复杂列表示例

您可能想知道范围插槽有什么好的用例。以下是一个示例:想象一个 <FancyList> 组件,它渲染一个项目列表 - 它可以封装加载远程数据的逻辑、使用数据显示列表,甚至高级功能如分页或无限滚动。然而,我们希望它对每个项目的样式灵活,将每个项目的样式留给父组件来处理。所以期望的使用方式可能看起来像这样

template
<FancyList :api-url="url" :per-page="10">
  <template #item="{ body, username, likes }">
    <div class="item">
      <p>{{ body }}</p>
      <p>by {{ username }} | {{ likes }} likes</p>
    </div>
  </template>
</FancyList>

<FancyList> 内部,我们可以多次渲染相同的 <slot>,并使用不同的项目数据(注意我们使用 v-bind 将对象作为插槽属性传递)

template
<ul>
  <li v-for="item in items">
    <slot name="item" v-bind="item"></slot>
  </li>
</ul>

无渲染组件

我们上面讨论的 <FancyList> 用例封装了可重用逻辑(数据获取、分页等)和视觉输出,并通过范围插槽将部分视觉输出委托给消费者组件。

如果我们进一步推进这个概念,我们可以得到只封装逻辑而不自行渲染的组件 - 视觉输出完全通过范围插槽委托给消费者组件。我们称这种类型的组件为 无渲染组件

一个示例无渲染组件可能是封装跟踪当前鼠标位置逻辑的组件

template
<MouseTracker v-slot="{ x, y }">
  Mouse is at: {{ x }}, {{ y }}
</MouseTracker>

虽然这是一个有趣的模式,但使用组合式API可以以更高效的方式实现大多数使用无渲染组件可以完成的事情,而不必承担额外的组件嵌套的开销。稍后我们将看到如何将相同的鼠标跟踪功能实现为一个 可组合的组件

话虽如此,范围插槽在需要同时封装逻辑 组合视觉输出的情况下仍然很有用,例如在 <FancyList> 示例中。

插槽已加载