原文:Using Slots In Vue.js

插槽在Vue.js中是一个强大的工具,可以用来创建重复可用的组件,尽管它不是最容易理解的功能。让我们看看如何使用插槽以及在Vue应用程序中使用插槽的一些示例。

随着Vue 2.6的最新发布,使用插槽的语法变得更加简洁。插槽的这些变化使我重新对发现插槽的潜在能力产生了兴趣,这个潜在能力可以为基于Vue的项目提供可复用性,新功能以及更清晰的可读性。插槽真正能做什么?

如果你是Vue的新手,或者还没有看到版本2.6的更改,请继续阅读。也许学习插槽的最佳资源是Vue自己的文档,但是我将在这里做一个简要的介绍。

什么是插槽?

插槽是Vue组件的一种机制,它允许你以严格的父子关系以外的其他方式来组合组件。插槽为你提供了一个渠道,可以将内容放置在新位置或使组件更通用。理解他们的最好方式是看到它们的实际效果。让我们从一个简单的示例开始:

  1. // frame.vue
  2. <template>
  3. <div class="frame">
  4. <slot></slot>
  5. </div>
  6. </template>

这个组件有个包裹元素div。让我们假装div是为了创建一个围绕其内容的样式框架。此组件可以通用地将框架包装在任何你想要的内容周围。我们来看看它是怎么用的。这里的frame组件是指我们上面刚刚创建的组件。

  1. // app.vue
  2. <template>
  3. <frame><img src="an-image.jpg"></frame>
  4. </template>

位于frame开始和结束标签之间的内容将会被插入到插槽所在的frame组件中,并替换slot标签。这是最基本的用法。你也可以在插槽中指定默认的内容,只需简单地将内容填入插槽:

  1. // frame.vue
  2. <template>
  3. <div class="frame">
  4. <slot>This is the default content if nothing gets specified to go here</slot>
  5. </div>
  6. </template>

所以现在如果我们这样使用它:

  1. // app.vue
  2. <template>
  3. <frame />
  4. </template>

默认文本“如果这里没有指定任何内容,这里就是默认内容”将会显示出来,但是如果我们像以前一样使用它,默认文本将会被img标签覆盖。

多个/指定插槽

你可以向一个组件添加多个插槽,但是如果你这样做了,那么除了其中一个之外,其他所有插槽都需要有名称。如果有一个没有名字的插槽,它就是默认插槽。下面是如何创建多个插槽:

  1. // titled-frame.vue
  2. <template>
  3. <div class="frame">
  4. <header><h2><slot name="header">Title</slot></h2></header>
  5. <slot>This is the default content if nothing gets specified to go here</slot>
  6. </div>
  7. </template>

我们保留了相同的默认插槽,但是这次我们添加了一个名为header的插槽,你可以在其中输入标题。你可以这样使用它:

  1. // app.vue
  2. <template>
  3. <titled-frame>
  4. <template v-slot:header>
  5. <!-- The code below goes into the header slot -->
  6. My Images Title
  7. </template>
  8. <!-- The code below goes into the default slot -->
  9. <img src="an-image.jpg">
  10. </titled-frame>
  11. </template>

就像之前一样,如果我们想要将内容添加到默认插槽中,只需将其直接放到titled-frame组件中即可。但是,要将内容添加到命名插槽中,我们需要使用v-slot指令将代码包装在template标签中。在v-slot之后添加一个冒号(:),然后写入要传递内容的slot的名称。请注意,v-slot是Vue 2.6的新语法,因此如果你使用的是旧版本,那么你需要阅读有关废弃slot语法的文档

作用域插槽

还需要了解的一件事是,插槽可以将数据/函数传递给他们的子元素。为了演示这一点,我们需要一个完全不同的带有插槽的示例组件,一个甚至比前一个更精心设计:让我们通过创建一个组件来从文档复制一个示例,这个组件会将有关当前用户的数据提供给其插槽:

// current-user.vue
<template>
  <span>
    <slot v-bind:user="user">
      {{ user.lastName }}
    </slot>
  </span>
</template>

<script>
export default {
  data () {
    return {
      user: ...
    }
  }
}
</script>

该组件有一个名为user的属性,其中包含用户的详细信息。默认情况下,组件显示用户的姓,但是请注意,它使用v-bind将用户数据绑定到slot。这样,我们可以用这个组件为它的后代提供用户数据:

// app.vue
<template>
  <current-user>
    <template v-slot:default="slotProps">{{ slotProps.user.firstName }}</template>    
  </current-user>
</template>

为了访问传递给slot的数据,我们使用v-slot指令的值指定范围变量的名称。

这里有一些注意事项:

  • 我们指定了default的名称,尽管不需要为默认插槽指定名称。相反,我们可以只使用v-slot="slotProps"
  • 你无需使用slotProps作为名称。你可以随意叫它。
  • 如果你仅使用默认插槽,可以跳过该内部的template标签,然后将v-slot指令直接放在current-user标签上。
  • 你可以使用对象解构来创建对作用域插槽数据的直接引用,而不必使用单个变量名。换句话说,你可以使用v-slot="{user}"代替v-slot="slotProps",然后可以直接使用user代替slotProps.user

考虑到这些事项,上面的示例可以像这样重写:

// app.vue
<template>
  <current-user v-slot="{user}">
    {{ user.firstName }}
  </current-user>
</template>

还有几件事要记住:

  • 你可以使用v-bind指令绑定多个值。因此,在示例中,我可以操作的不仅仅是user。
  • 你也可以将函数传递给作用域插槽。许多库用它来提供可重用的功能组件,稍后你将看到这一点。
  • v-slot的别名为。因此,你可以用#header="data"代替v-slot:header="data"。当你不使用作用域插槽时,也可以只指定#header代替v-slot:header。至于默认插槽,使用别名时,你需要指定default的名称。换句话说,你需要编写#default="data"而不是#="data"

你可以从文档中了解更多要点,但这些足以帮助你了解本文其余所讨论的内容。

你能用插槽做什么?

插槽不是为一个单一的目的而创造的,或者至少如果是的话,它们已经超越了最初的意图,成为能做许多不同事情的强大工具。

可重复使用的模式

组件总是被设计为可以重复使用,但是某些模式用单个“普通”的组件实现是不切实际的,因为定制它所需的props数量可能过多,或者你需要通过props传递大量的内容以及潜在的其他组件。插槽可以用于包含模式的“外部”部分,并允许其他HTML和/或组件放在它里面以自定义“内部”部分,从而允许具有插槽的组件定义模式,同时注入到插槽中的组件将会变成唯一。

对于第一个例子,让我们从一个简单的东西开始:一个按钮。假设你和你的团队正在使用Bootstrap *。使用Bootstrap时,你的按钮通常和基本的btn类和指定颜色的类绑定在一起,例如btn-primary。你还可以添加一个大小类,例如btn-lg

*我既不鼓励也不反对你这样做,我只是想举一些例子,这是众所周知的。

为了简单起见,我们现在假设你的app/网站总是使用btn-primarybtn-lg。你不希望总是在你的按钮上写这三个类,或者你不相信一个新手会记住这三个类。在这种情况下,你可以创建一个自动拥有所有这三个类的组件,但是如何允许自定义的内容呢?props是不实际的,因为button标签允许包含各种HTML,所以我们应该使用插槽。

<!-- my-button.vue -->
<template>
  <button class="btn btn-primary btn-lg">
    <slot>Click Me!</slot>
  </button>
</template>

现在我们可以用任何你想要的内容在任何地方使用它:

<!-- somewhere else, using my-button.vue -->
<template>
  <my-button>
    <img src="/img/awesome-icon.jpg"> SMASH THIS BUTTON TO BECOME AWESOME FOR ONLY $500!!!
  </my-button>
</template>

当然,你可以选择比按钮更庞大的东西。继续使用Bootstrap,让我们来看看一个modal,或者至少是HTML部分;我还不会涉及功能…。

<!-- my-modal.vue -->
<template>
<div class="modal" tabindex="-1" role="dialog">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <slot name="header"></slot>
        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">×</span>
        </button>
      </div>
      <div class="modal-body">
        <slot name="body"></slot>
      </div>
      <div class="modal-footer">
        <slot name="footer"></slot>
      </div>
    </div>
  </div>
</div>
</template>

现在,让我们使用它:

<!-- somewhere else, using my-modal.vue -->
<template>
  <my-modal>
    <template #header><!-- using the shorthand for `v-slot` -->
      <h5>Awesome Interruption!</h5>
    </template>
    <template #body>
      <p>We interrupt your use of our application to
      let you know that this application is awesome 
      and you should continue using it every day for 
      the rest of your life!</p>
    </template>
    <template #footer>
      <em>Now back to your regularly scheduled app usage</em>
    </template>
  </my-modal>
</template>

上述类型的插槽示例显然非常有用,但是它可以做得更多。

复用功能

Vue组件并不全是HTML和CSS的。它们是使用JavaScript构建的,因此它们也与功能有关。插槽对于创建一次性的功能并在多个位置使用它很有用。让我们回到modal示例,并添加一个关闭modal的函数:

<!-- my-modal.vue -->
<template>
<div class="modal" tabindex="-1" role="dialog">
  <div class="modal-dialog" role="document">
    <div class="modal-content">
      <div class="modal-header">
        <slot name="header"></slot>
        <button type="button" class="close" data-dismiss="modal" aria-label="Close">
          <span aria-hidden="true">×</span>
        </button>
      </div>
      <div class="modal-body">
        <slot name="body"></slot>
      </div>
      <div class="modal-footer">
        <!--
          using `v-bind` shorthand to pass the `closeModal` method
          to the component that will be in this slot
        -->
        <slot name="footer" :closeModal="closeModal"></slot>
      </div>
    </div>
  </div>
</div>
</template>

<script>
export default {
  //...
  methods: {
    closeModal () {
      // Do what needs to be done to close the modal... and maybe remove it from the DOM
    }
  }
}
</script>

现在,当你使用这个组件时,可以向页脚添加一个可以关闭modal的按钮。通常,在使用Bootstrap modal的情况下,你只需在按钮添加data-dismiss="modal",但是我们希望将Bootstrap特定的东西隐藏在这个modal组件中的组件之外。所以我们给他们传递一个他们可以调用的函数,而他们并不知道Bootstrap的参与:

<!-- somewhere else, using my-modal.vue -->
<template>
  <my-modal>
    <template #header><!-- using the shorthand for `v-slot` -->
      <h5>Awesome Interruption!</h5>
    </template>
    <template #body>
      <p>We interrupt your use of our application to
      let you know that this application is awesome 
      and you should continue using it every day for 
      the rest of your life!</p>
    </template>
    <!-- pull in `closeModal` and use it in a button’s click handler -->
    <template #footer="{closeModal}">
      <button @click="closeModal">
        Take me back to the app so I can be awesome
      </button>
    </template>
  </my-modal>
</template>

无渲染组件

最后,你可以充分利用你所知道的关于使用插槽传递可重用功能的内容,并删除几乎所有的HTML,只需使用插槽即可。这就是无渲染组件的本质:一个只提供功能而不提供任何HTML的组件。

要使组件真正地变得无渲染可能有些棘手,因为你需要编写渲染函数,而不是使用模板来去掉需要的根元素,但这并非总是必要的。让我们看一个简单的示例,这示例确实允许我们首先使用模板:

<template>
  <transition name="fade" v-bind="$attrs" v-on="$listeners">
    <slot></slot>
  </transition>
</template>
<style>
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}
.fade-enter, .fade-leave-to {
  opacity: 0;
}
</style>

这是无渲染组件的一个奇怪例子,因为它甚至没有任何JavaScript。这主要是因为我们只是在创建一个内置无渲染功能的预先配置的可重用版本:transition

是的,Vue具有内置的无渲染组件。此特定示例摘自Cristi Jora的有关可重用transition的文章,并显示了一种创建无渲染组件的简单方法,该组件可以标准化整个应用程序中使用的transition。Cristi的文章更加深入,并展示了可重用transition的一些高级变体,因此我建议你查看它。

对于另一个示例,我们将创建一个组件,用于处理在不同Promise状态下显示的内容:挂起、成功解决和失败。这是一种常见的模式,虽然它不需要大量代码,但如果不提取逻辑以实现可重用,那么它会使很多组件变得混乱。

<!-- promised.vue -->
<template>
  <span>
    <slot  name="rejected"  v-if="error" :error="error"></slot>
    <slot  name="resolved"  v-else-if="resolved" :data="data"></slot>
    <slot  name="pending"  v-else></slot>
  </span>
</template>

<script>
export  default {
  props: {
    promise:  Promise
  },

  data: () => ({
    resolved:  false,
    data:  null,
    error:  null
  }),  

  watch: {
    promise: {
      handler (promise) {
        this.resolved  =  false
        this.error  =  null

        if (!promise) {
          this.data  =  null
          return
        }

        promise.then(data  => {
          this.data  =  data
          this.resolved  =  true
        })
        .catch(err  => {
          this.error  =  err
          this.resolved  =  true
        })
      },
      immediate:  true
    }
  }
}
</script>

这是怎么回事?首先,请注意,我们正在收到一个名为promise的prop,这是一个Promise。在watch函数中,我们监视promise的更改,当它发生更改时(或者在组件创建时立即更改,因为有immediate属性),我们清除状态,然后调用thencatch promise,当它成功完成或失败时更新状态。

然后,在模板中,我们根据状态显示不同的插槽。请注意,我们未能使它真正变成无渲染,因为我们需要一个根元素才能使用模板。我们还将数据和错误传递给相关的插槽范围。

下面是一个使用它的例子:

<template>
  <div>
    <promised :promise="somePromise">
      <template #resolved="{ data }">
        Resolved: {{ data }}
      </template>
      <template #rejected="{ error }">
        Rejected: {{ error }}
      </template>
      <template #pending>
        Working on it...
      </template>
    </promised>
  </div>
</template>
...

我们将somePromise传递给无渲染组件。在等待完成的过程中,由于有pending的插槽,我们正在显示“正在处理……”。如果成功,则显示“已解决:”和解析值。如果失败,则显示“ 已拒绝:”和导致拒绝的错误。现在,我们不再需要跟踪该组件中的promise状态,因为该部分被提取到自己的可重用组件中。

那么,该如何处理promise.vue中围绕插槽的span呢?要删除它,我们需要删除模板部分并将渲染功能添加到我们的组件中:

render () {
  if (this.error) {
    return this.$scopedSlots['rejected']({error: this.error})
  }

  if (this.resolved) {
    return this.$scopedSlots['resolved']({data: this.data})
  }

  return this.$scopedSlots['pending']()
}

这里没有什么棘手的事情。我们只是使用一些if块来查找状态,然后返回正确的作用域范围内的插槽(通过this.$scopedSlots['SLOTNAME'](...)),然后将相关数据传递给插槽作用域。当你不使用模板时,通过将JavaScript从script标签中提取并将其放入.js文件中可以跳过使用.vue文件扩展名。在编译那些Vue文件时,这应该会给你带来轻微的性能提升。

这个例子是vue-promised的精简和稍微调整的版本,我建议你不要使用上面的示例,因为它们涵盖了一些潜在的缺陷。还有很多其他优秀的无渲染组件的例子。Baleada是一个包含无渲染组件的完整的库,这些组件提供了类似这样有用的功能。还有vue-virtual-scroller,用于根据屏幕上可见的内容来控制列表项的呈现,或者PortalVue用于将内容“传送”到DOM的完全不同的部分。

结束了

vue的插槽将基于组件的开发提升到了一个全新的高度,虽然我已经演示了很多可以使用插槽的好方法,但是还有无数的方法。你能想到什么好主意?你认为插槽有什么办法可以升级?如果你有,一定要把你的想法带到Vue团队。上帝保佑,编码愉快。