概要
下拉刷新是很常见的应用需求,也是目前很主流的一种交互手段,之前一直使用的是mint-ui的load-more组件,但是要配置的项太多,比较复杂,今天有空自己写了一个下拉刷新的组件,主要是自己体会一下这种组件的实现机制和编写的难点,折腾了一个小时,终于写出了一版像样一点的,也算是小有收获吧,这边文章记录了写该组件时遇到的一些问题及解决办法。
首先,你需要会的知识点有:
1.H5的touch事件
2.父子组件的数据交互;插槽
3.Promise()的用法
我们先来看一下效果图:
是不是跟正常使用的差不多呢?这里我只是实现了基本功能,对样式、加载文字动画什么的都没做处理,有兴趣的读者可以自己尝试的去封装一个个性化的下拉刷新组件。个人比较懒,所以下面先介绍整体思路和问题分析,再给出代码。
组件分析
首先,写之前我们应该想想,这个组件适用的场景有哪些,当然常用的列表页下拉刷新就不用提了,还可以用在详情页的刷新等场景。所以,我们这个组件就相当于一个外层的容器,它可以下拉(移动),同时下拉结束之后还会触发容器内部的数据变化。所以,这个组件的两个重点,一个是下拉时的移动实现,一个是将下拉动作和数据进行绑定使其有交互。带着这两个问题,我们可以有针对性的往下继续了。
下拉移动
我们知道,一个div或其他元素运动,肯定是其样式中有关位置或者大小的属性有改变了。比如绝对定位时,left的值一直增加,该div就会一直往右移动;相对布局中,marginLeft的值一直增加,该div也会一直右移。所以,我们可以通过操作外层容器的相关属性,来达到下拉刷新的展示效果。
那么问题又来了,这个left或者是这个marginLeft的值要增加多少呢?这就需要你知悉H5中的touch事件了,我不对这个事件做详细介绍,总之它能获取到你鼠标点下(手机上是手指按下),移动,松开,取消的四个动作。有了这个API,我们的问题就迎刃而解了:我们可以在按下时记录下开始位置,移动过程中记录手指移动的距离,把这个值赋给marginTop属性,这样我们的组件就可以随着手指移动而移动对应的距离了,最后在手指松开的时候是不是就可以刷新数据了呢?
上面的分析貌似是可行的,不过我们可以想一下,倘若我们一直往下拉,这个容器就会一直往下移动,虽然我们移动的距离有限,但是我们如果从该容器的顶部滑到底部,那这个容器就会移出我们的可视区,这样的用户体验非常不好,我们下拉只是希望容器顶部显示出下拉刷新的提示字样,然后随着我们的移动的距离进行不同的提示,比如达到一个值以后,可以提示“松开刷新”,松开时可以提示“刷新中...”等。所以我们不用让容器一直随着我们手指的移动而移动,给一个界定的最大值就可以了。
最后一个问题,当我们移动完了,数据也加载了,想要让容器滚回它原来的位置怎么办呢?好办,我们上面已经记录下了移动的距离了,想要回去,不就是把marginTop的值慢慢的减回原来的值不就行了吗?一个定时器就可以搞定了!
数据刷新
展示我们已经实现了,那么最重要的一步:数据刷新 如何实现呢?展示都只是小事,你拉完了没变化不是白拉了吗?所以最重要的一步就是数据刷新了。但是又有难题了,我这个下拉刷新的组件只是提供一个容器的作用,要怎么展示不同业务场景下的界面呢?很简单,用插槽啊!不会的话,百度啊!
OK,接下来我们就要进行父子组件的交互了,这个也很简单,子组件内emit一下绑定的方法就可以了。但是,我们要在数据加载完成后让容器回位啊,我的数据加载方法是在父组件内,控制容器回位的方法是在子组件内的,怎么办呢??
OK,你又知道了,用Promise()啊,子组件内使用一个Promise,调用加载数据方法时,将resolve作为参数传给父组件,父组件在加载完数据之后调用一下resolve就可以了!这样子组件就可以做自己想做的事了。
组件代码
详细注释都在代码中。
<!--
@CreationDate:2018/3/16
@Author:Joker
@Usage:下拉刷新组件
-->
<template>
<div class="pull-to-refresh-app">
<div class="content-box">
<div class="refreshing-box">
<div>{{tipText}}</div>
</div>
<div class="present-box">
<slot></slot>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.pull-to-refresh-app {
.content-box {
height: 300px;
position: relative;
.refreshing-box {
line-height: 40px;
height: 40px;
text-align: center;
}
.present-box {
background-color: lighten(#c4e3f3, 10%);
}
}
}
</style>
<script>
export default {
name: 'PullToRefresh',
data(){
return {
startX: '',
endX: '',
startY: '',
endY: '',
moveDistance: 0,
tipText: '下拉刷新',
el: null
}
},
methods: {
/**
* 绑定touch事件
*/
bindTouchEvent(){
let that = this;
this.el.addEventListener('touchstart', this._touchStart);
this.el.addEventListener('touchmove', this._touchMove);
this.el.addEventListener('touchend', this._touchEnd)
},
/**
* 开始下拉的监听 这里主要是记录下初始坐标 下拉只需记录y即可(这里方便以后测其他的使用,也记录了 x)
* @param e 下拉事件
*/
_touchStart(e){
let touch = e.changedTouches[0];
this.tipText = '下拉刷新';
this.startX = touch.clientX;
this.startY = touch.clientY;
},
/**
* 下拉过程的监听 这里记录下移动的距离
* @param e
*/
_touchMove(e){
let touch = e.changedTouches[0];
//获取下拉的距离
let _move = touch.clientY - this.startY;
//这里主要是让内容区随着下拉操作而往下滚动
//_move>0是指往下滑动(下拉),_move<100是给一个上限,不然一直下拉的话整个内容区就会随着下拉距离一直增大,用户体验不是很好
//这里下拉操作主要是显示出顶上的一层tipText
if (_move > 0 && _move < 100) {
this.el.style.marginTop = _move + 'px';
//记录下下拉的距离
this.moveDistance = touch.clientY - this.startY;
if (_move > 50) {
this.tipText = '松开即可刷新'
}
}
},
/**
* 下拉动作结束(松开手指)监听
* @param e
* @private
*/
_touchEnd(e){
let touch = e.changedTouches[0];
this.endX = touch.clientX;
this.endY = touch.clientY;
let that = this;
if (this.moveDistance > 50) {
this.tipText = '数据加载中...';
//调用父组件的加载数据的方法
//这时候要在父组件的数据加载完成后,才将div还原,所以这里把resolve传进了父组件中,也可以采取其他方法
new Promise((resolve, reject) => {
this.$emit('load', resolve);
}).then(() => {
that._resetBox();
});
} else {
this._resetBox();
}
},
/**
* 重置视图
* 这里的操作主要是将移动的距离还原,用一个定时器慢慢将marginTop的值减回去直到0为止
*/
_resetBox(){
let that = this;
if (this.moveDistance > 0) {
let timer = setInterval(function () {
that.el.style.marginTop = --that.moveDistance + 'px';
if (Number(that.el.style.marginTop.split('px')[0]) <= 0) clearInterval(timer);
}, 1)
}
}
},
mounted(){
this.el = document.querySelector(".content-box");
this.bindTouchEvent();
}
}
</script>
使用组件
<!--
@CreationDate:2018/3/16
@Author:Joker
@Usage:
-->
<template>
<div class="pull-to-refresh-page-app">
<mt-header fixed title="下拉刷新组件测试">
<router-link to="/tool" slot="left">
<mt-button icon="back">返回</mt-button>
</router-link>
</mt-header>
<div class="pull-content">
<pull-to-refresh @load="load">
<div v-for="i in players" class="list-item">
{{ i }}
</div>
</pull-to-refresh>
</div>
</div>
</template>
<style scoped lang="scss">
.pull-to-refresh-page-app {
.pull-content {
.list-item {
height: 40px;
line-height: 40px;
border-bottom: 1px solid #ffffff;
padding-left: 5px;
&:last-child {
border-bottom: none;
}
}
}
}
</style>
<script>
import PullToRefresh from '../../components/PullToRefresh'
export default {
name: 'PullToRefreshPage',
components: {
PullToRefresh
},
data(){
return {
players: ['kobe', 'fisher', 'jordan', 'shark', 'duncun']
}
},
methods: {
load(resolve){
setTimeout(() => {
for (let i = 0; i < 4; i++) {
this.players.unshift('player No.' + Math.floor(Math.random() * 10) + 1);
}
resolve();
}, 1000)
}
}
}
</script>
哦,对了,忘了说了,关于如何将最上面的提示字样一开始先隐藏起来,我使用了一个比较投机取巧的方法,就是先把它藏在Header的后面,哈哈,你也可以尝试其他的方法。优化点
1,上面说的,提示字样放置的位置不能这么投机取巧;
2,加载文字应该让用户自定义,应该作为props或者slots传进来。最好还是slots比较好,可以加一些gif图让界面更好看。
3, 未添加容错处理,所以健壮性有待改进。
github
如果您觉得这篇博客对你有帮助,请给个star,这么晚了写个blog不容易。
Git地址:https://github.com/JerryYuanJ/a-vue-app-template
附:
当前项目的全部功能演示如图所示:
如您在阅读本篇博客的时候发现有问题或者有bug,请及时联系我,不然就很尴尬;也可以在git上提issue。
谢谢!