说起轮播图,就会想起自己刚学js以及css动画的那段日子,也曾经用原生js什么的去实现过,也说得出不同实现方式的优劣。之后呢,做项目都是用框架,用过layui、jq-weui什么的,都是直接用他们封装好的轮播图组件。直到最近学了Vue,看着他2.0有动画过渡什么的,就想着自己尝试着封装一下,之前没有造轮子的经验,写的不好好请多指教。
先上一下效果图:
因为前前后后做了两种实现,第二种是在第一种的基础上优化的(其实第一种没做完),所以分开来讲。
版本一
版本一的思路是按照掘金上的教程来的 这里贴一下教程地址 作者:limingru 侵联立删
需求分析
- 在点击右侧箭头时,图片向左滑动到下一张;点击左侧箭头时,图片向右滑到下一张
- 过渡效果,图片滑动
- 小圆点指示当前图片(当前图片对应的小圆点有特殊样式)
- 无限滚动,即在滚动到最后一张时,再点击下一张时会继续向左滑动到第一张
原理讲解
接下来,我们怎么实现移动呢? 其实很简单,我们只要改变我们的视口(如图红色框框)的样式, 比如说他的初始样式:
transform: translate3d(-888px, 0,0);
我们改变为
transform: translate3d(0px, 0,0);
视口就向左移动一张图片的宽度了(这里图片宽度为888px) 然后,我们只要给左右按钮一个点击事件,点击的时候传递参数(移动距离,移动方向) 就可以移动视口
代码架构
- html架构
<div class="slide">
<!-- 视窗 -->
<div class="window">
<!-- 图片区域 -->
<ul class="container"
:style="containerStyle">
<li><img :src="sliders[sliders.length-1].img" alt=""></li>
<li v-for="(item, index) in sliders"><img :src="item.img" alt=""></li>
<li><img :src="sliders[0].img" alt=""></li>
</ul>
<!-- 左右箭头 -->
<div class="direction">
<div @click="move(888,1)" class="left-btn icon-box">
<i class="icon"></i>
</div>
<div @click="move(888,-1)" class="right-btn icon-box">
<i class="icon"></i>
</div>
</div>
<!-- 下面的小点 -->
<div class="dots-box">
<div v-for="(item,index) in sliders"
class="dot" :class="{'dot-active': cur == index'}"></div>
</div>
</div>
</div>
<script src="../vue.js"></script>
<script src="slide.js"></script>
<script>
let sliders = new Vue({
el: '.slide',
data: {
sliders: [
{
img: 'imgs/0.jpg'
},
{
img: 'imgs/1.jpg'
},
{
img: 'imgs/2.jpg'
},
{
img: 'imgs/3.jpg'
},
{
img: 'imgs/4.jpg'
}
],
cur: 0,
distance: -888,
},
computed: {
containerStyle() { //这里用了计算属性,用transform来移动整个图片列表
console.log('------change containerStyle-------');
return {
transform:`translate3d(${this.distance}px, 0, 0)`
}
}
},
methods: {
move(offset, direction) {
this.distance += offset * direction;
if( direction == 1) {
this.cur--;
} else {
this.cur++;
}
if(this.cur == 5 ) this.cur = 0;
if(this.cur == -1) this.cur = 4;
console.log('------set distance-------');
if (this.distance < -4440) {
console.log('------set distance 4440to888-------');
this.distance = -888;
}
if (this.distance > -888) {
console.log('------set distance 888to4440-------');
this.distance = -4440;
}
}
}
})
</script>
简单讲解下关键的布局要求:
- window会有一个overflow:hidden的样式,所以页面上其实是存在一排的图片,而我们只看得到视口位置的图片
- 上面原理图所标志的红色框就是container,为了可以在初始时显示图一而不是图五,我们得给他一个transform: translate3d(${-图片宽度}px, 0, 0)的样式
- 为了实现图片并排而不是挤下来,我们给container display:flex
- 为了实现图片移动的时候有动画过渡,我们再给container transition: all .5s;
有关Vue的代码解释
- v-for指令,遍历在js中注册的data中的sliders数组
<li v-for="(item, index) in sliders"><img :src="item.img" alt=""></li>
- @click绑定事件,给元素绑定move()这个事件,事件在Vue实例中声明
<div class="direction">
<div @click="move(888,1)" class="left-btn icon-box">
<i class="icon"></i>
</div>
<div @click="move(888,-1)" class="right-btn icon-box">
<i class="icon"></i>
</div>
</div>
- :class绑定类名 踩坑实例:
<!-- 下面的小点 -->
<div class="dots-box">
<div v-for="(item,index) in sliders"
class="dot" :class="{dot-active: cur == index'}"></div>
</div>
我刚开始这样写,直接报错 原因很简单,绑定类名的时候,如果类名带有-等,则需要这样写
<div class="dot" :class="{'dot-active': cur == index'}"></div>
在这里上个效果图:
<li><img :src="sliders[0].img" alt=""></li>),然后等动画完成后,再瞬间切换到左边的第一张,我稍微想了想,决定放弃这种实现方式,改用我自己的思路。
所以做到这里,我决定重新构建
版本二
vue列表过渡
踩坑实例
模仿着官网demo
<head>
<meta charset="UTF-8" />
<title>测试</title>
<style>
.list-item {
display: inline-block;
margin-right: 10px;
}
.list-enter-active, .list-leave-active {
transition: all 1s;
}
.list-enter, .list-leave-to
/* .list-leave-active for below version 2.1.8 */ {
opacity: 0;
transform: translateY(30px);
}
.list-move {
transition: transform 1s;
}
.fade-enter-active, .fade-leave-active {
transition: all 1s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
transform: translateY(50px);
}
.fade-move {
transition: all 1s;
}
.li-item {
display: inline-block;
}
.li-img {
width: 100px;
margin-right: 10px;
}
</style>
</head>
<body>
<div id="list-demo" class="demo">
<button v-on:click="add">Add</button>
<button v-on:click="remove">Remove</button>
<transition-group name="list" tag="p">
<span v-for="item in items" v-bind:key="item" class="list-item">
{{ item }}
</span>
</transition-group>
<transition-group name="fade" tag="ul">
<li v-for="(item, index) in sliders" class="li-item">
<img class="li-img" :src="item.img" alt="">
</li>
</transition-group>
</div>
<script src="../vue.js"></script>
<script>
new Vue({
el: '#list-demo',
data: {
items: [1,2,3,4,5,6,7,8,9],
nextNum: 10,
sliders: [
{
img: 'imgs/0.jpg'
},
{
img: 'imgs/1.jpg'
},
{
img: 'imgs/2.jpg'
},
{
img: 'imgs/3.jpg'
},
{
img: 'imgs/4.jpg'
}
],
},
methods: {
randomIndex: function () {
return Math.floor(Math.random() * this.sliders.length)
},
add: function () {
this.items.splice(this.randomIndex(), 0, this.nextNum++);
this.sliders.splice(this.randomIndex(), 0, {img: 'imgs/4.jpg'})
},
remove: function () {
this.items.splice(this.randomIndex(), 1);
this.sliders.splice(2,1);
},
}
})
</script>
</body>
但是发现结果并不是我想的那样
官方:当 Vue.js 用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。
其实从原理分析,为什么要复用 我是这么理解的:我们在一个数组[0,1,2,3]第二个元素的位置插入一个4,变成[0,4,1,2,3] 一般按照常理我们做法是,直接在第二个元素插入4,然后后面的向后移动一个位置 但是,学过数组插入的我们都知道,数组插入的耗费很大 所以会有另一个办法:把第二个元素的值改成4,之后的依次拿前面的值,再在最后添加原来最后的值(比如说这里的3) 以上都是本人一本正经瞎解释的,如果有人知道底层实现的话麻烦指导我一下。
知道了问题所在,不就是设置一个key吗,简单
<transition-group name="fade" tag="ul">
<li v-for="(item, index) in sliders" key="index" class="li-item">
<img class="li-img" :src="item.img" alt="">
</li>
</transition-group>
然和我就写出了这样的代码,可以运行一下发现,依然无效 这又是怎么回事? 稍一分析,不难发现,这里的key值我用了index,这就是问题所在 设想一下,我们在插入/删除的瞬间,这个index其实也是会跟着变化的,这就达不到key预期所要标志元素的效果 改进一下
//html
<transition-group name="fade" tag="ul">
<li v-for="(item, index) in sliders" :key="item.tag" class="li-item">
<img class="li-img" :src="item.img" alt="">
</li>
</transition-group>
//js
new Vue({
el: '#list-demo',
data: {
items: [1,2,3,4,5,6,7,8,9],
nextNum: 10,
sliders: [
{
tag: 0,
img: 'imgs/0.jpg'
},
{
tag: 1,
img: 'imgs/1.jpg'
},
{
tag: 2,
img: 'imgs/2.jpg'
},
{
tag: 3,
img: 'imgs/3.jpg'
},
{
tag: 4,
img: 'imgs/4.jpg'
}
],
tag: 5
},
methods: {
randomIndex: function () {
return Math.floor(Math.random() * this.sliders.length)
},
add: function () {
this.items.splice(this.randomIndex(), 0, this.nextNum++);
this.sliders.splice(this.randomIndex(), 0, {img: 'imgs/4.jpg' ,tag: this.tag++})
},
remove: function () {
this.items.splice(this.randomIndex(), 1);
this.sliders.splice(2,1);
},
}
可以发现我给slider每个元素一个独一无二的tag
阿勒,你怎么瞬移呀?内心好崩溃
在网上查了一下解决方式后,发现需要给fade-leave-active这个类加一个绝对定位
//css
.fade-leave-active {
position: absolute;
}
个人理解:这个类的作用是元素在被删除,进入离开直至动画结束都保有的一个类 在原来没加的时候,动画完成之前,元素一直占据他的布局位置,直到结束后才瞬间失去他的位置,造成其他元素瞬移(这时其他元素没有-move变化延时) 绝对定位之后,刚进入动画的时候就失去位置,其他元素被出发-move的变化延时
原理分析
我们顺着vue列表过渡这个思路来 再对图片数量进行优化,要实现轮播效果,其实我们页面只需要三个图片元素
页面常规状态
图片向右滑动
图片向左滑动
//没有overflow:hidden
<body>
<div class="slide">
<!-- 视窗 -->
<div class="window" id="sliders">
<!-- 图片区域 -->
<transition-group name="fade" class="container" tag="ul" >
<li v-for="(item,index) in sliders" :key="item.tag">
<img :src="imgs[item.imgIndex]" alt="">
</li>
</transition-group>
<!-- 左右箭头 -->
<div class="direction">
<div @click="toRight()" class="left-btn icon-box">
<i class="icon"></i>
</div>
<div @click="toLeft()" class="right-btn icon-box">
<i class="icon"></i>
</div>
</div>
<!-- 下面的小点 -->
<div class="dots-box">
<div v-for="(item,index) in imgs" :key="index" class="dot"
:class="{'dot-active': index === cur}"></div>
</div>
</div>
</div>
<script src="../vue.js"></script>
<script src="slide.js"></script>
<script>
let sliders = new Vue({
el: '.slide',
data: {
imgs: ['imgs/0.jpg','imgs/1.jpg','imgs/2.jpg','imgs/3.jpg','imgs/4.jpg'],
sliders: [
{
imgIndex: 0,
tag: 0
},
{
imgIndex: 1,
tag: 1
},
{
imgIndex: 2,
tag: 2
}
],
tag: 3,
cur: 1,
lock: true
},
computed: {
},
methods: {
toRight: function() {
this.sliders.pop();
let insert = this.sliders[0].imgIndex-1;
if(insert == -1) insert = 4;
this.sliders.unshift({ imgIndex: insert, tag: insert});
if( --this.cur == -1) this.cur = 4;
},
toLeft: function() {
let insert = this.sliders[this.sliders.length-1].imgIndex+1;
if(insert == 5) insert = 0;
this.sliders.push({ imgIndex:insert, tag: insert});
this.sliders.shift();
if( ++this.cur == 5) this.cur = 0;
}
}
})
</script>
</body>
//加锁
//1s后开锁
if( !this.lock ) return;
this.lock = false;
setTimeout(()=>{
this.lock = true;
},1000);
至此,轮播图开发完毕
等等,怎么还没完
标题写的很清楚,这次是开发一个轮播图组件 说到组件,vue给我们实现的机制非常便利,我们在封装东西的时候也十分简单
//slide.js
var sliders_component = {
template:
`
<div class="window" >
<transition-group name="fade" class="container" tag="ul" >
<li v-for="(item,index) in sliders" :key="item.tag">
<img :src="sonimgs[item.imgIndex]" alt="">
</li>
</transition-group>
<div class="direction">
<div @click="toRight()" class="left-btn icon-box">
<i class="icon"></i>
</div>
<div @click="toLeft()" class="right-btn icon-box">
<i class="icon"></i>
</div>
</div>
<div class="dots-box">
<div v-for="(item,index) in sonimgs" :key="index" class="dot"
:class="{'dot-active': index === cur}"></div>
</div>
</div>
`,
props: [
'sonimgs', 'length'
],
data: function(){
return {
sliders: [
{
imgIndex: 0,
tag: 0
},
{
imgIndex: 1,
tag: 1
},
{
imgIndex: 2,
tag: 2
}
],
tag: 3,
cur: 1,
lock: true
};
},
methods: {
toRight: function() {
//加锁
//1s后开锁
if( !this.lock ) return;
this.lock = false;
setTimeout(()=>{
this.lock = true;
},1000);
this.sliders.pop();
let insert = this.sliders[0].imgIndex-1;
if(insert == -1) insert = this.length-1;
this.sliders.unshift({ imgIndex: insert, tag: insert});
if( --this.cur == -1) this.cur = this.length-1;
},
toLeft: function() {
//加锁
//1s后开锁
if( !this.lock ) return;
this.lock = false;
setTimeout(()=>{
this.lock = true;
},1000);
let insert = this.sliders[this.sliders.length-1].imgIndex+1;
if(insert == this.length) insert = 0;
this.sliders.push({ imgIndex:insert, tag: insert});
this.sliders.shift();
if( ++this.cur == this.length) this.cur = 0;
}
}
}
这个js文件中定义了一个sliders组件 我们在使用的时候,只需要引入对应的css,vue原件,还有这个slide.js文件
<div id="app">
<sliders :sonimgs=ParentImgs :length=ParentImgs.length ></sliders>
</div>
<script src="../vue.js"></script>
<script src="slide.js"></script>
<script>
new Vue({
el: '#app',
components: {
sliders: sliders_component
},
data: {
ParentImgs: ['imgs/0.jpg','imgs/1.jpg','imgs/2.jpg','imgs/3.jpg','imgs/4.jpg'],
}
})
</script>
在使用的时候通过父子通信传递图片链接就行了。