vue2-demo仿宅急送移动端订餐页面项目总结
项目介绍
此项目是本人对vue.js框架学习的进阶性练习,目的是熟悉vue2框架开发spa单页面应用
由vue-cli脚手架搭建,涵盖vue-router以及vuex操作
所仿项目原页面:https://m.4008823823.com.cn/
项目线上展示页面: http://zyuyu.cn/
项目代码:https://github.com/myyqmy2/vue2-kfc
完成内容:主页、商品详情页以及购物车的样式和交互
开发内容为纯前端,采用easy-mock模拟数据接口,实现前后端分离
购物车由vuex数据共享模拟实现
相关问题
以下是开发过程中遇到的一些问题以及解决方法,希望跟大家分享
1、vue项目粘性定位的原生实现
粘性定位,也就是页面滚动到一定位置产生吸顶效果
最简单的实现当然是position:sticky;但由于其兼容性问题,还是选择自己实现这个效果。
参考文章:https://www.jianshu.com/p/07330174e51d
原理什么的上面这篇文章讲的比较详细了,下面我把我的实现过程贴出来分析一下
//需要粘性定位的banner元素,核心就是根据isFix变量动态绑定的fix类
<div class="banner" :class="{'fix': isFix}">
<ul>
<li v-for="(item,index) in banner" :key="index"><img :src="item.imageUrl" alt=""><span></span></li>
</ul>
</div>
下面看一下isFix的变量逻辑
handleScroll() {
//scrollTop为页面顶端滚动距离,headerHeight为产生吸顶效果的目标距离
//这里省略了headerHeight的设置过程,根据需要实现
if(this.scrollTop >= this.headerHeight) {
this.isFix = true
}else {
this.isFix = false
}
}
很容易理解吧,现在唯一的问题就是这个scrollTop的获取,字面上我们也可以看出来这个变量肯定是跟页面的scroll事件有关,所以我们从监听页面的scroll事件下手
注意:我的这个项目中主页也是采用的better-scroll滚动实例,如果你使用的是原生滚动,请参考我刚才说的那篇文章里的实现方法(document.documentElement.scrollTop)
//lodash库的throttle节流函数,用来防止事件操作过于频繁影响性能,具体使用方法请查看lodash文档
this.throttleScroll = throttle(this.handleScroll, 100)
//在滚动实例上监听scroll事件,并将当前滚动坐标(y轴)存入scrollTop,然后执行频率为0.1s的回调函数
this.appWrapper.on('scroll', (pos) => {
this.scrollTop = -pos.y
this.throttleScroll()
})
这样的话每次滚动列表的时候,就会把当前的滚动距离存入scrollTop变量中,然后根据scrollTop和headerHeight的大小来判断是否到达指定位置,从而应用fix类样式。
顺便说一下动态更改元素透明度样式的问题,也是利用刚才存入的scrollTop坐标
//先将要绑定元素的opacity样式与setOpacity变量关联
<header class="header-title" :style="{'opacity': setOpacity}"><span class="title-text">菜单</span></header>
然后将setOpacity的赋值表达式加入到刚才scroll事件的回调函数中
this.setOpacity = this.scrollTop / this.headerHeight
搞定。
2、加入购物车时的小球抛物线动效
这个效果很常用,原理也很简单,不过有的文章中解释的可能不是太详细
在这里我把原理做成图示跟大家分享
首先看一下点击时小球轨迹
如图,这个效果的关键是我们要明白在点击前小球的位置,和在点击时小球的位置发生了怎样的变化
下面这张是我们点击前小球的位置
没错,就是上图中小球终点的位置,是不是有些疑惑? 没关系先往下看
其实小球一直都存在于购物车的固定位置,只不过由于默认的display:none;被隐藏了只有在点击的时候被激活我们才能看到
接下来我们就来看一下点击添加按钮时,小球发生了怎样的变化
很明显了吧,小球在我们点击的时候,其位置变到了动效轨迹的(起点)位置,当然这是通过js实现的,然后我们再设置一下动效结束时小球的位置,也就是上图中小球的原始位置。这中间的过程我们就可以触发css中的transition过渡效果,从而产生运动轨迹。还需要知道的一点是,由于过渡效果只针对单向的运动,所以要产生抛物线的话我们就需要做个运动合成,即小球分内外两层,外层负责y向移动,内层负责x向移动,具体合成效果大家可以去参考一下贝塞尔曲线。
好了以上就是小球动效的原理,下面我们来看一下本项目中的实现代码:
首先我们在购物车组件中引入运动小球组件
//shopCart.vue
<shop-cart-balls ref="ball"></shop-cart-balls>
//shopCartBalls.vue
<template>
<div class="balls-container">
<transition-group tag="div" @before-enter="beforeEnter" @enter="enter" @after-enter="afterEnter">
<div class="ball" v-for="(item,index) in balls" :key="index" v-show="item.show">
<div class="inner inner-hook"></div>
</div>
</transition-group>
</div>
</template>
这个是小球组件的模板,小球分内外两层div,vue的transition组件控制过渡,注意里面的过渡钩子,负责改变小球状态。
下面是小球组件js部分
//shopCartBalls.vue
<script>
export default {
data() {
return {
//注意这里设置了三个小球是因为防止在第一个小球还在运动过程中我们再次点击添加按钮时无球可用,show控制每个小球是否可见
balls: [
{show:false},
{show:false},
{show:false}
],
dropBalls: [] //小球处于运动状态时推入该数组
}
},
methods: {
//将一个当前可用小球推入dropBalls数组,并将el属性绑定为点击元素
drop(target) {
for(let i=0;i<this.balls.length;i++) {
var ball=this.balls[i]
if(!ball.show) {
ball.show=true
ball.el=target
this.dropBalls.push(ball)
return
}
}
},
beforeEnter(el) { //小球进入前钩子函数
let count = this.balls.length
while(count--) {
let ball = this.balls[count]
if(ball.show) {
let rect = ball.el.getBoundingClientRect()
let x = rect.left - 55 //获取小球x方向移动距离
let y = -(window.innerHeight-rect.top-45) //获取y方向移动距离
el.style.display = ''
el.style.webkitTransform = `translate3d(0, ${y}px, 0)`
el.style.transform = `translate3d(0, ${y}px, 0)` //外层移动
let inner = el.getElementsByClassName('inner-hook')[0]
inner.style.webkitTransform = `translate3d(${x}px, 0, 0)`
inner.style.transform = `translate3d(${x}px, 0, 0)` //内层移动
}
} //以上操作使小球移动到轨迹起始位置
},
enter(el,done) { //注意done回调必须有,用来通知下一个钩子
let rf = el.offsetHeight
this.$nextTick(()=>{ //动画异步执行,提高性能
el.style.webkitTransform = 'translate3d(0, 0, 0)'
el.style.transform = 'translate3d(0, 0, 0)'
let inner = el.getElementsByClassName('inner-hook')[0]
inner.style.webkitTransform = 'translate3d(0, 0, 0)'
inner.style.transform = 'translate3d(0, 0, 0)'
el.addEventListener('transitionend', done) //通知下一个钩子执行
}) //enter函数操作小球运动结束位置
},
afterEnter(el) {
//过渡结束时,隐藏小球,从dropBalls数组拿出
let ball = this.dropBalls.shift()
if(ball) {
ball.show = false
el.style.display = 'none'
}
}
}
}
</script>
然后就是在商品列表组件中触发小球组件中的drop方法
//goodsContainer.vue
addCart(item,event) {
this.addToCart(item)
this.$emit('add-to-cart',event.target)
}
在app组件中监听子组件传出来的addToCart事件,然后调用购物车组件的drop()方法
//shopCart.vue
drop(target) {
this.$refs.ball.drop(target)
}
搞定。(关键是要充分理解过渡钩子)
3、better-scroll插件实现双滚动列表的联动
better-scroll插件是做滚动列表用的比较多的一个插件,功能也比较完善,具体使用可参考官方文档。
主要是实现本项目中产品列表和产品导航间的联动效果,即滚动产品列表时导航列表也会滚动到对应位置,点击导航时,产品列表滚至对应位置。
参考文章:https://blog.csdn.net/guxiansheng1991/article/details/79083845
上面这篇文章把原理和具体操作都说的清楚了,我就不再解释了,下面分析一下本项目中的代码以及一些细节的问题。
有一点需要注意:Bscroll实例的滚动容器的高度要确保小于滚动列表,并且他默认容器的第一个子元素为滚动列表。
//引入better-scroll,并在DOM加载完成后初始化Bscroll实例
import Bscroll from 'better-scroll'
mounted() {
this.$nextTick(() => {
this.navScroll = new Bscroll(this.$refs.nav, {
//我这里改了一些效果时间,影响不大
//由于我外层还嵌套了一个Bscroll所以没有打开这里的click事件,不然会重复点击
bounceTime: 500,
swipeTime: 500,
swipeBounceTime: 300
})
this.goodsScroll = new Bscroll(this.$refs.groups, {
probeType : 3
})
//商品滚动列表监听滚动事件,并将实时滚动位置传入scrollY变量
this.goodsScroll.on('scroll',(pos) => {
this.scrollY = Math.abs(Math.round(pos.y))
})
})
}
联动最关键的一个部分就是各楼层高度数组的设置
_calculateHeight() {
let list = this.$refs.listGroup.children
let height = 0
this.listHeight.push(height)
//计算每一层商品的累加高度,推入listHeight数组,用来判断当前层数
for(let i=0;i<list.length;i++) {
let item = list[i]
height += item.clientHeight
this.listHeight.push(height)
}
}
这个_calculateHeight设置函数什么时候执行呢?
我是把它放在了watch中,每当监听到goodsList改变就执行一次
watch: {
goodsList: function() {
this.$nextTick(()=>{
this._calculateHeight()
})
}
}
然后就是计算当前层数的计算属性
computed:{
currentIndex() {
//将当前滚动位置与层高数组各项对比,判断出层数
for(let i=0;i< this.listHeight.length;i++) {
let height1 = this.listHeight[i];
let height2 = this.listHeight[i+1];
if(!height2 || (this.scrollY >= height1 && this.scrollY < height2)){
return i;
}
}
return 0;
}
}
wacth中添加对currentIndex的监听
//每当层数改变时,将导航滚至对应位置
currentIndex: function(val) {
let list=this.$refs.navList.children
this.navScroll.scrollToElement(list[val])
}
完成了商品列表滚动时对商品导航的定位
接下来添加导航列表的点击事件
//这个切换效果没有满足交互需求,中间能看到nav列表的滚动过程,而不是像他原网页那样瞬间切换的,我还没有想到解决方法,如果有好想法可以告诉我
selectNav(index,event) {
if(!event._constructed) {
return ;
}
let list = this.$refs.listGroup.children
let el = list[index];
this.goodsScroll.scrollToElement(el,100);
}
基本搞定,还有一些就是动态设置滚动容器高度之类的代码,可以去看项目的具体代码。
4、一些细节交互的实现
整个项目下来,还是遇到了一些坑的,不过基本都得到了解决,主要还是要多思考,
(1)比如用电脑开发的时候在chrome浏览器开发者模式下的手机模式调试时未发现问题,一上手机浏览器真机测试就发现ios浏览器原生滚动的屏幕回弹问题,导致我放弃了原生滚动,主页也采用了better-scroll插件,但是bscroll滚动列表会对内部的fixed定位产生影响,需要注意。
(2)还有特别要注意的是在子组件内获取父组件传递数据时,一定要注意操作时父组件异步获取的值是否已经传进来了,必要时可用watch监听一下然后再操作。
(3)在做详情页和商品列表的切换时,为了保证切换时的连贯性我选择将商品列表组件放入<keep-alive>
中进行缓存,但切换时发现一旦详情页内操作vuex数据造成列表内变量改变,未能及时刷新bscroll滚动实例,再切回商品列表是会出现无法滚动现象。由于<keep-alive>
中组件切换时,生命周期只有activated和deactivated两个过程所以我手动设置了商品列表离开时存储位置并且进入时跳转到上次离开时的位置,代码如下:
activated() {
this.$nextTick(()=>{
this.goodsScroll.refresh()
this.goodsScroll.scrollTo(0,this.savePosition)
})
},
deactivated() {
this.savePosition = this.goodsScroll.y
},
大概就是以上内容,下一篇文章打算写一下vue核心原理相关内容。
感谢观看。