Vue中的插槽

648 阅读7分钟

问题导读

在学习Vue的时候,你可能或多或少的在很多文档中提交Vue的插槽,不知道在你刚接触的时候是怎么想的,我当初刚学Vue的时候,在看这块内容的时候,总有种似懂非懂的感觉,你说懂吧,好像不太懂,你说不懂吧,感觉知道点什么。这种感觉在我使用ElementUI做项目的时候,才彻底的解决,原因是我在使用el-drawer组件的时候,我在想,为什么我能定制section里面的内容,而且还能把el-table这样的内容放进去,它是如何实现的呢?带着这样的想法,我去看了一下element的代码,不出所料,确实使用了插槽,而这也让我真正的见识了插槽这个概念在实际中的使用。

相信大家和我有同样的感受,很多概念可能在初始学习的时候,看文档总是觉得好像是那么回事儿,但是在使用使用的时候,感觉还是差点意思。希望大家在遇到同样的问题时,能够保持这种为什么的心态,来进行学习。

本篇文章,我们将学习插槽的基本内容,最后在一个适应案例,来结束。

基本内容

插槽内容

Vue 实现了一套内容分发的 API,这套 API 的设计灵感源自 Web Components 规范草案,将 <slot> 元素作为承载分发内容的出口。

比如,定义一个组件,命名为navigation-link

<a v-bind:href="url" class="nav-link" > <slot></slot> </a>

那么当我们在父组件里使用该组件的时候,就可以这么使用

<navigation-link url="/profile"> Your Profile </navigation-link>

当组件渲染的时候,<slot></slot> 将会被替换为“Your Profile”。插槽内可以包含任何模板代码,包括 HTML:

<navigation-link url="/profile"><span class="fa fa-user"></span> Your Profile </navigation-link>

那么当组件渲染的时候,<slot></slot> 将会被替换为<span class="fa fa-user"></span> Your Profile

<slot></slot><navigation-link> 中的位置可以由你自由的放置。但是如果 <navigation-link> 的 template 中没有包含一个 <slot> 元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃。

编译作用域

当你想在一个插槽中使用数据时,例如:

<navigation-link url="/profile"> 
Clicking here will send you to: {{ url }}
</navigation-link>

这里的 url 会是 undefined,因为其 (指该插槽的) 内容是 传递给 <navigation-link> 的而不是 在 <navigation-link> 组件内部定义的。

也就是说,url是父组件的data函数返回值对象里面的属性,并不是子组件<navigation-link> 里面定义的属性。如果仅仅是这么组织代码,你在<navigation-link> 组件中自然是拿不到这个值的。

后备内容

有时为一个插槽设置默认的内容是很有用的,它只会在没有提供内容的时候被渲染。例如在一个 <submit-button> 组件中:

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

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

<submit-button></submit-button>

<slot></slot>就没有内容,如果你在标签中提供了内容,<slot></slot>部分才会有内容。

但是有时候可能需要提供一个默认的值在<slot></slot>处进行渲染,那么可以直接在<slot></slot>标签中加入默认内容,作为不提供内容时的默认值。

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

具名插槽

当在一个组件模板中,需要多处分发的内容时,可以写多个<slot></slot>,针对每个<slot></slot>,我们提供一个name属性,来命名各个<slot></slot>.

<div class="container"> 
<header> 
    <slot name="header"></slot> 
</header> 

<main> 
    <slot></slot> 
</main>
<footer> 
    <slot name="footer"></slot> 
</footer>
</div>

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

在父组件中,我们就可以这么分发内容

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

</base-layout>

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

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

任何没有被包裹在带有 v-slot 的 <template> 中的内容都会被视为默认插槽的内容。

最终的效果就是

<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>

作用域插槽

有时让插槽内容能够访问子组件中才有的数据是很有用的。例如,设想一个带有如下模板的 <current-user> 组件:

<span> <slot>{{ user.lastName }}</slot> </span>

我们可能想换掉备用内容,用名而非姓来显示。如下

<current-user> {{ user.firstName }} </current-user>

然而上述代码不会正常工作,因为只有 <current-user> 组件可以访问到 user,而我们提供的内容是在父级渲染的。

如果你不在明天上面说了什么,那就来看一下下面的代码:

<template>
  <div id="app">
    <HelloWorld>
      <template v-slot:default="slotProps">
        {{ slotProps.user.firstName }}
      </template>
    </HelloWorld>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  }
}
</script>

在下面这里使用slot来分发内容

<template>
  <div class="hello">
    <slot v-bind:user="user">{{ user.lastName }}</slot>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  data() {
    return {
      user: {
        firstName: "三",
        lastName: "张"
      }
    }
  }
}
</script>

这样是无法工作的,这个就很清楚了,父级作用域,我们并没有定义user,当然不能用了。

为了让 user 在父级的插槽内容中可用,我们可以将 user 作为 <slot> 元素的一个 attribute 绑定上去:

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

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

<template>
  <div id="app">
    <HelloWorld>
      <template v-slot:default="slotProps">
        {{ slotProps.user.firstName }}
      </template>
    </HelloWorld>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'

export default {
  name: 'App',
  components: {
    HelloWorld
  }
}
</script>

独占默认插槽的缩写语法

在上述情况下,当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。这样我们就可以把 v-slot 直接用在组件上:

<HelloWorld v-slot:default="slotProps">
    {{ slotProps.user.firstname }}
 </HelloWorld>

不用再<template></template>上加v-slot.直接写到组件标签上。

这种写法还可以更简单。就像假定未指明的内容对应默认插槽一样,不带参数的 v-slot 被假定对应默认插槽:

<HelloWorld v-slot="slotProps">
        {{ slotProps.user.firstname }}
</HelloWorld>

注意默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确:

<!-- 无效,会导致警告 --> 
<current-user v-slot="slotProps"> 
    {{ slotProps.user.firstName }} 
    <template v-slot:other="otherSlotProps"> 
        slotProps is NOT available here 
    </template>
</current-user>

只要出现多个插槽,请始终为所有的插槽使用完整的基于 <template> 的语法.

解构插槽Prop

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

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

这意味着 v-slot 的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式.

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

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

<current-user v-slot="{ user: person }"> 
    {{ person.firstName }} 
</current-user>

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

<current-user v-slot="{ user = { firstName: 'Guest' } }"> 
    {{ user.firstName }}
</current-user>

动态插槽名

动态指令参数也可以用在 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> 
    <p>A paragraph for the main content.</p> 
    <p>And another one.</p> 
    <template #footer> 
        <p>Here's some contact info</p> 
    </template> 
</base-layout>

然而,和其它指令一样,该缩写只在其有参数的时候才可用。这意味着以下语法是无效的:

<!-- 这样会触发一个警告 --> 
<current-user #="{ user }"> 
    {{ user.firstName }} 
</current-user>

如果你希望使用缩写的话,你必须始终以明确插槽名取而代之:

<current-user #default="{ user }"> 
    {{ user.firstName }} 
</current-user>

其他实例

插槽 prop 允许我们将插槽转换为可复用的模板,这些模板可以基于输入的 prop 渲染出不同的内容。 这在设计封装数据逻辑同时允许父级组件自定义部分布局的可复用组件时是最有用的。

例如,我们要实现一个 <todo-list> 组件,它是一个列表且包含布局和过滤逻辑:

<ul> 
    <li v-for="todo in filteredTodos" v-bind:key="todo.id" > 
        {{ todo.text }} 
    </li> 
</ul>

我们可以将每个 todo 作为父级组件的插槽,以此通过父级组件对其进行控制,然后将 todo 作为一个插槽 prop 进行绑定:

<ul> 
    <li v-for="todo in filteredTodos" v-bind:key="todo.id" > 
        <!-- 我们为每个 todo 准备了一个插槽, 将 `todo` 对象作为一个插槽的 prop 传入。 --> 
        <slot name="todo" v-bind:todo="todo">
            <!-- 后备内容 --> 
            {{ todo.text }} 
        </slot> 
    </li> 
</ul>

现在当我们使用 <todo-list> 组件的时候,我们可以选择为 todo 定义一个不一样的 <template> 作为替代方案,并且可以从子组件获取数据:

<todo-list v-bind:todos="todos"> 
    <template v-slot:todo="{ todo }"> 
        <span v-if="todo.isComplete">✓</span> {{ todo.text }} 
    </template> 
</todo-list>

这种思想其实还是很常见的,通过父级组件来控制插槽的内容展示

示例

在element UI中,较多的组件其实都使用了插槽,因为,很多组件都是可以支持我们再内容里面增加我们的业务代码的,而插槽很好了承载了这一部分内容。而这或许就是插槽的意义。

packages/drawer/src/main.vue

<header class="el-drawer__header" id="el-drawer__title" v-if="withHeader">
	<slot name="title">
	  <span role="heading" :title="title">{{ title }}</span>
	</slot>
	<button
	  :aria-label="`close ${title || 'drawer'}`"
	  class="el-drawer__close-btn"
	  type="button"
	  v-if="showClose"
	  @click="closeDrawer">
	  <i class="el-dialog__close el-icon el-icon-close"></i>
	</button>
</header>
<section class="el-drawer__body" v-if="rendered">
	<slot></slot>
</section>