博客更新地址啦~,欢迎访问:[https://jerryyuanj.github.io/](https://jerryyuanj.github.io/)
 

 

概要

下拉刷新是很常见的应用需求,也是目前很主流的一种交互手段,之前一直使用的是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。

谢谢!

 

Logo

前往低代码交流专区

更多推荐