Vue 插槽从零到精通

600 阅读5分钟

基础

插槽内容

简单示例:

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

<navigation-link> 的模板中:

// 当组件渲染的时候,`<slot></slot>` 将会被替换为“Your Profile”。
<a v-bind:href="$attrs.url">
  <slot></slot>
</a>

如果 <navigation-link> 没有包含一个 <slot>元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃。

编译作用域

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

<navigation-link url="/profile">
  Logged in as {{ user.name }}
</navigation-link>

该插槽跟模板的其它地方一样可以访问相同的实例属性 (也就是相同的“作用域”),而不能访问 ``的作用域。例如url` 是访问不到的。

后备(默认)内容

我们可能希望这个 <button> 内绝大多数情况下都渲染文本“Submit”。为了将“Submit”作为后备内容,我们可以将它放在 <slot> 标签内:

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

具名插槽

有时我们需要多个插槽。例如对于一个带有如下模板的 <base-layout> 组件:

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

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

<div class="container">
  <header>
    // 具名插槽 header 
    <slot name="header"></slot>
  </header>
  <main>
    // 默认插槽 default  一个不带 `name` 的 `<slot>` 出口会带有隐含的名字“default”。
    <slot></slot>
  </main>
  <footer>
    // 具名插槽 footer
    <slot name="footer"></slot>
  </footer>
</div>

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

<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 <template>中的内容都会被视为默认插槽的内容。

具名插槽的缩写

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

作用域插槽

作用域插槽的目的:让插槽内容能够访问子组件中的数据!

// 子组件的后备内容可以访问 子组件的数据
<span>
  <slot>{{ user.lastName }}</slot>
</span>
// 父组件的插槽内容却无法访问
<current-user>
  {{ user.firstName }}
</current-user>

修改如下:

// 将传递给父组件的属性 绑定到 slot标签的attr中,可以是静态 例如 age=17  也可以动态 :user="user"
<span>
  <slot v-bind:user="user">
    {{ user.lastName }}
  </slot>
</span>
// 此时 如果想获得子组件插槽中的attr ,需要搭配 template标签 + v-slot指令
<current-user>
  <template v-slot:default="slotProps">
    {{ slotProps.user.firstName }}
  </template>
</current-user>

独占默认插槽的缩写语法

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

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

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

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

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

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

组件上的v-slot使用

<div id="app">
<one-child>
  <template v-slot:default="defaultProps">

    <!-- 可以访问 app 组件中的 数据 -->
    <h2>{{message}}</h2>

    <!--  defaultProps可以访问到 OneChild 组件上default插槽定义的属性 -->
    <h2>{{defaultProps}}</h2>

    <!--  v-slot:two_slot 这里的具名插槽(也可以设置为default,匿名插槽) two_slot 是指在two-child组件中有一个 slot标签 name属性的值为two_slot -->

    <two-child v-slot:two_slot="slotProps">
      
      <!-- slotProps可以访问到 TwoChild 组件上two_slot插槽定义的属性 -->
      <h3>{{slotProps}}</h3>
      
      <!--  此处defaultProps也可以访问到 OneChild 组件上default插槽定义的属性 -->
      <h3>{{defaultProps}}</h3>

      <!-- 可以访问 app 组件中的 数据 -->
      <h3>{{message}}</h3>
      
      <three-child>
          <template v-slot:three_slot="time">
           <!-- time可以访问到 ThreeChild 组件上three_slot插槽定义的属性 -->
            <h4>{{time}}</h4>
        
            <!-- 此处slotProps可以访问到 TwoChild 组件上two_slot插槽定义的属性 -->
             <h4>{{slotProps}}</h4>

            <!--  此处defaultProps也可以访问到 OneChild 组件上default插槽定义的属性 -->
             <h3>{{defaultProps}}</h4>
            
            <!-- 此处可以访问 app 组件中的 数据 -->
            <h4>{{message}}</h4>
          </template>
      </three-child>
    </two-child>

    <!-- 这个组件不能放在template标签的外边,否则会渲染不出来 -->
    <three-child></three-child>
  </template>

  <template v-slot:other="otherProps">
    <!-- OtherProps 可以访问到OneChild 组件上other插槽定义的属性 -->
     <h2>{{otherProps}}</h2>
  </template>
  
</one-child>
</div>

Vue.component("OneChild", {
        template: `
        <div>
                <h1>我是OneChild组件开头</h1>
                <slot firstName="eason" :lastName="ming">我是默认插槽</slot>
                <slot name="other" :age="people_age" sex="man">我是other插槽</slot>
                <h1>我是OneChild组件结尾</h1>
        </div>
        `,
        data() {
                return {
                        people_age: 27,
                        ming: "zengwei"
                }
        },
})
Vue.component("TwoChild", {
        template: `
        <div>
                <h4>我是TwoChild组件</h4>
                <slot name="two_slot" :color="value" size="16px"></slot>
        </div>
        `,
        data() {
                return {
                        value: "red"
                }
        },
})
Vue.component("ThreeChild", {
        template: `
        <div>
                <h4>我是ThreeChild组件</h4>
                <slot name="three_slot" :time="Date()"></slot>
        </div>
        `,
})
var vm = new Vue({
        el: "#app",
        data() {
                return {
                        message: "Hello Slot!"
                }
        },
})
  • v-slot指令可以只能使用在template标签、自定义组件标签

    v-slot标签可以指定参数作为具名插槽,指定值作为作用域插槽。当作用在template标签上:template标签下可以访问该组件的相关<slot>标签绑定的属性。与template标签上v-slot不同的是,自定义组件标签上的v-slot访问的是该自定义组件内部相关<slot>标签绑定的属性。

template标签

<base-layout>
  <template v-slot:header=”header_slotProps“>
    // 这里的header_slotProps作用域是 base-layout组件中 header具名插槽上绑定的attrs
    // 以下内容 会被插入到 base-layout组件中 header具名插槽中
    <h1>{{header_slotProps}}</h1>
  </template>
</base-layout>

自定义组件标签

<base-layout>
  <current-user v-slot:footer="footer_slotProps">
    // 这里的footer_slotProps作用域是 current-user组件中 footer具名插槽上绑定的attrs
    // // 以下内容 会被插入到 current-user组件中 footer具名插槽中
    <h1>{{footer_slotProps}}</h1>
  </current-user>
</base-layout>
  • 不能再含有v-slot指令的自定义组件标签内部,插入含有v-slot指令的template标签

<!-- 无效,会导致警告 -->
<base-layout>
  <current-user v-slot:footer="footer_slotProps">
    {{ slotProps}}
    <template v-slot:other="otherSlotProps">
      slotProps is NOT available here
    </template>
  </current-user>
</base-layout>
// 原因是:current-user 自定义组件标签内部的所有代码会被插入到 current-user组件模板的具名插槽footer中,而template标签v-slot指令 不知道该把此 template 标签插入到哪。也就是说自定义组件插槽,内部的所有内容只能插入到某一个特定的插槽(可以说default也可以是具名),然后内部的所有内容就不能再出现template v-solt插槽了。

//等价于
<base-layout>
  <tempalte v-slot:default>
    <current-user>
      <template v-slot:footer="footer_slotProps">
      {{ slotProps }}
      // 相当于插槽里再插槽,所以会警告
      <template v-slot:other="otherSlotProps">
        slotProps is NOT available here
      </template>
      </template>
    </current-user>
  </tempalte>
</base-layout>
  • 含有v-slot指令的template标签内部,插入含有v-slot指令的自定义组件标签

<one-child>
  <template v-slot:header="headerProps">
		<!-- 可以访问 one-child 组件的 具名插槽header 的作用域-->
    {{headerProps}}
    <two-child v-slot:two_slot="slotProps">
		<!-- 可以访问 one-child 组件的 具名插槽header 的作用域-->
    {{headerProps}}
    <!-- 可以访问 two-child 组件的 具名插槽two_slot 的作用域--> 
    {{slotProps}}
    </two-child>
  </template>
</one-child>

解构插槽Prop

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

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

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

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

别名

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

定义后备内容,用于插槽 prop 是 undefined 的情形

动态插槽名

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

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

感悟

Vue官网:插槽 prop 允许我们将插槽转换为可复用的模板,这些模板可以基于输入的 prop 渲染出不同的内容。

// 原始
<ul>
  <li
    v-for="todo in filteredTodos"
    v-bind:key="todo.id"
  >
    {{ todo.text }}
  </li>
</ul>
// 个性定制
<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>

高级插槽

待更新