vue 系列 -- 插槽

519 阅读4分钟

插槽的作用是什么?

todo-button.vue——自定义组件

<button class="btn-primary"></button>

index.vue——引入 todo-button 组件

<todo-button>
  Add todo <!-- 文本不会渲染,无法给按钮自定义文字 -->
</todo-button>

为了能给自定义组件添加或设置自定义内容,使得自定义组件通用化,于是有了插槽

只需要修改 todo-button.vue 即可,修改内容如下:

<button class="btn-primary">
  <slot></slot> <!-- slot 元素将会被 <todo-button> 组件里的内容代替 -->
</button>

这样就实现了给自定义组件添加或设置自定义内容,使得自定义组件通用化

匿名插槽及后备内容

以上 <slot> 元素就被称为 匿名插槽

为了防止开发者使用自定义组件的时候没有设置自定义内容时,自定义组件也能正常使用,我们需要给 slot 元素里面添加内容,这就是 后备内容

<button type="submit">
  <slot>Submit</slot> <!-- slot 元素将会被 <todo-button> 组件里的内容代替 -->
</button>

现在当我在一个父级组件中使用   并且不提供任何插槽内容时:

<submit-button></submit-button>

后备内容“Submit”将会被渲染出来。当然,当我们在 <submit-button> 组件里面添加自定义内容,其默认值自然会被替代。

具名插槽

为了给更加具有可控性,又比如需要有多个插槽,我们就得让 <todo-button> 组件里包裹的自定义内容代替 todo-button.vue 的指定位置,我们就需要 具名插槽

例如一个带有如下模板的 <base-layout> 组件:

<div class="container">
  <header>
    <!-- 我们希望把页头放这里 -->
  </header>
  <main>
    <!-- 我们希望把主要内容放这里 -->
  </main>
  <footer>
    <!-- 我们希望把页脚放这里 -->
  </footer>
</div>

对于这样的情况,<slot> 元素有一个特殊的 attribute:name。这个 attribute 可以用来定义额外的插槽:

<div class="container">
  <header>
    <slot name="header"></slot> <!-- 具名插槽 -->
  </header>
  <main>
    <slot></slot> <!-- 匿名插槽 -->
  </main>
  <footer>
    <slot name="footer"></slot> <!-- 具名插槽 -->
  </footer>
</div>

一个不带 name 的 <slot> 出口会带有隐含的名字“default”。

在向具名插槽提供内容的时候,我们可以在一个 <template> 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称:

<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>

  <template v-slot:default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>

  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

现在 <template> 元素中的所有内容都将会被传入相应的插槽。

最终渲染的 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>

父组件传数据给子组件渲染

使用自定义组件(作为子组件)时我们需要将父组件中的数据传递给子组件,这样才显得子组件更有使用价值,方法是三部曲:

  1. 父组件 v-bind:xxx = xxx 绑定数据
  2. 子组件通过 prop 属性接住
  3. 子组件直接通过 xxx 属性渲染出来

示例图解

作用域插槽——让父组件可以拿到子组件中的数据

有时让插槽内容能够访问子组件中才有的数据是很有用的。举个例子:我们将父组件的表格数据传递给子组件(一个自定义的表格组件),然后我们给表格各行添加点击事件,需要拿到该行的数据(如:索引、该行的数据对象等)。这时候就需要 作用域插槽 来辅助我们完成这一点

假设我们有一个组件,包含 todo-items 的列表。

app.component('todo-list', {
  data() {
    return {
      items: ['Feed a cat', 'Buy milk']
    }
  },
  template: `
    <ul>
      <li v-for="(item, index) in items">
        {{ item }}
      </li>
    </ul>
  `
})

我们可能需要替换插槽以在父组件上自定义它:

<todo-list>
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}</span>
</todo-list>

但是,这是行不通的,因为只有 <todo-list> 组件可以访问 item,我们将从其父组件提供槽内容。

要使 item 可用于父级提供的 slot 内容,我们可以添加一个 <slot> 元素并将其绑定为属性:

<ul>
  <li v-for="( item, index ) in items">
    <slot :item="item"></slot>
  </li>
</ul>

绑定在 <slot>  元素上的 attribute 被称为 插槽 prop。现在在父级作用域中,我们可以使用带值的 v-slot 来定义我们提供的插槽 prop 的名字:

<todo-list>
  <template v-slot:default="slotProps"> <!-- slotProps可以命名为其他任意名字 -->
    <i class="fas fa-check"></i>
    <span class="green">{{ slotProps.item }}</span>
  </template>
</todo-list>

数据流向图解

附录内容

解构插槽 Prop

作用域插槽的内部工作原理是将你的插槽内容包括在一个传入单个参数的函数里:

function (slotProps) {
  // ... 插槽内容 ...
}

这意味着 v-slot 的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式。你也可以使用 ES2015 解构来传入具体的插槽 prop,如下:

<todo-list v-slot="{ item }">
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}</span>
</todo-list>

这样可以使模板更简洁,尤其是在该插槽提供了多个 prop 的时候。它同样开启了 prop 重命名等其它可能,例如将 item 重命名为 todo

<todo-list v-slot="{ item: todo }">
  <i class="fas fa-check"></i>
  <span class="green">{{ todo }}</span>
</todo-list>

你甚至可以定义后备内容,用于插槽 prop 是 undefined 的情形:

<todo-list v-slot="{ item = 'Placeholder' }">
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}</span>
</todo-list>

动态插槽名

动态指令参数 也可以用在 v-slot 上,来定义动态的插槽名:

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

具名插槽的缩写

跟 v-on 和 v-bind 一样,v-slot 也有缩写,即把参数之前的所有内容 (v-slot:) 替换为字符 #。例如 v-slot:header 可以被重写为 #header

<base-layout>
  <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>
</base-layout>

然而,和其它指令一样,该缩写只在其有参数的时候才可用:

<!-- 以下语法是无效的 -->
<todo-list #="{ item }">
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}</span>
</todo-list>
<!-- 如果你希望使用缩写的话,你必须始终以明确插槽名取而代之 -->
<todo-list #default="{ item }">
  <i class="fas fa-check"></i>
  <span class="green">{{ item }}</span>
</todo-list>

以上均为 vue 3.0 的写法,下面介绍 vue 2.x 的写法

vue 2.x 的写法:

带有 slot attribute 的具名插槽

<base-layout>
  <h1 slot="header">Here might be a page title</h1>
  <p>A paragraph for the main content.</p>
  <p>And another one.</p>
  <p slot="footer">Here's some contact info</p>
</base-layout>

带有 slot-scope attribute 的作用域插槽

<slot-example>
  <span slot-scope="slotProps"> {{ slotProps.msg }} </span>
</slot-example>

参考文章

深入了解组件 - 插槽(官方文档)