编写可复用的组件,我们可以收获很多

7,629 阅读9分钟

编写可复用的组件在实际开发中是非常常见的需求,根据需求的不同会有很多不同的组件出现,有简单的比如Alert这样的弹窗组件,也有复杂些的日历组件等等

今天我们一起来写写组件(Vue组件),看看从中能不能掌握什么知识点吧!!!

编写一个轮播图组件

轮播图组件想必是使用最多的组件之一了,尤其是每个首页当中必定会出现它的身影。

那么,我们就闲言少叙了,实现一个适用于移动端上的轮播图组件吧

先来爆照

效果图和项目的目录已经呈现在观众眼前了,接下来我们先从App.vue文件开始搞起

先划个重点:要实现组件的开发,先简单看一下下面的3条逻辑

核心逻辑

  1. 初始化轮播图
  2. 自动播放
  3. 左右切换轮播图

工欲善其事必先利其器,首先不管怎样,先把组件引入一番再说

引入组件

组件的使用向来是先引入再说,毫不例外的,看下面的组件引用

// App.vue文件  ->  js部分

<script>
// 引入Swiper和SwiperItem组件
import Swiper from './components/Swiper/Swiper';
import SwiperItem from './components/Swiper/SwiperItem';

export default {
    // 组件内注册组件
    components: {
        Swiper,
        SwiperItem
    }
}
</script>

js部分已经将引入的组件注册到了当前App组件中,下面就开始在模板中使用它们

// App.vue文件  ->  html部分

<template>
    <div id="app">
        <Swiper v-model="currentId">
            <SwiperItem :id="currentId">
                <div>第一张图</div>
            </SwiperItem>
            <SwiperItem :id="currentId">
                <div>第二张图</div>
            </SwiperItem>
            <SwiperItem :id="currentId">
                <div>第三张图</div>
            </SwiperItem>
        </Swiper>
    </div>
</template>

小朋友,你是否有很多问号?别着急,且听风吟

Swiper组件里放了3个SwiperItem组件,而且SwiperItem组件里实现的内容是完全相同的

所以,没必要写3遍,直接用一个数组通过v-for渲染出来就OK了

数据

这里我简单用node写了个接口,返回轮播图的数据,如果不想写个接口的话,我直接把数据贴出来,让大家直观的看到

mock数据

// 轮播图数据
const albums = [
    { "id": 1, "title": "叶惠美", "public": 2003, "song": '晴天', "img": "https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=281827478,1045705647&fm=26&gp=0.jpg"},
    { "id": 2, "title": "七里香", "public": 2004, "song": '七里香', "img": "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1584873891083&di=7892d2142e6aba7e203d270e20599235&imgtype=0&src=http%3A%2F%2Fpic.rmb.bdstatic.com%2Fc1505303db7c257f248adc87b6e22fd5.jpeg"},
    { "id": 3, "title": "十一月的萧邦", "public": 2005, "song": '夜曲', "img": "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1584873911041&di=51fc38d5805edc63fdd7301dbcef316f&imgtype=0&src=http%3A%2F%2Fzximg.ingping.com%2Fueditor%2Fjsp%2Fupload%2F201705%2F201705031153520358724.jpg"},
    { "id": 4, "title": "依然范特西", "public": 2006, "song": '听妈妈的话', "img": "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1584873937432&di=ebf0092e78d5499f54728eeb43449414&imgtype=0&src=http%3A%2F%2Fimg1.dongqiudi.com%2Ffastdfs2%2FM00%2F66%2FBA%2FChOqM1rO_0mADp5rAACEhAjn7aY043.jpg"}
];

我写的接口是/getalbums,返回的是json格式,{code: 0, data: albumes}

下面我还是按照正常的请求来操作,在之前的目录结构中,大家应该看到了api/shop.js文件,这里是我封装好的用来请求轮播图数据的代码

// api/shop.js文件

import axios from 'axios';
// 拦截返回的响应数据
axios.interceptors.response.use(res => res.data);

export default {
    async getAlbums() {
        let { data } = await axios.get('/api/getalbums');
        return data;
    }
}

请求数据

现在让我们回到App.vue这里,开始进行请求操作,然后v-for遍历数组并渲染SwiperItem组件

// App.vue文件  ->  js部分

<script>
import Swiper from './components/Swiper/Swiper';
import SwiperItem from './components/Swiper/SwiperItem';
// 引入封装好的请求方法
// @是webpack配的alias别名,指代src目录
import shop from '@/api/shop';

export default {
    data() {
        // 轮播图数据
        sliders: [],
        // 当前id,用来显示对应的图片
        currentId: 1
    },
    async mounted() {
        // 请求数据并更新slider数组
        this.sliders = await shop.getAlbums();   
    },
    components: {
        Swiper,
        SwiperItem
    }
}
</script>

渲染数据

渲染数据的部分要交给我们的模板来处理了

// App.vue文件  ->  html部分

<template>
    <div id="app">
        <Swiper v-model="currentId" v-if="sliders.length">
            <template v-for="item in sliders">
                <SwiperItem :key="item.id" :id="item.id">
                    <img :src="item.img" />
                </SwiperItem>
            </template>
        </Swiper>
    </div>
</template>

上面模板为什么写成这样?

  1. v-model也可以在组件上进行绑定
    • 一个组件上的v-model默认会利用名为value的prop和名为input的事件
  2. v-if控制组件是在拿到数据后才进行渲染
  3. SwiperItem上的id动态属性是用来区分当前应该展示哪张图片的重点

好了,写到这里,基本的展示效果已经搞定了,img元素的宽高,在App.vue的css部分可以定义一下如:#app img { width: 100%; height: 220px; },这样限制了宽高看起来就比较合适了

现在,开始尽请的开发吧

Swiper组件

Swiper内部写了多个SwiperItem组件,所以需要一个slot插槽来进行内容的分发,并且接收了v-model绑定数据后传递过来的value

下面,来看一眼实现的逻辑吧

// Swiper.vue文件

<template>
    <div class="swiper">
        <div class="view">
            <slot></slot>
        </div>
    </div>
</template>

<script>
export default {
    props: {
        // v-model默认传递过来的value属性
        value: {
            type: Number,
            default: 1
        }
    }
}
</script>

<style scoped>
.swiper {
    position: relative;
    width: 100%;
    height: 220px;
    overflow: hidden;
}
</style>

先写了一个基本的逻辑,剩下的逻辑稍后再写,我们再把SwiperItem组件实现一下

SwiperItem组件

轮播图和选项卡很类似,都是当前只显示一张图,其余的都隐藏起来,SwiperItem组件就是用来做这些事情的

// SwiperItem.vue文件

<template>
    <transition name="items">
        <div class="swiper-item" v-if="isShow">
            <slot></slot>
        </div>
    </transition>
</template>

<script>
export default {
    props: {
        id: {
            type: Number,
            // 必填属性
            required: true
        }
    },
    data() {
        return {
            selectedId: 1 
        }
    },
    computed: {
        isShow() {
            // 根据props传过来的值判断v-if显示
            return this.id === this.selectedId;
        }
    }
}
</script>

<style scoped>
.items-enter-active,
.items-leave-active {
    transition: .5s linear;
}
.items-leave-to {
    transform: translateX(-100%);
}
.items-enter {
    transform: translateX(100%);
}
.items-enter-active {
    position: absolute;
    width: 100%;
    height: 100%;
    left: 0;
    top: 0;
}
</style>

初始化轮播图

回到Swiper组件里,我们需要写一个方法用来展示轮播图,那么就简单的起名为show方法吧,

它的作用就是:遍历$children子组件实例,修改子组件里的selectedId的值,然后显示初始轮播图

// Swiper.vue文件

<script>
export default {
    ...省略,
    data() {
        return {
            index: 1
        }
    },
    methods: {
        show() {
            // 给index赋值为传递过来的value
            // 如果没有传递value值,就默认取第一个子组件里的id值
            this.index = this.value || this.$children[0].id;
            // 遍历子组件,并修改实例上对应的selectedId值
            this.$children.forEach(vm => {
                vm.selectedId = this.index;
            });
        }
    },
    mounted() {
        // 初始化
        this.show();
    }
}
</script>

实现轮播图小点

轮播图下方常见会有对应的小点,让用户直观的看到一共有几张轮播图片,其实实现起来很easy,因为只要我们知道有几条数据就可以利用v-for对应循环出来了

// Swiper.vue文件

<template>
    <div class="swiper">
        <div class="view">
            <slot></slot>
        </div>
        <div class="dots">
            <span class="dot"
                  v-for="dot in len"
                  :key="dot" 
                  :class="{active: index === dot}">
            </span>
        </div>
    </div>
</template>

<script>
export default {
    ...省略,
    data() {
        return {
            index: 1,
            // 记录轮播图的图片数量
            // 定义在data是为了在模板使用
            len: 0
        }
    },
    methods: {
        ...省略
    },
    mounted() {
        // 初始化
        this.show();
        // 更新len的长度,其实就是有几个子组件而已
        this.len = this.$children.length;
    }
}
</script>

<style scoped>
.swiper {
    position: relative;
    width: 100%;
    height: 220px;
    overflow: hidden;
}
/* 轮播图小点样式 */
.swiper .dots {
    position: absolute;
    bottom: 10px;
    left: 50%;
    transform: translateX(-50%);
}
.swiper .dots .dot {
    width: 10px;
    height: 4px;
    border-radius: 6px;
    background-color: rgba(255, 255, 255, 0.7);
    display: inline-block;
    margin: 0 2px;
}
.swiper .dots .active {
    width: 14px;
    background-color: #fff;
}
</style>

经过上面的一番折腾,总该有点该有的样子了吧,让我们看看效果图

自动播放

很多轮播图效果都是自带自动播放的,所以我们当前也不会置之不理。我们通过给Swiper组件传递autoplay的prop来进行设置

// App.vue文件  ->  html部分

<template>
    <div id="app">
        <Swiper v-model="currentId" v-if="sliders.length" :autoplay="true">
            ...省略
        </Swiper>
    </div>
</template>

父组件App传递了autoplay了,那么接下来就继续回到我们的Swiper组件

我们再实现一个play方法,主要是写一个定时器,然后再通过change方法来修改value的值,做到自动播放

// Swiper.vue文件  ->  js部分

<script>
export default {
    props: {
        ...省略,
        autoplay: {
            type: Boolean,
            default: false
        }
    },
    data() {
        return {
            index: 1,
            len: 0
        }
    },
    methods: {
        show() {...省略},
        // 定时器播放
        play() {
            if (this.autoplay) {
                this.timer = setInterval(() => {
                    this.change(this.index + 1);
                }, 3000);
            }
        },
        // 切换轮播图
        change(index) {
            // 处理右边界如果超过了len
            if (index === this.len + 1) {
                // 将index改回到1
                index = 1;
            } else if (index === 0) {
                index = this.len;
            }
            // 通过$emit,触发input事件,修改value为index值
            this.$emit('input', index);
        },
        // 清除定时器
        stop() {
            clearInterval(this.timer);
            this.timer = null;
        }
    },
    watch: {
        // 这里是关键
        // 监听value值的变化,value赋给了index,随着每次的index变化
        // 就重新调用一下show方法,刷新视图
        value() {
            this.show();
        }
    },
    mounted() {
        this.show();
        this.len = this.$children.length;
        // 调用自动播放
        this.play();
    }
}
</script>

一起来看看效果吧

左右切换

默认自动播放的时候,图片都是向左移动到-100%的位置。如果考虑到用户切换的话,需要先在mounted时记录一下前一个索引值prevIndex

前一个索引值prevIndex和当前的索引值index去比较,如果大于了当前的index,那么就说明是反向轮播,图片需要向右移动

让我们再回到SwiperItem组件,给它添加一个动态class,用来显示正反方向的过渡效果

不同方向过渡

// SwiperItem.vue文件

<template>
    <transition name="items">
        <div class="swiper-item" v-if="isShow" :class="{direction}">
            <slot></slot>
        </div>
    </transition>
</template>

<script>
export default {
    ...省略
    data() {
        return {
            index: 1,
            // 判断方向
            direction: false
        }
    }
}
</script>

<style scoped>
...省略
/* 反向过渡 */
.items-leave-to.direction {
    transform: translateX(100%);
}
.items-enter.direction {
    transform: translateX(-100%);
}
</style>

判断方向

上面的代码为我们修改了不同方向的过渡效果,废话不再多说,直接看代码吧

// Swiper.vue文件  ->  js部分

<script>
export default {
    ...省略,
    methods: {
        show() {
            this.index = this.value || this.$children[0].id;
            
            this.$children.forEach(vm => {
                this.$nextTick(() => {
                    vm.selectedId = this.index;
                });
                // 如果prevIndex大于了index就是反向轮播
                vm.direction = this.prevIndex > this.index;
                
                // 处理自动播放时的边界情况
                if (this.timer) {
                    if (this.prevIndex === 1 && this.index === this.len) {
                        // 处理从第1张反向跳到最后一张的条件
                        vm.direction = true;
                    } else if (this.prevIndex === this.len && this.index === 1) {
                        // 处理最后一张正向跳到第一张的条件
                        vm.direction = false;
                    }
                }
            });
        },
        change(index) {
            this.prevIndex = this.index;
            ...省略
        }
    },
    mounted() {
        this.show();
        this.len = this.$children.length;
        this.play();
        // 先记录当前的值
        this.prevIndex = this.index;
    }
}
</script>

简单梳理

  1. 修改show方法
  • prevIndex最开始是undefined,因为在mounted按照顺序调用show方法的时候,prevIndex还没有赋值为index
  • 相当于prevIndex > index第一次比较的时候,是undefined和1在比较,之后在每次切换图片,调用change方法的时候又给prevIndex赋了index的值
  • $nextTick是为了在vm.direction数据被修改了,等待DOM更新后再修改对应的selectedId以对应展示对应图片
  1. 修改change方法
  • change方法修改的地方很少,this.prevIndex = this.index就是在每次切换的时候把上一次的index赋给了prevIndex
  • 比如: 最开始prevIndex为undefined,index为1;当prevIndex为1的时候,index为2,以此类推的赋值,这样大家就能够理解了吧

至此,我们把左右切换的过渡效果代码逻辑搞定了,还差最后一步了,让我们给轮播图添加touch事件,让用户可以左右切换图片

添加touch事件

上面已经完成了判断方向的逻辑,用户左右切换简直是小菜一碟了,首先要给元素上添加touch事件

// Swiper.vue文件

<template>
    <div class="swiper">
        <div class="view" @touchstart="touchstart" @touchend="touchend">
            <slot></slot>
        </div>
        ...省略
    </div>
</template>

<script>
export default {
    ...省略,
    methods: {
        touchstart(e) {
            // 刚触摸屏幕时记录一个x坐标
            this.startX = e.touches[0].pageX;
            // 并且停止自动播放
            this.stop();
        },
        touchend(e) {
            // 计算开始点和手指抬起时离开点的坐标差值
            let disX = e.changedTouches[0].pageX - this.startX;
            
            // 如果小于0就表示是正向切换(向左滑动)
            // 反之,就是反向切换(向右滑动)
            if (disX < 0) {
                this.change(this.index + 1);
            } else {
                this.change(this.index - 1);
            }
            // 切换完毕后,继续进行自动播放
            this.play();
        },
        ...省略
    }
}
</script>

写到这里就算完成了,让我们再来一起看看效果吧

最后的最后

由于组件里用到了定时器,那么别忘记了在beforeDestroy的时候,调用stop方法清除一下定时器

学无止境

本来还想写两个别的可复用组件,不过怕字数太多,大家看的疲劳了,就点到为止吧,我也会把相应的地址发出来,让大家方便参考

拾人牙慧,学无止境,感谢大家观看了