vue3无限滚动

Vue很常用、Vue3也讨论比较多,所以这次Blue带大家拿Vue3做个”无限下拉加载”的应用(我不太擅长起名字,谁有好名字欢迎贡献出来😂),先来看看效果

0

简单说一下,当我们向下滚动页面快滚到底部时,会继续加载更多数据,让用户可以继续往下滚——实现无限下拉的效果,那么,我们想做到这点,需要考虑几件事:

  1. 一些基础的:项目搭建、基本布局
  2. 如何加载数据
  3. 如何监听下拉、如何判断到底了
  4. 组件如何划分、功能谁来负责

第一步、创建项目

既然咱们要做东西,肯定要先有个项目,动手来创建一下

创建项目

如果你还没有vue的cli,可以先装一下(有了也可以升级一下,最新版cli才支持vue3):

npm install -g @vue/cli  #如果嫌慢,可以配一下cnpm的淘宝源或者yarn,快很多
1

然后我们直接用vue命令完成剩下的创建工作

vue create endless-scroll  #endless-scroll是项目的名称,你可以随便取自己喜欢的

接下来它会让你选择项目的类型,我们这里用vue3

2

下面那个”手动选择”有很多特性可用,我们这里暂时不用,有兴趣大家可以自己翻一下看看,各种测试啥的

接下来就是漫长的等待了,可以泡杯茶或者转一圈,时间大概10来分钟

3

茶喝完了,它也完事了,接下来我们可以试试启动它

cd endless-scroll  #跟着你自己的项目名走
npm run serve      #它里面有三种常用启动方式,serve是开发期调试用的
4

这样就算没问题了,可以通过红框里的地址来访问我们的项目了(一般本机用localhost那个就行),如果一切顺利,打开http://localhost:8080会看到如下的默认内容

5

那么,我们的开发之旅就算正式开始了

项目结构

首先,我们先来做一点小事,认识一下我们刚创建的项目,该删的删,轻装上阵

6

简简单单给大家说一下

.git             #git本地库,版本控制用的,我们这儿是个测试项目可以删
node_modules     #所有的依赖模块,以后装的第三方模块也会放在这儿,极其脆弱,得供着
public           #静态公共文件(例如index.html、图标啥的)
src              #项目的核心,你写的代码都在这儿
.gitignore       #上传git的过滤器,一般本地IDE也会来读,留着
babel.config.js  #babel配置,留着
package.json     #项目配置文件——依赖模块、启动命令、项目配置啥的
README.md        #说明文件,可以看看,看完可以删掉
yarn.lock        #模块版本锁,留不留都行(删了还会自动创建),锁定版本用的

做个扫除

稍微删了一下,现在张这样

7

不光目录,代码里面我们也整理整理,首先,组件删一下,它带的那个HelloWorld没啥用,直接删就行

8

最后把我们的App.vue根组件也给清理一下,留个template就够了

清理前

9

清理后

10

至此为止,准备工作彻底完事,来吧,开始搞东西

第二步、搞定布局

那么我们从何开始呢,先来搭个大框架吧

11

直接开始写

<template>
  <div class="container">
    <div class="left">内容区</div>
    <div class="right">列表</div>
  </div>
</template>

<style>
/* 把样式重置一下,这里没仔细写,不过重置样式大家都会的,就不浪费时间了 */
* {
  margin: 0;
  padding: 0;
  list-style: none;
}
body {
  background: #389acc;
}
</style>

<style scoped>
/* App自己的样式 */
.container {
  width: 1080px;
  margin: 50px auto;
  display: flex;
}

.left {
  flex: 1;
  background: #fff;
  margin-right: 10px;
  text-align: center;
  line-height: 200px;
  color: #ccc;
  font-size: 26px;
}
.right {
  width: 350px;
  background: #fff;
}
</style>

这里我们搞了两个div,左边的自动大小,右边的固定,结果就长这样

12

创建List组件

大框架搭好了,那么接下来,我们搭一些细节的东西,因为我们要做的就是右边的list,所以单独拉一个组件出来,方便我们折腾

13

接下来,先试试它能不能出来,没问题了再接着往下写,我们从App.vue中引入它

14

这里我们要做几件事:

  • 指定开发语言(lang="ts"):vue3对ts的支持已经比较完善了(和v2相比),那自然用起来啊,强类型多香啊
  • 定义一个组件(defineComponent):帮我们定义组件——主要是ts里的各种类型,非常方便

结果长这样

15

然后,我们需要引入刚刚写好的List组件(虽然没啥东西吧),三件事:

  • import引入List组件:这个简单,我们用东西都要先引入的
  • 注册List组件:组件中可以用components来注册局部组件,然后在template中使用
  • 添加到模板中
16

这时候,我们就可以在页面中看到List组件了

17

基本布局已经搞定了,下面我们正式开始编写List

编写List组件

首先,不考虑功能,列表得有个样子吧,所以咱们加一些html和样式:

<!-- List.vue的布局 -->
<template>
  <div>
    <!-- 我们搞两条数据示意一下 -->
    <div class="item">
      <h3 class="header">这是一个标题,可能有点长,是的,特别特别长的那种</h3>
      <p class="content">
        这是一些文本的内容啊内容,这是一些文本的内容啊内容,这是一些文本的内容啊内容,这是一些文本的内容啊内容
      </p>
    </div>
    <div class="item">
      <h3 class="header">这是一个标题,可能有点长,是的,特别特别长的那种</h3>
      <p class="content">
        这是一些文本的内容啊内容,这是一些文本的内容啊内容,这是一些文本的内容啊内容,这是一些文本的内容啊内容
      </p>
    </div>
  </div>
</template>

这时候还没加样式,所以我们的东西长这样:

18

亲娘啊,真的丑,没关系,我们来装饰一下

<style scoped>  /* scoped-局部样式 */
.item {
  box-sizing: border-box;
  width: 350px;
  height: 180px;
  padding: 20px 20px;
  background: #fff;
  border: 1px solid #ccc;
  margin-bottom: -1px; /* 上下边框合并一下 */
}
.header {
  font-size: 22px;
  margin-bottom: 20px;

  /* 文本过长自动截断 */
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.content {
  color: #999;
  font-size: 14px;
  line-height: 26px;
  height: 78px;

  /* 多行文本截断 */
  overflow: hidden;
  text-overflow: ellipsis;
  -webkit-line-clamp: 3;
}
</style>

来看看是不是好多了:

19

咱不敢说多惊艳吧,至少能见人了,对吧

第三步、加上数据

到目前这一步,有点东西了,不过还是死的,直接写出来的肯定不行,我们要数据,先从最简单的开始

第一版、静态数据

List.vue里,我们加上数据

<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
  setup() {
    //在这里我们先做一个静态数据,调好了再读服务器的
    const datas = [
      {
        ID: 1,
        title: "深夜突发!多个航班无法降落,疑因有不明飞行器活动",
        content:
          "记者从民航爱好者常使用的一款飞机轨迹查询软件看到,4日晚的确有多趟航班在抵达杭州萧山国际机场前在空中进行了盘旋等待,或备降到了周边的宁波栎社国际机场。",
      },
      {
        ID: 2,
        title:
          "湖州长兴有家公司破产资产2亿元起拍,背后有一个“最快跌落的中国首富”的身影",
        content:
          "是的,“汉能”,“汉能薄膜”在2013年和2014年间曾经是港股里最看不懂的“妖股”,而它的创始人李河君在2015年3月以1600亿的身家荣登中国首富,不过仅仅3个月,其身家就暴跌1000亿,被称为最快跌落的首富。",
      },
    ];

    return {
      datas, //返回出去,以便在template里使用
    };
  },
});
</script>

然后还是List.vue,我们加上循环和输出

<template>
  <div>
    <div class="item">
      <h3 class="header">这是一个标题,可能有点长,是的,特别特别长的那种</h3>
      <p class="content">
        这是一些文本的内容啊内容,这是一些文本的内容啊内容,这是一些文本的内容啊内容,这是一些文本的内容啊内容
      </p>
    </div>
  </div>
</template>

<!-- 改成 -->

<template>
  <div>
    <div class="item" v-for="item in datas" :key="item.ID">
      <h3 class="header">{{ item.title }}</h3>
      <p class="content">{{ item.content }}</p>
    </div>
  </div>
</template>

看看效果怎么样

20

挺好的,该有的一个不缺,那么接下来,我们要让数据动起来

如何使用动态数据

首先,想读取数据,肯定需要ajax,这一次我们重点不是ajax本身,所以简单点,直接搞个axios来用用

先安装,ctrl+c把项目停掉,然后安装(注意,混用包管理器容易出问题)

npm i axios -S
#或
cnpm i axios -S
#或
yarn add axios

装完

21

接下来,我们需要一个文件来装数据(本课为了简化,不涉及服务器开发,大家感兴趣的话,会单独来说)

22

顺便一说,public里的文件,会被原样复制到编译结果中,所以上线后这套东西依然能访问

读取数据

<script lang="ts">
import { defineComponent } from "vue";
//引进来准备读数据
import axios from "axios";

export default defineComponent({
  setup() {
    //读的就是我们刚才的那个文件
    axios("/datas.json").then((res) => {
      console.log(res.data); //读过来的东西打出来瞅瞅
    });

    return {
      datas: [], //暂时搞个空的
    };
  },
});
</script>

跑起来看看效果(别忘了启动服务npm run serve

23

不错,该有的都有了

更新数据

接下来需要面对另一个问题——如何更新数据

在Vue3中,我们有多种方式来构建”响应式数据”,所谓响应式,就是在你修改这个数据后,视图会自动重新渲染,以便响应你对数据的改动,这里我们选择ref,对单个数据(比如这里的数组)使用起来很方便

如何使用Ref

我们先用个简单的例子,看看如何使用ref

<template>
  <div>
    <div>
      姓名:{{ name }}
      <button @click="name += 'a'">修改</button>
    </div>
    <div>
      年龄:{{ age }}
      <button @click="age++">修改</button>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";

export default defineComponent({
  setup() {
    //准备两个数据作为对比
    let name = "blue"; //普通数据(非响应)
    let age = ref(18); //响应数据

    return { name, age };
  },
});
</script>

运行起来长这样:

24

先不管它丑不丑哈,我们点击会怎样?

25

上图我们可以看到两件奇怪的事:

  • 修改name(普通变量)没反应
  • 修改age(响应数据)有反应,而且连name的修改一起生效了

其实很简单,一个个解释:

  • 为什么修改name没反应? 出于性能考虑,vue并不会监听所有改动,只有特定的数据(ref、reactive等)才会触发渲染
  • 为什么agename一起生效了? 因为重新渲染时,vue会检查所有数据(包括非响应数据)的改动,自然name的改动也被发现了

第二版、服务器数据

是时候把我们上面的东西整合起来了

<template>
  <div>
    <div class="item" v-for="item in datas" :key="item.ID">
      <h3 class="header">{{ item.title }}</h3>
      <p class="content">{{ item.content }}</p>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";
import axios from "axios";

export default defineComponent({
  setup() {
    const datas = ref([]); //能响应的数组(人话版:改了会自动更新HTML的数组)

    axios("/datas.json").then((res) => {
      //1-ref需要.value,它本质上是一个reactive({value})
      //2-把老数据+新数据合并为一个新的数组
      datas.value = [...datas.value, ...res.data];
    });

    return {
      datas, //输出到页面中使用
    };
  },
});
</script>

看看能不能出来:

26

数据这块基本搞定,不过有个问题,拉到最下面它居然不会自动更新(废话,还没做呢😂),怎么办,继续搞

第四步、监听页面滚动

其实我们想自动加载挺简单的,两件事:

  • 监听滚动
  • 发现快滚到底了,就再来点新的

我们按照这个步骤来

添加监听

首先,我们知道vue里大部分事件都是@xxx来添加的,但滚动事件是window的啊,我们不能window.@scroll这么鬼畜吧?其实很简单,手动加上就是了

import { onMounted, onUnmounted } from "vue";

export default defineComponent({
  setup() {
    //滚动事件处理函数
    function scrollHandle() {
      console.log('滚了');
    }

    onMounted(() => {
      //组件挂载时,添加scroll监听
      window.addEventListener("scroll", scrollHandle, false);
    });
    onUnmounted(() => {
      //组件卸载时,停止监听
      window.removeEventListener("scroll", scrollHandle, false);
    });
  }
});

效果怎么样呢?

27

看来没问题,确实能提醒我们页面滚动了,那么第二个问题来了——我们怎么知道快到底了

它到底快到底了没?

什么玩意,整的跟绕口令似的😂

简单来说,我们需要检测”用户是否快要滚到底部了”,那么如何做到这点?跟Blue一起先来看个图

28

画的比较粗糙(你管这叫比较粗糙??),不过意思很明确:页面很高,可视区在页面中上下滑动(onscroll),我们可以计算可视区和底部的距离,小于某个阈值就可以认为”快到头了”

说着没问题,那具体怎么计算这个”距离”呢,再来看个图吧

29

页面里有几个数值可以用

  • #1-scrollTop:滚动距离
  • #2-scrollHeight:页面内容总高度
  • #3-clientHeight:可视区高度
//距离=总高-滚动距离-可视区高
let distance = scrollHeight - scrollTop - clientHeight;

放到代码里试试哈

30

看看结果

31

结果出来是个-100左右的值,为啥啊?其实特简单,我们的margin造成的

32

因为margin不算做物体本身的高度,所以搞的内容比实际看来小了一块,咋办?改呗

33

试试效果哈

34

这回好多了,所以我们只要判断这个值小到一定程度(比如200,看你需求)

35

我们来试试

36

但是又有问题啊,它出现了好多次呢,咋办?

防止重复加载

简单来说,我们可以在触发加载后,就把他”锁住”,在加载完成前,不允许其再次触发

//加载逻辑

//修改前
axios("/datas.json").then((res) => {
  datas.value = [...datas.value, ...res.data];
});


//修改后
let readyForLoad = true; //默认允许加载一次

if (readyForLoad) {
  //需要加载才进来,防止重复
  readyForLoad = false; //进来了就"锁上"

  axios("/datas.json").then((res) => {
    datas.value = [...datas.value, ...res.data];
    readyForLoad = true; //加载完了才"开锁",允许再次触发
  });
}

因为我们需要经常加载,所以把他封装成函数,变成这样

let readyForLoad = true; //默认允许加载一次

function loadMore() {
  if (readyForLoad) {
    //需要加载才进来,防止重复
    readyForLoad = false; //进来了就"锁上"

    axios("/datas.json").then((res) => {
      datas.value = [...datas.value, ...res.data];
      readyForLoad = true; //加载完了才"开锁",允许再次触发
    });
  }
}

然后我们的setup会变成这样:

setup() {
  //滚动事件处理函数
  function scrollHandle() {
    ...省略部分代码...

    if (distance <= 200) {
      console.log("快到底了!");
      loadMore(); //多次触发没关系,加载完之前只能有一个实际执行
    }
  }

  onMounted(...);
  onUnmounted(...);

  const datas = ref([]);

  let readyForLoad = true; //默认允许加载一次

  loadMore(); //初始加载一次
  function loadMore() {
    if (readyForLoad) {
      //需要加载才进来,防止重复
      readyForLoad = false; //进来了就"锁上"

      axios("/datas.json").then((res) => {
        datas.value = [...datas.value, ...res.data];
        readyForLoad = true; //加载完了才"开锁",允许再次触发
      });
    }
  }

  return {
    datas, //输出到页面中使用
  };
},

试试效果,基本没问题了(GIF这格式太坑了,压出来居然有10M,webm才几百K好不,慎点)

37

总结

是时候梳理一遍Blue讲过的东西了,那么首先

15-三连
  • 使用@vue/cli快速搭建项目,方便
  • 用axios完成数据的读取
  • mountunmount里,监听页面scroll滚动事件
  • 计算页面底部距离(距离=总高-滚动距离-可视区高),小于一定阈值(例如200)则视为触底
  • 通过”节流”的方式,防止重复触发加载动作
  • 加载后,[...oldData, ...newData]将所有数据连接起来,构成新的数组使用
  • 通过ref实时更新视图

发表回复