概述
轮播图相信每个做前端的都用过,但是基本上都是直接使用组件库现成的,或者一些比较流行的轮播图插件比如swiper,现在就带你手写vue轮播图组件。你将会学到:
- 发布订阅和跨组件通信的解决方案。
- 组件之间数据相互获取的技巧
最终目标
实现原理
在原生js实现轮播图中,是比较麻烦的,具体原生js的写法,大家可以自行搜索,这里介绍使用框架来实现轮播组件,相比原生js的写法来说,你一定看完觉得框架使用起来实现这个功能相当简单。原理如下:
- 首先我们将所有的一个个item绝对定位然后叠在一起。
- 动态根据初始选中项的索引,然后决定显示对应索引项的那一个item,其他的隐藏(v-if)
- 至于切换动画我们可以使用transition组件来过渡完成
- 至于左右切换箭头,和指示器,可以根据用户自定义配置来决定是否显示
是不是觉得思路很简单,好吧,我们开始写吧
项目结构
自行搭建vue2项目的脚手架,目录层级结构如下
- components
- Carousel
- Carousel.vue(暴露给用户使用的组件)
- CarouselArrow.vue(左右箭头切换组件)
- CarouselIndicator.vue(底部指示器组件)
- CarouselItem.vue(一个个轮播item组件)
- index.js(按需导出文件)
- Pubsub.js(用于跨组件通信)
- Carousel
App.vue
<template>
<div id="app">
<!-- 添加自定义配置项,具体用途请看Carousel组件props -->
<carousel
:height="300"
:initialIndex="2"
trigger="click"
:autoplay="true"
:interval="2000"
:showArrow="true"
:showIndicator="true"
@change="handleCarouselChange"
>
<carousel-item v-for="(item, index) in list" :key="index">
<div class="img-item">
<img :src="item.src" />
</div>
</carousel-item>
</carousel>
</div>
</div>
</template>
<script>
import { Carousel, CarouselItem } from "./components/Carousel/index";
export default {
name: "App",
components: {
Carousel,
CarouselItem,
},
data() {
return {
//轮播数据
list: [
{
id: 1,
src: require("@/assets/img/01.webp"),
},
{
id: 2,
src: require("@/assets/img/02.webp"),
},
{
id: 3,
src: require("@/assets/img/03.webp"),
},
{
id: 4,
src: require("@/assets/img/04.webp"),
},
],
};
},
methods: {
//切换的时候订阅的事件
handleCarouselChange(item) {
console.log(item);
},
},
};
</script>
<style lang="less">
.img-item {
img {
width: 500px;
height: 300px;
}
}
}
</style>
components/Carousel/Carousel.vue
<template>
<div
class="carousel"
:style="{ height: height + 'px' }"
@mouseenter.stop="handleMouseEnter"
@mouseleave.stop="handleMouseLeave"
>
<!--S 主体内容 -->
<slot></slot>
<!--E 主体内容 -->
<!--S 指示器 -->
<div class="carousel-indicator-wrap" v-if="showIndicator">
<carousel-indicator
:length="len"
:currentIndex="currentIndex"
@carouselIndicatorChange="carouselIndicatorChange"
></carousel-indicator>
</div>
<!--E 指示器 -->
<!--S 切换箭头 -->
<transition-group name="arrow-animate">
<carousel-arrow
v-if="currentArrowStatus && showArrow"
@click="arrowChange('prev')"
class="left-arrow arrow"
key="prev"
><</carousel-arrow
>
<carousel-arrow
v-if="currentArrowStatus && showArrow"
@click="arrowChange('next')"
class="right-arrow arrow"
key="next"
>></carousel-arrow
>
</transition-group>
<!--E 切换箭头 -->
</div>
</template>
<script>
import Pubsub from "./utils";//跨组件通信,本质发布订阅
import CarouselIndicator from "./CarouselIndicator.vue";//指示器
import CarouselArrow from "./CarouselArrow.vue";//切换剪头
export default {
components: { CarouselIndicator, CarouselArrow },
props: {
// 高度
height: {
type: Number,
default: 300,
},
// 初始显示项
initialIndex: {
type: Number,
default: 0,
},
// 指示器的触发方法
trigger: {
type: String,
default: "click",
},
// 是否自动播放
autoplay: {
type: Boolean,
default: true,
},
// 自动播放时间间隔
interval: {
type: Number,
default: 3000,
},
// 是否显示左右箭头
showArrow: {
type: Boolean,
default: false,
},
// 是否显示指示器
showIndicator: {
type: Boolean,
default: false,
},
},
data() {
return {
// 当前活跃项,用户可自定义,默认为0
currentIndex: this.initialIndex || 0,
// 轮播定时器标识
timer: null,
// 轮播项的数据长度,这里我们可以使用插槽获取到
len: this.$slots.default.length,
// 当前箭头滑过的显示状态
currentArrowStatus: false,
};
},
mounted() {
// 挂载的时候就开始自动轮播
this.autoPlay();
},
methods: {
// 自动轮播
autoPlay() {
this.timer = setInterval(() => {
this.handlePlay();
}, this.interval);
},
/**
* @Description 轮播逻辑
* 轮播的下标在[0,lenght-1]之前,超出下标之后,要重置
**/
handlePlay(type) {
switch (type) {
case "prev":
this.currentIndex--;
if (this.currentIndex < 0) {
this.currentIndex = this.len - 1;
}
//发布更新事件,告知CarouselItem组件进行切换
Pubsub.notice("updateCurrentIndex", this.currentIndex);
break;
case "next":
default:
this.currentIndex++;
if (this.currentIndex >= this.len) {
this.currentIndex = 0;
}
//发布更新事件,告知CarouselItem组件进行切换
Pubsub.notice("updateCurrentIndex", this.currentIndex);
break;
}
// 切换后发布change事件,告知用户,当前项已经切换了,参数为当前索引
this.$emit("change", this.currentIndex);
},
// 切换箭头点击触发
arrowChange(type) {
this.handlePlay(type);
},
// 鼠标进入
handleMouseEnter() {
this.stopInterval();
this.currentArrowStatus = true;
},
// 鼠标离开
handleMouseLeave() {
this.autoPlay();
this.currentArrowStatus = false;
},
// 指示器点击切换
carouselIndicatorChange(index) {
this.currentIndex = index;
//发布更新事件
Pubsub.notice("updateCurrentIndex", this.currentIndex);
},
// 清除定时器
stopInterval() {
clearInterval(this.timer);
this.timer = null;
},
},
beforeDestroy() {
// 销毁前清空订阅的事件和定时器
this.clearInterval();
Pubsub.clearEventQueen();
},
};
</script>
<style lang="less">
.carousel {
overflow: hidden;
position: relative;
width: 100%;
overflow: hidden;
.arrow {
position: absolute;
top: 50%;
transform: translateY(-50%);
}
.left-arrow {
left: 10px;
}
.right-arrow {
right: 10px;
}
.carousel-indicator-wrap {
position: absolute;
bottom: 12px;
left: 50%;
transform: translateX(-50%);
}
.arrow-animate-enter-active,
.arrow-animate-leave-active {
transition: opacity 0.3s;
}
.arrow-animate-enter,
.arrow-animate.leave-top {
opacity: 0;
}
}
</style>
</style>
components/Carousel/CarouselItem.vue
<template>
<transition name="carousel-item-animate">
<!-- 只显示当前活跃项 -->
<div class="carousel-item" v-if="key == currentIndex">
<slot></slot>
</div>
</transition>
</template>
<script>
import Pubsub from "./utils";
export default {
data() {
return {
/*
*为什么获取这些这么麻烦,因为我们组件内部需要的东西,需要我们自己想办法,组件使用者只需要关注配置
*/
// 在App中我们绑定了每个item组件的key值,可以通过当前组件的vnode对象获取到
key: this.$vnode.key,
// 当前活跃项,我们需要获取到父组件当中Carousel的currentIndex
currentIndex: this.$parent.currentIndex,
};
},
mounted() {
// 挂载的时候订阅更新当前活跃索引,然后更新数据,触发视图更新
Pubsub.subscribe("updateCurrentIndex", (...arg) => {
// 参数为当前活跃索引
this.currentIndex = arg[0];
});
},
};
</script>
<style lang="less">
.carousel-item {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
/*
*下面的是轮播过渡的重点,本质就是前一个组件像左边移动自身的长度,下一个显示的组件从右边距离自身100%的地方进来,然后过程加上动画
*/
// 离开进入中
.carousel-item-animate-enter-active,
.carousel-item-animate-leave-active {
transition: all 0.3s linear;
}
// 进入初始状态
.carousel-item-animate-enter {
transform: translateX(100%);
}
// 进入后
.carousel-item-animate-enter-to {
transform: translateX(0);
}
//离开后的状态
.carousel-item-animate-leave-to {
transform: translateX(-100%);
}
</style>
components/Carousel/CarouselArrow.vue
<template>
<!-- 点击的时候进行切换,发布事件给 Carousel组件-->
<div class="carousel-arrow" @click="$emit('click')">
<slot></slot>
</div>
</template>
<style lang="less">
.carousel-arrow {
width: 50px;
height: 50px;
background: rgba(0, 0, 0, 0.5);
border-radius: 50%;
color: #fff;
text-align: center;
line-height: 50px;
font-size: 20px;
cursor: pointer;
}
</style>
components/Carousel/CarouselIndicator.vue
<template>
<!-- 指示器在当前活跃的时候高亮显示,点击的时候发布切换事件逻辑 -->
<div class="carousel-indicator">
<span
v-for="(item, index) in length"
:key="index"
:class="[currentIndex == index ? 'active' : '']"
@click="$emit('carouselIndicatorChange', index)"
></span>
</div>
</template>
<script>
export default {
props: {
// 数据长度
length: {
type: Number,
default: 0,
},
// 当前活跃项
currentIndex: {
type: Number,
default: 0,
},
},
};
</script>
<style lang="less">
.carousel-indicator {
span {
display: inline-block;
width: 30px;
height: 3px;
background-color: rgba(0, 0, 0, 0.4);
margin: 0 5px;
cursor: pointer;
}
span.active {
background-color: #fff;
}
}
</style>
components/Carousel/Pubsub.js
- 对于发布订阅模式不清楚的,请自行查阅资料
- 这里只对基本功能进行封装
- vue内部实现自定义事件,本质也是这种方法,只不过这里我们简化了
/**
* @Description 发布订阅的简单封装实现
* 使用这种方式实现跨组件的通信理由如下:
* 1.组件是给使用者使用的,不能过多要求使用者传递过多你内部逻辑实现需要的参数
* 2.跨组件通信我们其实还可以使用vuex,但是组件库中,涉及到的只是单个组件,使用vuex也就没必要了
**/
class Pubsub {
constructor() {
this.queenList = [];
}
// 订阅事件
subscribe(eventName, fn) {
let cur = this.queenList.find((item) => item.eventName == eventName);
if (cur) {
cur.callback.push(fn);
} else {
this.queenList.push({
eventName,
callback: [fn],
});
}
}
// 发布事件
notice(eventName, ...args) {
let cur = this.queenList.find((item) => item.eventName == eventName);
if (cur) {
cur.callback.forEach((fn) => {
fn(...args);
});
}
}
// 清空事件队列
clearEventQueen() {
this.queenList = [];
}
}
export default new Pubsub();
components/Carousel/index.js
//导出组件暴露给用户
import Carousel from "./Carousel";
import CarouselItem from "./CarouselItem";
export { Carousel, CarouselItem };
总结
知道基本思路之后,其实组件写轮播图还是蛮简单的,就是跨组件之间数据交互麻烦了点,但是组件既然是给用户使用,我们就只能自己想办法解决内部传值的问题。