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

前言

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

正文

准备工作

创建项目

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

安装依赖

  1. npm i
  2. npm i pinia

文件夹位置

types/article.ts

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

typescript 复制代码
export interface article{
    id: number,
    name: string,
    desc: string
}

store/article.ts

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

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

下面来介绍这个useArticleStore函数:

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

typescript 复制代码
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的创建和管理逻辑:

  • 导入必要的库
python 复制代码
import { ref, watch, onMounted } from "vue";
import type { Ref } from "vue";
  • 定义hook,定义两个形参
  1. nodeRef:是一个Ref,它指向页面上的一个DOM元素或null
  2. landmore:一个函数,当目标元素进入视口后会被调用,用于加载更多的数据
  • 创建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 作为参数
typescript 复制代码
<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>

实现效果

结语

相关推荐
一个处女座的程序猿O(∩_∩)O1 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
hackeroink4 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者6 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-6 小时前
验证码机制
前端·后端
燃先生._.7 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
超爱吃士力架8 小时前
邀请逻辑
java·linux·后端
高山我梦口香糖8 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235248 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240259 小时前
前端如何检测用户登录状态是否过期
前端