解决Vue + Vant下拉加载功能-快速滑动闪动问题,实现无缝历史消息加载体验

1,995 阅读5分钟

前言

更新日期:2023-8-4

找到了如何处理闪动的问题

在开发中,遇到了一些页面闪动的问题,通过本文,我将分享如何解决这个问题,以及如何实现下拉加载历史消息的功能。

问题解决:处理页面闪动

有时候,在数据的头部添加新数据,页面会出现闪动。这是由于Vue的渲染机制造成的,新数据在前,旧数据被“挤下去”,当我们使用scrollTop从顶部迅速滑动到指定位置时,新数据和旧数据之间的不一致会导致页面闪动。

为了解决这个问题,我采取了以下步骤:

  1. 首先,我添加了一个遮罩层来阻止用户看到滑动的场面。
  2. 设计遮罩层的颜色和占比,使其完全覆盖数据区域。
  3. 开始滑动到原先用户所见的位置,监听数据向下滑动结束的回调。
  4. 取消遮罩层,显示数据。 这样,用户就不会看到页面闪动的情况了

这样,用户就不会看到页面闪动的情况了。

介绍

简介:在这篇优化文章中,我们探讨了如何使用 Vant 组件库实现类似微信的下拉加载更多历史消息功能。通过对下拉刷新组件的改造,我们让用户只需在第一次下拉时手动开启加载更多,随后页面滑动到顶部时将自动触发加载,直到数据全部加载完毕。同时,我还解决了页面闪动的问题,提升了用户体验。通过详细的步骤和代码示例,读者可以轻松理解并应用这一功能,从而为移动应用的用户体验注入新的活力。

在现代移动应用中,像微信那样实现加载更多历史消息的功能已经成为了提升用户体验的一项重要需求。为了达到这个目标,我们借助了 Vant 组件库,并进行了一些改造,将下拉刷新转化为下拉加载更多的形式。这意味着用户只需要在第一次下拉时手动开启加载更多,之后只需将页面滑动到顶部,加载更多就会自动触发,直到所有数据都加载完毕。当没有更多数据可加载时,会显示“无更多数据”的提示。

这个优化的方案旨在让用户在使用过程中更加便捷地加载历史消息,从而提高用户体验。你可以在 Vant 组件的官方地址 vant-contrib.gitee.io/vant/v2/#/z… 获取更多关于该组件的信息。

实现效果

首先,让我们看一下实现的效果。我创建了一个示例的 new promise,在延迟了 1 秒之后返回数据。下图展示了在页面中如何触发下拉加载更多的动作:

动画11.gif

开始

下面将逐步介绍如何在你的项目中实现这一下拉加载历史消息的功能。

引入 Vant 组件

首先,我们需要引入 Vant 组件库中的下拉刷新组件,这个组件将为我们提供实现下拉加载功能的基础。

import { PullRefresh } from "vant";

export default {
 components: {
    "van-pull-refresh": PullRefresh
  },
}

在模板中使用组件

在模板中,我们将使用 Vant 的下拉刷新组件来实现下拉加载历史消息的功能。根据不同的状态,我们设置了相应的文本和属性。

<van-pull-refresh 
      v-model="isRefreshLoading"
      pulling-text="下拉加载数据"
      loosing-text="释放即可刷新"
      :success-text="isRefreshDisabled ? '' : ''"
      :success-duration="800"
      :disabled="isRefreshDisabled"
      @refresh="onRefresh">
      <div v-if="isRefreshDisabled" class="refresh-top">已经到顶啦</div>
      <div ref="courseListHeight">
        <div v-for="(item, index) in videoList" :key="index" class="content" :style="{backgroundColor:item.backgroundColor}">
          {{ item.content }}
        </div>
      </div>
    </van-pull-refresh>

在这里,我们使用了 Vant 提供的下拉刷新组件,并将其与我们的数据列表 videoList 进行关联。通过设置不同的属性,我们能够实现下拉加载的各个阶段效果。

使用参数和事件

我就怕大家懒得打开,给大家展示一些参数和事件来控制下拉加载的行为

  • 参数:

    • v-model:用于指示是否处于加载中的状态。
    • pulling-text:下拉过程的提示文案。
    • loosing-text:释放过程的提示文案。
    • success-text:刷新成功时的提示文案。
    • success-duration:刷新成功提示展示的时长。
    • disabled:是否禁用下拉刷新。
  • 事件:

    • refresh:下拉刷新时触发的事件。

数据处理与分页逻辑

在实现下拉加载功能的过程中,我们需要考虑数据的处理和分页逻辑。具体来说,以下是我们的处理方案:

处理方案:

  1. 通过 getBoundingClientRect 监听滑块到达特定区域,满足判断条件后开始请求下一页数据。
  2. 阻止在规定区域内多次调用 API,避免多次请求。
  3. 利用渲染后的高度,使用 scrollToscrollBy 滚动到下拉加载的数据处。
  4. 当请求数据完成后,关闭加载状态并禁用下拉刷新。

数据和参数

dataList: [
        111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 
        124,125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135
      ].reverse(),
params: {
    page_size: 5,
    page_num: 1
},
isRefreshLoading: false, // 是否处于加载中状态
isRefreshDisabled: false, // 是否禁用下拉刷新
videoList: [], // 数据内容
courseListPageHeight: 0, // 页面的高度
domVantRefreh: "", // 获取van-loading到顶部的高度
lockOneApiState: false // 防止反复的请求api

初始化数据请求

首次进入页面时,我们需要请求一次接口来获取初始数据:

 beforeMount() {
    this.getapi();
 },

下拉加载的处理

methods 中,我们处理下拉加载的逻辑。通过监听滚动事件并计算相关参数,实现了下拉加载更多数据的功能。

onRefresh() {
    // 获取 van-loading 到顶部的高度
    this.domVantRefreh = document.querySelector(".van-loading");
    // 加载监听 scroll 滚动事件
    window.addEventListener("scroll", this.loadingPage);
    this.params.page_num++;
    this.getapi();
},
getapi() {
  // 处理 API 请求和数据处理逻辑
},
loadingPage() {
  // 监听滚动事件,根据条件触发加载数据
}

贴上一次的代码

关于闪动在PC浏览器上调试可能看不出什么,但是在手机上就很清楚了。

该处代码的bug,就是上面说的闪动的问题,我们下面会采用新的写法了,这个就当作历史遗作,供大家对比:

<template>
  <div class="refresh">
    <van-pull-refresh 
      v-model="isRefreshLoading"
      pulling-text="下拉加载数据"
      loosing-text="释放即可刷新"
      :success-text="isRefreshDisabled ? '' : ''"
      :success-duration="800"
      :disabled="isRefreshDisabled"
      @refresh="onRefresh">
      <div v-if="isRefreshDisabled" class="refresh-top">已经到顶啦</div>
      <div ref="courseListHeight">
        <div v-for="(item, index) in videoList" :key="index" class="content" :style="{backgroundColor:item.backgroundColor}">
          {{ item.content }}
        </div>
      </div>
    </van-pull-refresh>
  </div>
</template>

<script>
import { PullRefresh } from "vant";
export default {
  components: {
    "van-pull-refresh": PullRefresh
  },
  data() {
    return {
      dataList: [
        111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124,
        125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135
      ].reverse(),
      params: {
        page_size: 5,
        page_num: 1
      },
      isRefreshLoading: false, // 是否处于加载中状态
      isRefreshDisabled: false, // 是否禁用下拉刷新
      videoList: [], // 数据内容
      courseListPageHeight: 0, // 页面的高度
      domVantRefreh: "", // 获取van-loading到顶部的高度
      lockOneApiState: false // 防止反复的请求api
    };
  },
  beforeMount() {
    this.getapi();
  },
  methods: {
    onRefresh() {
      // 获取van-loading到顶部的高度
      this.domVantRefreh = document.querySelector(".van-loading");
      window.addEventListener("scroll", this.loadingPage);
      this.params.page_num++;
      this.getapi();
    },
    getapi() {
      this.getDataList(this.params).then((res) => {
        console.log(res);
        if (res.code == 200) {
          // 第一次添加数据
          if (this.params.page_num == 1) {
            this.videoList = res.result.video_list;
            this.$nextTick(() => {
              window.scrollBy(0, 9999);
              this.courseListPageHeight =
                this.$refs.courseListHeight.clientHeight;
            });
          } else {
            this.videoList.unshift(...res.result.video_list);
          }
          // 对后面的数据进行处理
          if (
            res.result.video_list.length !== 0 &&
            this.params.page_num !== 1
          ) {
            this.isRefreshDisabled = false;
            this.$nextTick(() => {
              let heightElement =
                this.$refs.courseListHeight.clientHeight -
                this.courseListPageHeight;
              this.courseListPageHeight =
                this.$refs.courseListHeight.clientHeight;
              window.scrollBy(0, heightElement);
            });
          }
          // 请求无数据的时候
          if (res.result.video_list.length == 0) {
            this.isRefreshDisabled = true;
            this.isRefreshLoading = false;
            window.removeEventListener("scroll", this.loadingPage);
          }
        }
        this.lockOneApiState = true;
      });
    },
    loadingPage() {
      // 距离顶部多少px的时候进行加载数据,最好再快也要用户看到加载状态的时候,进行了加载
      const refreshTheTop = 0;
      if (
        this.domVantRefreh.getBoundingClientRect().top >= refreshTheTop &&
        this.lockOneApiState == true &&
        this.isRefreshDisabled == false
      ) {
        this.params.page_num++;
        this.getapi();
        this.lockOneApiState = false;
      }
    },
    // 优化数据样式
    updataList(data) {
      const getRandomColor = (index) => {
        const letters = "0123456789ABCDEF";
        let color = "#";
        for (let i = 0; i < 6; i++) {
          color += letters[Math.floor(Math.random(index) * 16)];
        }
        return color;
      };

      return data.reverse().map((res) => {
        return {
          content: res,
          backgroundColor: getRandomColor(res)
        };
      });
    },
    // 伪装数据请求
    getDataList(params) {
      return new Promise((resolve) => {
        const pageNum = params.page_num;
        const pageSize = params.page_size;
        let dataList = this.dataList.slice((pageSize * (pageNum - 1)), pageSize * pageNum);
        const dataUpdataList = this.updataList(dataList);
        let result = {
          code: 200,
          result: {
            video_list: dataUpdataList
          }
        };

        setTimeout(() => {
          resolve(result);
        }, 1000);
      });
    }
  }
};
</script>

<style lang="scss" scoped>
.refresh {
  padding: 0 20px 20px 20px;
  .content {
    width: 100%;
    height: 320px;
    line-height: 320px;
    text-align: center;
    margin-top: 20px;
    // background-color: cadetblue;
    font-size: 120px;
  }
}
.refresh-top{
    width: 100%;
    padding: 12px 0;
    text-align: center;
    position: relative;
    font-size: 14px;
    color: #858D9A;
    line-height: 20px;
  }
  .refresh-top::after{
    content: "";
    position: absolute;
    width: 36px;
    height: 1px;
    background-color: #858D9A;
    top: 50%;
    right: 25%;
  }
  .refresh-top::before{
    content: "";
    position: absolute;
    width: 36px;
    height: 1px;
    background-color: #858D9A;
    top: 50%;
    left: 25%;
  }
</style>

处理闪动问题

有时候会发现,下滑的时候出现闪动的情况,这对于用户使用,是一种毁灭性的问题,我们解决一下这个问题

问题解决:处理页面闪动

有时候,在数据的头部添加新数据,页面会出现闪动。这是由于Vue的渲染机制造成的,新数据在前,旧数据被“挤下去”,当我们使用scrollTop从顶部迅速滑动到指定位置时,新数据和旧数据之间的不一致会导致页面闪动。

为了解决这个问题,我采取了以下步骤:

  1. 首先,我添加了一个遮罩层来阻止用户看到滑动的场面。
  2. 设计遮罩层的颜色和占比,使其完全覆盖数据区域。
  3. 开始滑动到原先用户所见的位置,监听数据向下滑动结束的回调。
  4. 取消遮罩层,显示数据。 这样,用户就不会看到页面闪动的情况了

在模板上的使用

都不变和以前是一样的,主要变动的是在使用滑动回调、遮罩层的修改。

数据参数

添加遮罩层的数据

// 其他的不变....
+ isShowMaskLayer: false, // 是否现在遮罩层
+ refreshTheTop: 0, // 距离顶部多少px的时候进行加载数据

下拉加载处理

主要请求过程过程

我做了一些改动来控制遮罩层的显示和滑动的行为。

// 主要请求过程...
    getapi() {
      this.getDataList(this.params).then((res) => {
        console.log(res);
        if (res.code == 200) {
          // 第一次添加数据
          if (this.params.page_num == 1) {
            // ...其他代码...
          } else {
            this.videoList.unshift(...res.result.video_list);
+           this.isShowMaskLayer = true; // 滑动时显示遮罩层
          }
          // 对后面的数据进行处理
          if (res.result.video_list.length !== 0 && this.params.page_num !== 1) {
            this.isRefreshDisabled = false;
+            window.scrollTo(0, this.refreshTheTop + 60);// 滑动大于refreshTheTop就行,为了激活loadingPage,并且不会触发加载
          }
          // 请求无数据的时候
          if (res.result.video_list.length == 0) {
            this.isRefreshDisabled = true;
            this.isRefreshLoading = false;
+           this.isShowMaskLayer = false; // 取消遮罩层显示
            window.removeEventListener("scroll", this.loadingPage);
          }
        }
        this.lockOneApiState = true;
      });
    },

主要滑动回调

我修改了滑动回调的逻辑,用于控制遮罩层的隐藏和页面的滑动。

定义了 refreshTheTop, 放入了 data

   loadingPage() {
      // 由于css的快顶部有20px。所以请求的数据尽量小于20 - 1,可以看看滑动顶部的打印
      console.log(this.domVantRefreh.getBoundingClientRect().top);
      // 距离顶部多少px的时候进行加载数据,最好再快也要用户看到加载状态的时候,进行了加载
r+    if (this.domVantRefreh.getBoundingClientRect().top >= this.refreshTheTop && this.lockOneApiState == true && this.isRefreshDisabled == false) {
        this.params.page_num++;
        this.getapi();
        this.lockOneApiState = false;
      }

+      if (this.lockOneApiState == true && this.isShowMaskLayer == true) {
+        // 请求结束且滑动接受
+        let heightElement = this.$refs.courseListHeight.clientHeight - this.courseListPageHeight;
+        this.courseListPageHeight = this.$refs.courseListHeight.clientHeight;
+        window.scrollTo(0, heightElement);
+        this.isShowMaskLayer = false; // 隐藏遮罩层
+        console.log("请求结束且滑动接受");
+      }
+    },

关于滑动的遮罩层颜色

不想查看,就用吸管去吸一下颜色:来源于背景的颜色

.after-content{
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: 来源于背景的颜色;
  z-index: 999999;
}

完整代码

上次也没有搞码上掘金这次就不搞了,直接上全代码

以下是优化后的完整代码:

<template>
  <div class="refresh">
    <van-pull-refresh 
      v-model="isRefreshLoading"
      pulling-text="下拉加载数据"
      loosing-text="释放即可刷新"
      :success-text="isRefreshDisabled ? '' : ''"
      :success-duration="800"
      :disabled="isRefreshDisabled"
      @refresh="onRefresh">
      <div v-if="isRefreshDisabled" class="refresh-top">已经到顶啦</div>
      <div v-show="isShowMaskLayer" class="after-content" />
      <div ref="courseListHeight">
        <div v-for="(item, index) in videoList" :key="index" class="content" :style="{backgroundColor:item.backgroundColor}">
          {{ item.content }}
        </div>
      </div>
    </van-pull-refresh>
  </div>
</template>

<script>
import { PullRefresh } from "vant";
export default {
  components: {
    "van-pull-refresh": PullRefresh
  },
  data() {
    return {
      dataList: [
        111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124,
        125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135
      ].reverse(),
      params: {
        page_size: 5,
        page_num: 1
      },
      isRefreshLoading: false, // 是否处于加载中状态
      isRefreshDisabled: false, // 是否禁用下拉刷新
      isShowMaskLayer: false, // 是否现在遮罩层
      videoList: [], // 数据内容
      courseListPageHeight: 0, // 页面的高度
      refreshTheTop: 0, // 距离顶部多少px的时候进行加载数据
      domVantRefreh: "", // 获取van-loading到顶部的高度
      lockOneApiState: false // 防止反复的请求api
    };
  },
  beforeMount() {
    this.getapi();
  },
  methods: {
    onRefresh() {
      // 获取van-loading到顶部的高度
      this.domVantRefreh = document.querySelector(".van-loading");
      window.addEventListener("scroll", this.loadingPage);
      this.params.page_num++;
      this.getapi();
    },
    getapi() {
      this.getDataList(this.params).then((res) => {
        console.log(res);
        if (res.code == 200) {
          // 第一次添加数据
          if (this.params.page_num == 1) {
            this.videoList = res.result.video_list;
            this.$nextTick(() => {
              window.scrollBy(0, 9999);
              this.courseListPageHeight = this.$refs.courseListHeight.clientHeight;
            });
          } else {
            this.videoList.unshift(...res.result.video_list);
            this.isShowMaskLayer = true; // 滑动的时候给予遮罩层
          }
          // 对后面的数据进行处理
          if (res.result.video_list.length !== 0 && this.params.page_num !== 1) {
            this.isRefreshDisabled = false;
            window.scrollTo(0, this.refreshTheTop + 60);// 滑动大于refreshTheTop就行,为了激活loadingPage,并且不会触发加载
          }
          // 请求无数据的时候
          if (res.result.video_list.length == 0) {
            this.isRefreshDisabled = true;
            this.isRefreshLoading = false;
            this.isShowMaskLayer = false;
            window.removeEventListener("scroll", this.loadingPage);
          }
        }
        this.lockOneApiState = true;
      });
    },
    loadingPage() {
      console.log(this.domVantRefreh.getBoundingClientRect().top);
      // 距离顶部多少px的时候进行加载数据,最好再快也要用户看到加载状态的时候,进行了加载
      if (this.domVantRefreh.getBoundingClientRect().top >= this.refreshTheTop && this.lockOneApiState == true && this.isRefreshDisabled == false) {
        this.params.page_num++;
        this.getapi();
        this.lockOneApiState = false;
      }

      if (this.lockOneApiState == true && this.isShowMaskLayer == true) {
        // 请求结束且滑动接受
        let heightElement = this.$refs.courseListHeight.clientHeight - this.courseListPageHeight;
        this.courseListPageHeight = this.$refs.courseListHeight.clientHeight;
        // window.scrollBy(0, heightElement);
        window.scrollTo(0, heightElement);
        this.isShowMaskLayer = false;
        console.log("请求结束且滑动接受");
      }
    },
    // 优化数据样式
    updataList(data) {
      const getRandomColor = (index) => {
        const letters = "0123456789ABCDEF";
        let color = "#";
        for (let i = 0; i < 6; i++) {
          color += letters[Math.floor(Math.random(index) * 16)];
        }
        return color;
      };

      return data.reverse().map((res) => {
        return {
          content: res,
          backgroundColor: getRandomColor(res)
        };
      });
    },
    // 伪装数据请求
    getDataList(params) {
      return new Promise((resolve) => {
        const pageNum = params.page_num;
        const pageSize = params.page_size;
        let dataList = this.dataList.slice((pageSize * (pageNum - 1)), pageSize * pageNum);
        const dataUpdataList = this.updataList(dataList);
        let result = {
          code: 200,
          result: {
            video_list: dataUpdataList
          }
        };

        setTimeout(() => {
          resolve(result);
        }, 1000);
      });
    }
  }
};
</script>

<style lang="scss" scoped>
.after-content{
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: #f5f5f5;
  z-index: 999999;
}
.refresh {
  padding: 0 20px 20px 20px;
  .content {
    width: 100%;
    height: 320px;
    line-height: 320px;
    text-align: center;
    margin-top: 20px;
    // background-color: cadetblue;
    font-size: 120px;
  }
}
.refresh-top{
    width: 100%;
    padding: 12px 0;
    text-align: center;
    position: relative;
    font-size: 14px;
    color: #858D9A;
    line-height: 20px;
  }
  .refresh-top::after{
    content: "";
    position: absolute;
    width: 36px;
    height: 1px;
    background-color: #858D9A;
    top: 50%;
    right: 25%;
  }
  .refresh-top::before{
    content: "";
    position: absolute;
    width: 36px;
    height: 1px;
    background-color: #858D9A;
    top: 50%;
    left: 25%;
  }
</style>