工作笔记五——自己实现一个Vue的下拉刷新组件
概要
下拉刷新是很常见的应用需求,也是目前很主流的一种交互手段,之前一直使用的是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。
谢谢!
- 上一篇: vue.js进阶之组件
- 下一篇: vue 数据更新 视图不刷新