通俗易懂让你认识到 slot 插槽的魅力__Vue.js
发布于 4 年前 作者 banyungong 1745 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

前言

我们都知道组件的目的是为了可重用,开发中我们有的时候需要用这个组件,但是组件的功能又不能完全满足需求。我们想要在一个组件内加入一些开发者可自定义的一些内容,这个时候就可以在组件中利用slot插槽更好的对组件进行扩展与自定义。

vue 的 slot 插槽提供给了开发者更加灵活的方式去定义和扩展组件。可以通过slot给组件中添加额外的内容,也可以通过slot由组件内分发不同的内容

通过本篇文章你将了解到 vue 中 slot 插槽的使用方式。通过实现一个Cell组件和内容分发的例子让你对 Slot 插槽有一个更深刻的认识。

基本使用

  1. 在组件标签之间包裹的结构内容会被组件收集。
  2. 在组件内通过<slot>标签引入收集的内容,引入内容的展示位置根据<slot>标记的位置而定。
  3. 如果组件内没有定义slot标签,则收集的内容将会被舍弃
  4. 组件内<slot>标签之间可以定义后备内容,当外部组件标签内没有包裹任何结构内容时,将会默认展示后备内容
  5. 父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的

后文对组件标签之间包裹的结构内容称为扩展内容

举个例子:

views/Slot

<template>
  <div class='container'>
    <demo data="组件上的属性">
      <h1>你们好</h1>
      <div>我是yongcode</div>
      <p>{{msg}}</p>
      <!-- 这里data为undefined,因为当前模板是父级模板,编译时是在父级作用域编译的 -->
      <h4>{{data + ''}}</h4>
    </demo>

    <demo></demo>
  </div>
</template>

<script>
import demo from '@/components/Slot'
export default {
  data () {
    return {
      msg: '今天给大家介绍一下slot插槽',
    }
  },
  components: {
    demo
  }
}
</script>

components/Slot

<template>
  <div class='container'>
    你好啊
    <slot>
      <h1>这是插槽的后备内容</h1>
    </slot>
  </div>
</template>

<script>
export default {
  data () {
    return {
      msg: ''
    }
  },
}
</script>

最后效果如下:

更灵活的使用

具名插槽/多插槽的使用

我们在设计一个组件的时候,并不能确定使用者会使用到哪些扩展项。在 vue 中通过具名插槽来区分不同的扩展内容,更加灵活的对应扩展的内容进行控制。

  1. 在扩展内容中用 <template> 标签包裹一个结构内容,用v-slot对该结构进行命名(标记),组件中通过给<slot> 标签对应的name属性,定向收集对应的结构内容。
  2. 对于扩展内容中没有用<template> 标签包裹的部分将会作为default内容,被default插槽收集。<slot>标签不指定 name 则视为 default 插槽。
  3. v-slot 可以简写为 #
  4. <slot>插槽可以多次被使用

举个例子:

这里定义了三个插槽;header、footer、default

views/Slot

<template>
  <div class='container'>
    <demo data="组件上的属性">
      <template v-slot:footer>
        <div class="footer"> 我是footer </div>
      </template>

        <div>{{msg}}</div>
      <!-- 未标记的部分会默认被default插槽收集,同下结构-->
      <!-- <template v-slot:default>
        <div> {{msg}} </div>
      </template> -->
      
      <template #header>
        <div class="header"> 我是header </div>
      </template>
    </demo>
  </div>
</template>

<script>
import demo from '@/components/Slot'
export default {
  data () {
    return {
      msg: '今天给大家介绍一下slot插槽',
    }
  },
  components: {
    demo
  }
}
</script>
<style lang="scss" scoped>
  .footer{
    font-size: 20px;
    color: red;
  }
  .header{
    font-size: 24px;
    font-weight: 600;
  }
</style>

components/Slot

<template>
  <div class='container'>
   你好啊
    <slot name="header"></slot>
    <slot name="header"></slot>
    <slot></slot>
    <!-- 未命名的默认名为default -->
    <slot name="default"></slot>
    <slot name="footer"></slot>
  </div>
</template>

<script>
export default {
  props:['data'],
}
</script>

效果如下:

需要注意的是 声明插槽内容的时候,同名插槽会覆盖

最后显示的是header2

<template #header>
  <div class="header"> 我是header </div>
</template>
<template #header>
  <div class="header"> 我是header2 </div>
</template>

对于插槽内容的样式部分

  1. 不管你是写在外部还是组件内定义样式,都可以使样式生效。最好是结构写在哪就在哪给定样式。
  2. <slot></slot> 标签不会实际渲染成一个html标签结构,只是作为一个标记。让对应内容插入正确的位置,所有你在slot标签是定义class是没用的。原则上,slot标签本身不应该对外部的内容产生影响,它的存在只是为了让外部的内容正确的插入指定的位置。

作用域插槽

有的时候我们需要组件内的属性来控制扩展内容,但是在父级模板中不能直接拿到组件内的属性。这时候就需要作用域插槽,将组件内的属性暴露给外部模板上下文。

  1. 在组件内<slot>标签上绑定属性,扩展内容中通过给v-slot 指定一个变量来接受组件内绑定的值
  2. 如果扩展内容没有标记命名,只作为默认内容被收集和使用,这是组件标签就会被当中插槽的模板来使用,这样就可以把v-slot 直接用在组件上

举个例子:

views/Slot

<template>
  <div class='container'>
    <demo v-slot="slotProps">
      <div> {{msg}} </div>
      <div>{{slotProps.msg1}}</div>
      <div>{{slotProps.msg2}}</div>
    </demo>

    <!-- 对内部的值直接结构赋值使用 -->
    <demo v-slot="{msg1,msg2}">
      <div>{{msg}} </div>
      <div>{{msg1}}</div>
      <div>{{msg2}}</div>
    </demo>
  </div>
</template>

<script>
import demo from '@/components/Slot'
export default {
  data () {
    return {
      msg: '今天给大家介绍一下slot插槽',
    }
  },
  components: {
    demo
  }
}
</script>

components/Slot

<template>
  <div class='container'>
    你好啊
    <slot :msg1="msg1" :msg2="msg2"></slot>
  </div>
</template>

<script>
export default {
  data () {
    return {
      msg1: '组件内的数据1',
      msg2: '组件内的数据2'
    }
  },
}
</script>

效果如下:

总结与反思

实现 VantUI 中 Cell

通过具名插槽和插槽的备用内容写一个可扩展的组件

views/Slot

<template>
  <div class="container">
    <!-- 正常使用 -->
    <h4>正常使用</h4>
    <demo title="单元格1" value="内容1" label="描述信息1" :icon="img" iconSize="30"></demo>
    <!-- 展示默认内容 -->
    <h4>展示默认内容 </h4>
    <demo></demo>
    <!-- 自定义插槽内容 -->
    <h4>自定义插槽内容 </h4>
    <demo>
      <!-- 自定义左图标 -->
      <template #icon>
        <img width="20" height="20" src="../assets/logo.png" alt="">
      </template>
      <!-- 自定义title部分 -->
      <template #title>
        <span class="title">单元格</span>
        <span class="type">标签1</span>
      </template>
      <!-- 自定义value部分,多行溢出打点 value部分使用默认default槽-->
      <template>
        <span
          class="value"
        >内容内容内容内容内容内容内容内容内容内内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容内容容内容内容内容内容内容</span>
      </template>
      <!-- 自定义右图标 -->
      <template #right-icon>
        <img width="20" height="20" src="../assets/logo.png" alt="">
      </template>
      <!-- 自定义label部分 图片文字分栏 -->
      <template #label>
        <div class="content">
          <img
            src="https://dss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=2267599742,1201218992&fm=26&gp=0.jpg"
            alt
          />
          <div
            class="text"
          >今天给大家介绍一些vue slot 插槽的使用方式,今天给大家介绍一些vue slot 插槽的使用方式,今天给大家介绍一些vue slot 插槽的使用方式,今天给大家介绍一些vue slot 插槽的使用方式,今天给大家介绍一些vue slot 插槽的使用方式,</div>
        </div>
      </template>
      <!-- 额外的插槽内容 -->
      <template #extra>
        <img width="20" height="20" src="../assets/arrow_right.png" alt="">
      </template>
    </demo>
  </div>
</template>

<script>
import img from '@/assets/logo.png';
import demo from "@/components/Slot_1";
export default {
  data() {
    return {
      msg: "今天给大家介绍一下slot插槽",
      img
    };
  },
  components: {
    demo,
  },
};
</script>

<style lang="scss" scoped>

.title {
  font-size: 16px;
  color: #00bcd4;
}
.type {
  margin-left: 5px;
  background-color: #ee0a24;
  font-size: 10px;
  padding: 0 4px;
  color: #fff;
}
// 让value多行溢出打点
.value {
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2; //行数
  -webkit-box-orient: vertical;
  color: #999;
}
.content{
  display: flex;
  img{
    width: 100px;
    height: 100px;
  }
  .text{
    padding:  0 8px;
    box-sizing: border-box;
  }
}
</style>

components/Slot_1

<template>
  <div class="cell">
    <div class="cell_top">
      <div class="cell_top_left_box">
        <!-- 左侧icon -->
        <slot name="icon">
          <!-- 对应右icon属性的利用插槽的后备内容特性进行展示 -->
          <img v-if="icon" :width="iconSize" :height="iconSize" :src="icon" alt="">
        </slot>
        <!-- title -->
        <slot name="title">
          <div class="cell_top_title">{{title}}</div>
        </slot>
      </div>
      <div class="cell_top_right_box">
        <!-- value -->
        <slot>
          <div class="cell_top_value">{{value}}</div>
        </slot>
        <!-- right -->
        <slot name="right-icon"></slot>
        <!-- 额外槽 -->
        <slot name="extra"></slot>
      </div>
    </div>
    <div class="label_box">
      <!-- label -->
      <slot name="label">
        <div class="cell_content">{{label}}</div>
      </slot>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    title: {
      type: String,
      default: "标题",
    },
    value: {
      type: String,
      default: "内容",
    },
    label: {
      type: String,
      default: "描述信息",
    },
    icon:{
      type: String,
      default: "",
    },
    iconSize:{
      type: [String,Number],
      default: 20
    }
  },
  data() {
    return {
      msg: "组件内的数据",
      msg2: "组件内的数据2",
    };
  },

};
</script>

<style lang="scss" scoped>
%padding{
    width: 100%;
    padding: 10px 16px;
    box-sizing: border-box;
}

.cell {
  width: 100%;
  display: felx;
  flex-direction: column;
  background-color: #fff;
  font-size: 14px;
  .cell_top {
    @extend %padding;
    display: flex;
    justify-content: space-between;
    .cell_top_left_box {
      display: flex;
      align-items: center;
      max-width: 20%;
      min-width: max-content;
      margin-right: 8px;
      .cell_top_title {
        width: 100%;
        font-size: 14px;
        color: #333;
      }
    }
    .cell_top_right_box {
      display: flex;
      align-items: center;
      max-width: 78%;
      .cell_top_value {
        font-size: 14px;
        color: #999;
      }
    }
  }
  .label_box {
    @extend %padding;
    .cell_content {
      width: 100%;
      box-sizing: border-box;
      font-size: 12px;
      color: #999;
    }
  }
}
</style>

效果如下:

PC端 移动端

利用作用插槽的特性做内容分发

通过作用域插槽的特性,不仅可以根据具体需求分发数据,还可以更好的复用函数。下面示例中将toDetail函数暴露给外部做跳转。

views/StudentCard

<template>
  <div style="display:flex">
    <!-- 用该组件展示个人信息 -->
    <card>
      <!-- students 是暴露的数据 -->
      <!-- toDetail 是暴露的方法,如果不使用点击跳转可以忽略-->
      <template #default="{students}">
        <ul style="cursor:unset" v-for="student in students" :key="student.id">
          <li>姓名:{{student.name}}</li>
          <li>学号:{{student.id}}</li>
          <li>班级:{{student.class}}</li>
          <li>爱好:{{student.like}}</li>
        </ul>
      </template>
    </card>

    <!-- 用该组件展示个人属性 -->
    <card>
      <template #default="{students,toDetail}">
        <ul v-for="student in students" :key="student.id" @click="toDetail(student.id)">
          <li>姓名:{{student.name}}</li>
          <li>体力:{{student.VIT}}</li>
          <li>攻击:{{student.ATK}}</li>
          <li>防御:{{student.DEF}}</li>
        </ul>
      </template>student
    </card>
    <!-- 用该组件展示个人成绩 -->
    <card>
      <template #default="{students,toDetail}">
        <ul v-for="student in students" :key="student.id" @click="toDetail(student.id)">
          <li>姓名:{{student.name}}</li>
          <li>语文:{{student.performances.Chinese}}</li>
          <li>数学:{{student.performances.Math}}</li>
          <li>英文:{{student.performances.English}}</li>
        </ul>
      </template>
    </card>
  </div>
</template> <script>
import Card from "@/components/StudentCard";
export default {
  components: { 
    Card 
  },
};
</script>

<style lang="scss" scoped>
ul {
  width: 200px;
  background-color: #fff;
  border: 1px solid #333;
  border-radius: 4px;
  padding: 15px;
  box-sizing: border-box;
  list-style: none;
  cursor: pointer;
}
</style>

components/Slot_2

<template>
  <div class="container">
    <!-- 绑定数据 -->
    <slot :students="students" :toDetail="toStudentDetail">
      <ul v-for="person in students" :key="person.id">
        <li>{{person.name}}</li>
        <li>{{person.age}}</li>
      </ul>
    </slot>
  </div>
</template>

<script>
export default {
  data() {
    return {
      students: [
        {
          id: "1",
          name: "张三",
          class: "三年二班",
          like: "篮球",
          age: "18",
          VIT: "200",
          ATK: "10",
          DEF: "5",
          performances: {
            Chinese: "90",
            Math: "80",
            English: "70",
          },
        },
        {
          id: "2",
          name: "李四",
          class: "三年一班",
          like: "足球",
          age: "20",
          VIT: "500",
          ATK: "18",
          DEF: "10",
          performances: {
            Chinese: "80",
            Math: "85",
            English: "75",
          },
        },
        {
          id: "3",
          name: "赵五",
          class: "三年三班",
          like: "篮球",
          age: "20",
          VIT: "800",
          ATK: "25",
          DEF: "15",
          performances: {
            Chinese: "100",
            Math: "90",
            English: "80",
          },
        },
      ],
    };
  },
  methods:{
    // 给slot绑定属性,将该方法暴露给外部
    toStudentDetail(id){
      alert('进行跳转操作,当前学生id为' + id)
    }
  }
};
</script>

<style lang="scss" scoped>
.container {
  width: 100%;
  background-color: #f8f8f8;
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

</style>

效果如下:

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: YongCode 原文链接:https://juejin.im/post/6863343746080210951

回到顶部