likes
comments
collection
share

Hooks实现懒加载LoadMore —— Vue 3 TypeScript Hooks 实战

作者站长头像
站长
· 阅读数 23

前言

vue 3 的出现,引入了一系列的新特性和改进,其中最引人注目的就是 Composition API啦,Vue 3 不仅带来了性能上的提升,还通过 Composition API 引入了类似 React Hooks 的功能,极大地改善了状态管理组件复用的方式。而 TypeScript 作为一种静态类型检查的语言,能够帮助开发者在编写代码时发现潜在的错误,提高代码质量的同时也增强了团队协作的能力。Composition API 是一种全新的编程模型,它允许开发者以函数的形式组织和复用组件逻辑。下面通过Composition API 中“Hooks”来实现loadmore(懒加载),和我一起往下看看吧~

正文

准备工作

创建项目

  1. 打开终端或者cmd输入npm init vite
  2. 输入自己的项目名称(v3-ts-hooks-loadmore)
  3. 选择vue
  4. 选择typescript
  5. 打开创建的文件夹

安装依赖

  1. npm i
  2. npm i pinia

文件夹位置

Hooks实现懒加载LoadMore —— Vue 3 TypeScript Hooks 实战

types/article.ts

定义一个名为article的接口,该接口具有三个属性的对象结构:id、name、desc,通过export将这个接口抛出,使其可以被其它模块导入和使用:

export interface article{
    id: number,
    name: string,
    desc: string
}

store/article.ts

使用Pinia库来创建一个名为useArticleStore的状态管理store,来负责管理文章的相关数据:

  • 导入库和类型
import { defineStore } from "pinia";
2import { ref } from "vue";
3import type { article } from "../types/article";
  • 定义store(useArticleStore)并将其抛出:
  1. useArticleStore是一个函数,返回一个store对象
  2. article是store的唯一标识符,在其他地方可以通过这个ID来访问store
export const useArticleStore = defineStore("article", () => {})

下面来介绍这个useArticleStore函数:

  • 定义一个私有变量_articles(私有变量就是只能在它的作用域内访问和修改它),该变量是一个数组,里面包含多个文章对象
   const _articles = [
     // ... 多个文章对象
  ];
  • 定义一个响应式数据articles,它初始为一个空数组,它可被组件和其他的store使用,<article[]>这个为泛型,用来对类型进行约束,提供了类型的安全性
  const articles = ref<article[]>([]);
  • 定义一个获取文章列表的方法getArticles,该方法是一个异步方法:
  1. 接收两个参数page(页码)和size(每页的数量)
  2. 使用Promise来模拟数据的异步加载
  3. 对resolve里的数据类型进行约束
<{ // resolve里数据的类型
2         data: article[];
3         page: number;
4         total: number;
5         hasMore: boolean;
6       }>

下面介绍异步加载数据的逻辑:

  1. Promise的resolve函数返回一个对象,对象中包含当页的数据、页码、总条数和是否还有更多的文章数据信息
  2. 使用setTimeout来模拟网络的延迟
  3. 使用slice方法来分页获取_articles数组中的文章数据
  4. 将获取到的文章数据追加到articles数组中
  5. resolve函数返回的数据有:
  • data:当前页的文章数据

  • page:请求的页码

  • total:总共的文章数量

  • hasMore:是否还有更多的文章数据

  • 返回store的公共部分

import { defineStore } from "pinia";
import { ref } from "vue";
import type { article } from "../types/article";

export const useArticleStore = defineStore("article", () => {
   // 文章数据集
   const _articles = [
      {
       
        id: 1,
        name: '张三' ,
        desc: '擅长:睡眠障碍、运动神经元病、头晕、头痛...',
      },
      {
        id: 2,
        name: '张三' ,
        desc: '擅长:睡眠障碍、运动神经元病、头晕、头痛...',
      },
      {
          id: 3,
          name: '张三' ,
          desc: '擅长:睡眠障碍、运动神经元病、头晕、头痛...',
        },
        {
          id: 4,
          name: '张三' ,
          desc: '擅长:睡眠障碍、运动神经元病、头晕、头痛...',
        },
        {
          id: 5,
          name: '张三' ,
          desc: '擅长:睡眠障碍、运动神经元病、头晕、头痛...',
        },
        {
          id: 6,
          name: '张三' ,
          desc: '擅长:睡眠障碍、运动神经元病、头晕、头痛...',
        },
        {
          id: 7,
          name: '张三' ,
          desc: '擅长:睡眠障碍、运动神经元病、头晕、头痛...',
        },
        {
          id: 8,
          name: '张三' ,
          desc: '擅长:睡眠障碍、运动神经元病、头晕、头痛...',
        },
        {
          id: 9,
          name: '张三' ,
          desc: '擅长:睡眠障碍、运动神经元病、头晕、头痛...',
        },
        {
          id: 10,
          name: '张三' ,
          desc: '擅长:睡眠障碍、运动神经元病、头晕、头痛...',
        },
        {
          id: 11,
          name: '张三' ,
          desc: '擅长:睡眠障碍、运动神经元病、头晕、头痛...',
        },
        {
          id: 12,
          name: '张三' ,
          desc: '擅长:睡眠障碍、运动神经元病、头晕、头痛...',
        },
        {
          id: 13,
          name: '张三' ,
          desc: '擅长:睡眠障碍、运动神经元病、头晕、头痛...',
        },
        {
          id: 14,
          name: '张三' ,
          desc: '擅长:睡眠障碍、运动神经元病、头晕、头痛...',
        },
        {
          id: 15,
          name: '张三' ,
          desc: '擅长:睡眠障碍、运动神经元病、头晕、头痛...',
        },
        {
          id: 16,
          name: '张三' ,
          desc: '擅长:睡眠障碍、运动神经元病、头晕、头痛...',
        },
        {
          id: 17,
          name: '张三' ,
          desc: '擅长:睡眠障碍、运动神经元病、头晕、头痛...',
        },
        {
          id: 18,
          name: '张三' ,
          desc: '擅长:睡眠障碍、运动神经元病、头晕、头痛...',
        },
        {
          id: 19,
          name: '张三' ,
          desc: '擅长:睡眠障碍、运动神经元病、头晕、头痛...',
        },
        {
          id: 20,
          name: '张三' ,
          desc: '擅长:睡眠障碍、运动神经元病、头晕、头痛...',
        },
        {
          id: 21,
          name: '张三' ,
          desc: '擅长:睡眠障碍、运动神经元病、头晕、头痛...',
        },
        {
          id: 22,
          name: '张三' ,
          desc: '擅长:睡眠障碍、运动神经元病、头晕、头痛...',
        },
        {
          id: 23,
          name: '张三' ,
          desc: '擅长:睡眠障碍、运动神经元病、头晕、头痛...',
        },
        {
          id: 24,
          name: '张三' ,
          desc: '擅长:睡眠障碍、运动神经元病、头晕、头痛...',
        }
      ]; // 私有变量

   // 响应式文章数据
   const articles = ref<article[]>([]);

   // 获取每页文章列表 action
   const getArticles = (page: number, size: number = 10) => {
       // resolve 后的数据类型约束
       return new Promise<{ // resolve里数据的类型
         data: article[];
         page: number;
         total: number;
         hasMore: boolean;
       }>((resolve => {
          setTimeout(() => {
             // 按页切割得到当前页数据
             const data = _articles.slice((page - 1) * size, page * size);
             articles.value = [...articles.value, ...data];
             // 追加数据
             resolve({
                 data,
                 page,
                 total: _articles.length,
                 // 是否还有更多数据,如果没有数据后,就false
                 hasMore: page * size < _articles.length
             });
          }, 500);
       }));
   }

   return {
      articles,
      getArticles
   }
}
);

hooks/useIntersectionObserver.ts

IntersectionObserver:现代浏览器提供的 API,用于监测一个元素(目标元素)是否出现在视口(可视区域)内。当目标元素与视口相交时,IntersectionObserver会触发回调函数,报告目标元素与视口的相交情况。

自定义一个Hook(使用函数来组织和复用组件逻辑),封装了IntersectionObserver的创建和管理逻辑:

  • 导入必要的库
import { ref, watch, onMounted } from "vue";
import type { Ref } from "vue";
  • 定义hook,定义两个形参
  1. nodeRef:是一个Ref,它指向页面上的一个DOM元素或null
  2. loadmore:一个函数,当目标元素进入视口后会被调用,用于加载更多的数据
  • 创建IntersectionObserver实例:observer为一个IntersectionObserver实例,初始为null
  • 通过watch来观察nodeRef的变化
  1. nodeRef的值发生变化时,会执行回调函数
  2. 如果oldNodeRef存在且observer已经创建,那么取消对旧节点的监听
  3. 如果newNodeRef存在,则创建一个新的IntersectionObserver实例,开始监听新节点
  • 创建IntersectionObserver实例,传入一个回调函数,当被观察的元素进入视口时,这个回调函数会被调用,通过isIntersecting属性来判断元素是否进入了视口
  • 组件卸载时取消监听,当组件挂载时,如果 observer 存在,则取消所有监听,防止内存泄漏
  • 监听hasMore的变化,hasMore为一个响应式数据,初始值为true,当hasMore发生变化时,根据新值来决定是否继续监听nodeRef
  • 返回一个对象:
  1. hasMore
  2. setHasMore方法:访问和更新hasMore的值

App.vue

这里介绍一下ts的部分主要实现的功能有:

  1. 初始化文章数据
  2. 滚动加载更多
  • 导入库和自定义的hook
  • 初始化store和状态:
  1. 初始化store实例articleStore,它包含文章数据和相关的方法
  2. onMounted在组件挂载后调用,用于异步加载第一页的文章数据
  3. 定义一个变量articles,为ref类型(toRefs 是 Vue 3 中 Composition API 提供的一个实用函数,它用于将一个响应式对象(通常是 store 中的状态)转换为一个包含多个 ref 的对象),包含文章数据
  • 定义DOM引用和状态
  1. itemRef为一个DOM元素,用于IntersectionObserver 的监听
  2. hasMore来表示是否还有更多的文章数据可以加载
  3. currentPage为一个数字类型的ref类型,表示当前页数
  • 定义加载下一页的函数handleNextPage
  1. 为一个异步函数,用于加载下一页的数据
  2. 接收一个setHasMore函数作为参数,来更新hasMore的值
  3. currentPage递增,表示加载下一页
  4. 调用articleStore.getArticles 方法来获取下一页的文章数据
  5. 如果没有更多的文章数据,也就是hasMore为false,通过setHasMore来更新hasMore的状态,来表示没有更多数据来加载
  • 使用自定义的IntersectionObserver Hook
  1. useIntersectionObserver 是一个自定义的 Hook,它接收 itemRef 和一个回调函数作为参数
  2. 回调函数会在目标元素进入视口时被调用,这里调用了 handleNextPage 函数,并传递了 setHasMore 作为参数
<script setup lang="ts">
import { ref, onMounted, toRefs } from 'vue'
import {useArticleStore } from './store/article'
import  useIntersectionObserver  from './hooks/useIntersectionObserver.ts'
const articleStore = useArticleStore()
onMounted(async () => {
  await articleStore.getArticles(1)
})
const { articles } = toRefs(articleStore)

const itemRef = ref<HTMLElement | null>(null);
let hasMore = ref<boolean>(true);
// 定义当前的页数,初始值为1
const currentPage = ref<number>(1);

// 处理加载下一页
const handleNextPage = async (setHasMore:(value:boolean) => void) =>{
  currentPage.value++;
  const res = await articleStore.getArticles(currentPage.value);
  if(!res.hasMore){
    setHasMore(false);
    hasMore.value = false;
  }
}

const { setHasMore } = useIntersectionObserver(itemRef, ()=>{
  handleNextPage(setHasMore);
})



</script>

<template>
  <section>
    <article 
      class="item" 
      v-for="(item, index) in articles" 
      :key="item.id"
      :ref="(el)=>(index === articles.length-1 ? (itemRef = el as HTMLElement) : '')"
    >
        <div >{{item.name}}</div>
        <div >{{item.desc}}</div>
    </article>
    <div v-if="!hasMore">
      <div>没有数据了</div>
    </div>
  </section>
</template>

<style scoped>
.item{
  height: 20vh;

}
</style>

实现效果

Hooks实现懒加载LoadMore —— Vue 3 TypeScript Hooks 实战

结语

Hooks实现懒加载LoadMore —— Vue 3 TypeScript Hooks 实战

转载自:https://juejin.cn/post/7395868215622483980
评论
请登录