13.插槽的使用

认识插槽Slot

在开发中,我们会经常封装一个个可复用的组件:

  • 我们会通过props传递给组件一些数据,让组件来进行展示;
  • 但是为了让这个组件具备更强的通用性,我们不能将组件中的内容限制为固定的divspan等等这些元素;
    • 比如某种情况下我们使用组件,希望组件显示的是一个按钮,某种情况下我们使用组件希望显示的是一张图片;
  • 我们应该让使用者可以决定某一块区域到底存放什么内容和元素;
举个栗子:假如我们定制一个通用的导航组件 - NavBar
  • 这个组件分成三块区域:左边-中间-右边,每块区域的内容是不固定;
  • 左边区域可能显示一个菜单图标,也可能显示一个返回按钮,可能什么都不显示;
  • 中间区域可能显示一个搜索框,也可能是一个列表,也可能是一个标题,等等;
  • 右边可能是一个文字,也可能是一个图标,也可能什么都不显示;


    image.png
这个时候我们就可以来定义插槽slot:
  • 插槽的使用过程其实是抽取共性、预留不同
  • 我们会将共同的元素内容依然在组件内进行封装;
  • 同时会将不同的元素使用slot作为占位,让外部决定到底显示什么样的元素;
如何使用slot呢?
  • Vue中将 <slot> 元素作为承载分发内容的出口;
  • 在封装组件中,使用特殊的元素<slot>就可以为封装组件开启一个插槽
  • 该插槽插入什么内容取决于父组件如何使用;

插槽的基本使用

插槽分为匿名插槽(默认插槽),具名插槽, 作用域插槽

匿名插槽的使用

插槽的基本使用

1. 在组件SlotCpn.vue中预留匿名插槽
<template>
  <div>
    <div>组件开始</div>
    <slot></slot>
    <div>组件结束</div>
  </div>
</template>
2. 在组件App.vue中使用组件SlotCpn.vue,并在组件开始标签<slot-cpn>和结束标签</slot-cpn>插入的所有内容,都会被插入到组件的所有的匿名插槽一份
<template>
  <div>
    <!-- 不插入任何内容 -->
    <slot-cpn></slot-cpn>
    <slot-cpn>大风预警</slot-cpn>
    <slot-cpn>
      <button>添加</button>
    </slot-cpn>

    <!-- 使用组件时,不管在组件开始标签和结束标签传几项,都会全部传给默认插槽 -->
    <slot-cpn>
      <input type="text" />
      <button>提交</button>
    </slot-cpn>
    <slot-cpn>
      <i>why</i>
      <span>水果</span>
      <button>按钮</button>
    </slot-cpn>
  </div>
</template>

<script>
import SlotCpn from "./SlotCpn.vue";
export default {
  components: {
    SlotCpn,
  },
};
</script>

执行npm run serve,并在浏览器预览,可以看到,使用组件时在组件开始标签结束标签插入的所有内容都被插入到匿名插槽所在位置了

image.png

3. 如果在组件SlotCpn.vue中预留了多个匿名插槽
<template>
  <div>
    <div>组件开始</div>
    <slot></slot>
    <div>---分割线---</div>
    <slot></slot>
    <div>组件结束</div>
  </div>
</template>

执行npm run serve,并在浏览器预览,可以看到,在组件SlotCpn.vue中的每个匿名插槽的位置都被插入了使用组件时在组件开始标签结束标签插入的所有内容

image.png

4. 如果希望在使用组件SlotCpn.vue时,在组件开始标签结束标签中不插入内容时,显示默认内容,我们可以给插槽设置默认值
<template>
  <div>
    <div>组件开始</div>
    <slot>
      <h1>我是插槽1的默认值</h1>
    </slot>
    <div>---分割线---</div>
    <slot>
      <h1>我是插槽2的默认值</h1>
      <button>提交</button>
    </slot>
    <div>组件结束</div>
  </div>
</template>

执行npm run serve,并在浏览器预览,可以看到,使用组件时在组件开始标签结束标签不插入内容,在匿名插槽的位置会显示设置的默认值;
使用组件时在组件开始标签结束标签插入了内容,在组件SlotCpn.vue中的每个匿名插槽的位置都被插入了使用组件时在组件开始标签结束标签插入的所有内容

image.png

具名插槽的使用

如果希望把插入在组件开始标签结束标签中的内容的不同部分插入到组件的指定插槽位置,可以使用具名插槽

  • 具名插槽顾名思义就是给插槽起一个名字,<slot> 元素有一个特殊的 attribute:name,可以通过给name赋值,给插槽起名
  • 一个不带 name 的slot,会带有隐含的名字 default,即name='default',匿名插槽其实就是名字为default的插槽
  • 在使用组件时,可以在组件开始标签结束标签中间使用template标签,给template标签通过绑定v-slot:插槽名来指定template标签之间的内容插入到哪个具名插槽的位置

组件NavBar.vue中预留了三个具名插槽,名字分别为'left', 'right', 'center'

<template>
  <div class="tabs">
    <div class="left">
      <slot name="left">标题</slot>
    </div>
    <div class="center">
      <slot name="center">
        <input />
      </slot>
    </div>
    <div class="right">
      <slot name="right">
        <span>...</span>
      </slot>
    </div>
  </div>
</template>

<script>
  export default {
  }
</script>

<style scoped>
  .tabs {
    display: flex;
    align-items: center;
  }
  .center {
    flex: 1;
  }
</style>

组件App.vue

<template>
  <div>
    <nav-bar></nav-bar>
    <!-- 具名插槽的v-slot:插槽名字  只能放在template上 -->
    <nav-bar>
      <!-- 指定其中的内容插入到名字为left的插槽所在的位置 -->
      <template v-slot:left>
        <span >中誉</span>
      </template>
      <!-- 指定其中的内容插入到名字为right的插槽所在的位置 -->
      <template v-slot:right>
        <button>添加</button>
      </template>
      <!-- 指定其中的内容插入到名字为center的插槽所在的位置 -->
      <template #center>
        <div >详情1</div>
      </template>
    </nav-bar>
  </div>
</template>

<script>
  import NavBar from './NavBar.vue'
  export default {
    components: {
      NavBar
    }
  }
</script>

执行npm run serve,并在浏览器预览,可以看到,插入在组件开始标签结束标签中的内容的不同部分被插入到组件的指定插槽位置啦

image.png

具名插槽使用的时候缩写

v-slot: 替换为字符 #

组件App.vue

<template>
  <div>
    <nav-bar>
      <template #left>
        <span >中誉</span>
      </template>
      <template #right>
        <button>添加</button>
      </template>
      <template #center>
        <div >详情1</div>
      </template>
    </nav-bar>
  </div>
</template>

<script>
  import NavBar from './NavBar.vue'
  export default {
    components: {
      NavBar
    }
  }
</script>

动态插槽的使用

组件NavBar.vue中一个具名插槽的名字,是由组件被使用时,动态绑定属性name的值决定的

<template>
  <div class="tabs">
    <div class="left">
      <slot name="left">标题</slot>
    </div>
    <div class="center">
      <slot name="center">
        <input />
      </slot>
    </div>
    <div class="right">
      <!-- 动态插槽名 -->
      <slot :name="name">
        <span>...</span>
      </slot>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    name: {
      type: String,
      default: "",
    },
  },
};
</script>

<style scoped>
.tabs {
  display: flex;
  align-items: center;
}
.center {
  flex: 1;
}
</style>

组件App.vue

<template>
  <div>
    <!-- 使用组件时给组件的属性name传值 -->
    <nav-bar :name="name">
      <template v-slot:left>
        <span >中誉</span>
      </template>
      <!-- 动态插槽名 -->
      <template v-slot:[name]>
        <button>添加</button>
      </template>
      <template #center>
        <div >详情1</div>
      </template>
    </nav-bar>
  </div>
</template>

<script>
  import NavBar from './NavBar.vue'
  export default {
    components: {
      NavBar
    },
    data() {
      return {
        name: 'right'
      }
    }
  }
</script>

作用域插槽的使用

渲染作用域

在Vue中有渲染作用域的概念:

  • 父级模板里的所有内容都是在父级作用域中编译的;
  • 子模板里的所有内容都是在子作用域中编译的;

如下例子:

  • 子组件ChildCpn.vue中的变量title,可以在子组件的模板中使用
  • 在父组件App.vue中导入了子组件ChildCpn.vue,注册为ChildCpn,并在使用子组件<child-cpn>时给子组件的默认插槽传入<span>{{title}}</span>
  • 虽然<span>{{title}}</span>被写在子组件开始标签<child-cpn>和结束标签</child-cpn>之间,但依然是在App.vue的模板中,那它的作用域依然是App.vue的作用域,App.vue的data中没有title变量,所以会报错
    image.png

认识作用域插槽

但是有时候我们希望在使用子组件时,在子组件的开始标签结束标签之间为子组件的插槽插入内容时,可以获取到子组件中的变量,Vue给我们提供了作用域插槽
下面看一个案例:

  • 1.在App.vue中定义好数组names,通过props传递给子组件MyCpns.vue组件中
  • 2.MyCpns.vue组件中遍历names数组,设置匿名插槽,并把遍历的数组的每一项的索引,作为插槽属性的值,绑定在插槽上,
  • 这些插槽上绑定的属性名和属性值,除了name属性名和值外都被存储在一个对象中,以被获取
  • 3.通过v-slot:插槽名="slotProps"的方式获取到存储着绑定在对应插槽上的属性名和属性值(name属性名和值除外)的对象slotProps
  • 4.通过对对象slotProps成员访问的方式,获取到对应的值,以在父组件App.vue中使用

MyCpns.vue组件

<template>
  <div>
    <!--遍历数组names-->
    <div v-for="(item, index) in names" :key="item">
      <!-- 把数组中当前遍历的元素和索引以插槽属性值的方式绑定到插槽上 -->
      <slot :item="item" :index="index"></slot>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    names: {
      type: Array,
      default: () => [],
    },
  },
};
</script>

App.vue组件

<template>
  <div>
    <my-cpn :names="names">
      <template v-slot:default="slotProps">
        <span>{{ slotProps.index }} - {{ slotProps.item }}</span>
      </template>
    </my-cpn>
  </div>
</template>

<script>
import MyCpn from "./MyCpn.vue";
export default {
  components: {
    MyCpn,
  },
  data() {
    return {
      names: ["john", "kobe", "why"],
    };
  },
};
</script>

<style scoped></style>

如果我们的插槽是默认插槽default,那么在使用的时候 v-slot:default="slotProps"可以简写为v-slot="slotProps"

App.vue组件

<template>
  <div>
    <my-cpn :names="names">
      <template v-slot="slotProps">
        <span>{{ slotProps.index }} - {{ slotProps.item }}</span>
      </template>
    </my-cpn>
  </div>
</template>

<script>
import MyCpn from "./MyCpn.vue";
export default {
  components: {
    MyCpn,
  },
  data() {
    return {
      names: ["john", "kobe", "why"],
    };
  },
};
</script>

独占默认插槽

如果我们在使用组件时,只给组件的默认插槽插入内容,不管组件在定义时是否设置了具名插槽,组件的标签可以被当做插槽的模板来使用,这样,我们就可以将 v-slot 直接用在组件标签上,这种写法叫做:独占默认插槽
App.vue组件

<template>
  <div>
    <!-- 省略了template标签 组件的开始标签和结束标签中的内容会被插入到组件的默认插槽位置 -->
    <my-cpn :names="names" v-slot="slotProps">
       <span>{{ slotProps.index }} - {{ slotProps.item }}</span>
    </my-cpn>
  </div>
</template>

<script>
import MyCpn from "./MyCpn.vue";
export default {
  components: {
    MyCpn,
  },
  data() {
    return {
      names: ["john", "kobe", "why"],
    };
  },
};
</script>

同事给匿名插槽和具名插槽都插入内容

如果我们在使用组件时,给组件的默认插槽和具名插槽都插入内容,则不可省略template标签
App.vue组件

<template>
  <div>
    <my-cpn :names="names">
      <template v-slot="slotProps">
        <span>{{ slotProps.index }} - {{ slotProps.item }}</span>
      </template>
      <template v-slot:bottom="slotProps">
        <span>今天天气不错</span>
      </template>
    </my-cpn>
  </div>
</template>

<script>
import MyCpn from "./MyCpn.vue";
export default {
  components: {
    MyCpn,
  },
  data() {
    return {
      names: ["john", "kobe", "why"],
    };
  },
};
</script>

MyCpns.vue组件

<template>
  <div>
    <div v-for="(item, index) in names" :key="item">
      <!--匿名插槽-->
      <slot :item="item" :index="index"></slot>
      <!--具名插槽-->
      <slot name="why">coderwhy</slot>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    names: {
      type: Array,
      default: () => [],
    },
  },
};
</script>

跨组件插槽的使用

image.png

要实现这个目标,需要做:

1.给hy-table.vue设置若干个具名插槽
2.page-content使用子组件hy-table.vue时,给对应具名插槽所在位置插入内容时,插入的内容为设置同名具名插槽,供使用page-content.vue的父组件使用
3.goods.vue使用子组件page-content.vue,给对应插槽位置插入想要插入的内容就行了,插入的内容最终会被传入hy-table.vue中设置的同名具名插槽所在位置

hy-table.vue
我们使用element-plus封装了一个hy-table.vue组件,table中展示哪些列由propList决定,在table.vue中动态设置具名插槽

<template>
  <div class="hy-table">
    <el-table
      :data="listData"
      border
      style="width: 100%"
    >
      <template v-for="propItem in propList" :key="propItem.prop">
        <el-table-column v-bind="propItem" align="center" >
          <template #default="scope">
            <!-- 在默认插槽内容,动态注册具名插槽 -->
            <slot :name="propItem.slotName" :row="scope.row">{{scope.row[propItem.prop]}}</slot>
          </template>
        </el-table-column>
      </template>
    </el-table>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
  name: "hy-table",
  props: {
    listData: {
      type: Array,
      required: true,
    },
    propList: {
      type: Array,
      required: true,
    },
  },
  setup(props, { emit }) {
    return { };
  },
});
</script>

<style scoped lang="less">
.hy-table {
}
</style>

page-content.vue
在此组件中使用子组件hy-table.vue,给hy-table.vue一部分具名插槽插入固定形式的内容,另一部分具名插槽内部重新注册同名具名插槽,供使用page-content.vue的父组件自动义插入内容

<template>
  <div class="page-content">
    <hy-table
      :list-data="list"
      :prop-list="propList"
    >
      <!-- table 列公共插槽:status/createAt/updateAt/operate -->
      <template #status="scope">
        <el-button
          size="mini"
          :type="scope.row.enable ? 'primary' : 'danger'"
          >{{ scope.row.enable ? "启用" : "禁用" }}</el-button
        >
      </template>
      <template #createAt="scope">
        <span>{{ $filter.formatUtcTime(scope.row.createAt) }}</span>
      </template>
      <template #updateAt="scope">
        <span>{{ $filter.formatUtcTime(scope.row.updateAt) }}</span>
      </template>
      <template #operate">
        <div class="operate">
          <el-button
            type="text"
            icon="el-icon-edit"
            >编辑</el-button
          >
          <el-button
            type="text"
            icon="el-icon-delete"
            >删除</el-button
          >
        </div>
      </template>
      <!-- 列非公共插槽:非公共插槽中注册具名插槽,供页面使用 -->
      <template
        v-for="item in otherPropSlots"
        :key="item.prop"
        #[item.slotName]="scope"
      >
        <template v-if="item.slotName">
          <slot :name="item.slotName" :row="scope.row"></slot>
        </template>
      </template>
    </hy-table>
  </div>
</template>

<script lang="ts">
import { computed, defineComponent, ref, watch } from "vue";
import HyTable from "@/base-ui/table";
import { useStore } from "@/store";
export default defineComponent({
  name: "page-content",
  components: {
    HyTable,
  },
  props: {
    propList: {
      type: Array,
      required: true,
    },
    pageName: {
      type: String,
      required: true,
    },
  },
  setup(props, { emit }) {
    //1.获取列数据
    const store = useStore();
    const pageInfo = ref({
      pageSize: 10,
      pageNum: 1,
    });
    const getPageData = (queryInfo: any = {}) => {
      store.dispatch("system/getPageListAction", {
        pageName: props.pageName,
        queryInfo: {
          offset: pageInfo.value.pageSize * (pageInfo.value.pageNum - 1),
          size: pageInfo.value.pageSize,
          ...queryInfo,
        },
      });
    };
    getPageData();
    const list = computed(() =>
      store.getters["system/pageListData"](props.pageName),
    );

    //2.从内容配置的属性列表中过滤出有非公共插槽的属性配置数组
    const publicPropSlots = ["status", "createAt", "updateAt", "operate"];  
    const otherPropSlots = props.propList.filter((prop: any) => {
      return prop.slotName && !publicPropSlots.includes(prop.slotName);
    });

    return {
      list,
      otherPropSlots,
    };
  },
});
</script>

<style scoped lang="less">
.page-content {
  padding: 20px;
  border-top: 20px solid #f5f5f5;
}
</style>

goods.vue
在页面中使用page-content组件,并给具名插槽插入内容,最终内容会被插入在hy-table.vue中同名具名插槽所在的位置

<template>
  <div class="goods">
    <page-content
      :prop-list="propList"
      pageName="goods"
    >
      <template #img="scope">
        <el-image
          style="width: 60px; height: 60px"
          :src="scope.row.imgUrl"
          :preview-src-list="[scope.row.imgUrl]"
        >
        </el-image>
      </template>
    </page-content>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import PageContent from "@/components/page-content";
export default defineComponent({
  name: "goods",
  components: {
    PageContent,
  },
  setup() {
    const propList = [
      { prop: "name", label: "商品名", minWidth: "180" },
      { prop: "oldPrice", label: "原价", minWidth: "180" },
      { prop: "newPrice", label: "现价", minWidth: "180" },
      { prop: "imgUrl", label: "图片", minWidth: "180", slotName: "img" },
      { prop: "status", label: "状态", minWidth: "180", slotName: "status" },
      {
        prop: "createAt",
        label: "创建时间",
        minWidth: "180",
        slotName: "createAt",
      },
      {
        prop: "updateAt",
        label: "更新时间",
        minWidth: "180",
        slotName: "updateAt",
      },
      {
        label: "操作",
        minWidth: "180",
        slotName: "operate",
      },
    ],
 
    return {
      propList,
    };
  },
});
</script>

<style scoped lang="less"></style>

此文档主要内容来源于王红元老师的vue3+ts视频教程

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355

推荐阅读更多精彩内容