zoukankan      html  css  js  c++  java
  • 工作笔记五——自己实现一个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。

    谢谢!


  • 相关阅读:
    Docker 给 故障停掉的 container 增加 restart 参数
    使用docker化的nginx 反向代理 docker化的GSCloud 的方法
    apache benchmark 的简单安装与测试
    mysql5.7 的 user表的密码字段从 password 变成了 authentication_string
    Windows 机器上面同时安装mysql5.6 和 mysql5.7 的方法
    python4delphi 安装
    见证下神奇的时刻
    windows下面安装Python和pip终极教程
    python如何安装pip和easy_installer工具
    Tushare的安装
  • 原文地址:https://www.cnblogs.com/jerryyj/p/9621554.html
Copyright © 2011-2022 走看看