项目亮点:用ts+hooks+pinia手写一个懒加载

526 阅读8分钟

前言

今天给大家介绍的是如何用ts+hooks+pinia手写一个懒加载,让其可以成为我们项目之中的一大亮点,那么什么是懒加载呢,所谓的懒加载就是一种优化技术,它允许开发者推迟加载某些资源,直到这些资源被实际需要时才加载。这种方法可以显著提高网页和应用的加载速度和性能,因为它减少了初始加载时必须下载的数据量。我们可以通过下面的视频更好的理解。

65885107a4dc2685821e1e99ceb27b8e.gif 下面我们通过代码来实现这个功能。

正文

准备工作

首先先做好准备工作,我们先通过在终端选择创建一个vue项目并选择我们后面要用的TypeScript,然后在用npm i pinia装一下pinia库,操作如下: image.png

image.png

开始动手

我们现在进入正题,首先我们在src目录下创建我们需要用的文件,分别是hooks文件夹里的useIntersectionObserver.tsstore文件夹里的article.tstypes文件夹里的article.ts。如下图:

image.png

当前页面:type/article.ts

然后我们来到type文件夹的article.ts文件,对我们要在创建的仓库中的Article类型的对象进行泛型约束,任何符合 Article 类型的对象必须具有 idtitle 这两个属性,并且 id 必须是一个数字,title 必须是一个字符串,泛型约束是TypeScript中非常有用的功能,它允许你编写更加灵活和安全的代码。通过定义类型参数必须满足的条件,你可以确保你的泛型函数或类能够正确处理预期的类型,同时保持代码的简洁性和可维护性。如果不按这样写,系统就会爆红,便于我们代码类型的正确性。

export interface Article{
    id:number;
    title:string;
}

当前页面:main.ts

接着我们来到src下的main.ts,来全局引入一下我们的pinia,并use一下:

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { createPinia } from 'pinia'

createApp(App)
    .use(createPinia())
    .mount('#app')

当前页面:store/article.ts

我们再来到store文件夹下的article.ts文件,代码如下:

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

export const useArticleStore = defineStore('article',()=>{

    // 文章数据集
    const _articles = [
        {
            id:1,
            title:"恭喜拿下百度"
        },
        {
            id:2,
            title:"恭喜拿下百度"
        },
        {
            id:3,
            title:"恭喜拿下百度"
        },
        {
            id:4,
            title:"恭喜拿下百度"
        },
        {
            id:5,
            title:"恭喜拿下百度"
        },
        {
            id:6,
            title:"恭喜拿下百度"
        },
        {
            id:7,
            title:"恭喜拿下百度"
        },
        {
            id:8,
            title:"恭喜拿下百度"
        },
        {
            id:9,
            title:"恭喜拿下百度"
        },
        {
            id:10,
            title:"恭喜拿下百度"
        },
        {
            id:11,
            title:"恭喜拿下百度"
        },
        {
            id:12,
            title:"恭喜拿下百度"
        },
        {
            id:13,
            title:"恭喜拿下百度"
        },
        {
            id:14,
            title:"恭喜拿下百度"
        },
        {
            id:15,
            title:"恭喜拿下百度"
        },
        {
            id:16,
            title:"恭喜拿下百度"
        },
        {
            id:17,
            title:"恭喜拿下百度"
        },
        {
            id:18,
            title:"恭喜拿下百度"
        },
        {
            id:19,
            title:"恭喜拿下百度"
        },
        {
            id:20,
            title:"恭喜拿下百度"
        },
        {
            id:21,
            title:"恭喜拿下百度"
        },
        {
            id:22,
            title:"恭喜拿下百度"
        },
        {
            id:23,
            title:"恭喜拿下百度"
        },
        {
            id:24,
            title:"恭喜拿下百度"
        },
        {
            id:25,
            title:"恭喜拿下百度"
        },
        {
            id:26,
            title:"恭喜拿下百度"
       },
    ]

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

    // 获取每页文章列表 action

    const getArticles = (page:number, size:number = 10) => {
        // resolve 后的数据类型约束
        return new Promise<{
            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
    }
}
)
  1. 导入必要的依赖

    • 从 pinia 导入 defineStore,用于创建 Pinia store
    • 从 vue 导入 ref,用于创建响应式引用。
    • 从 ../types/article 导入 Article 类型,用于定义文章对象的结构。
  2. 定义 Store

    • 使用 defineStore 创建一个名为 'article' 的 store,这个store里有我们的数据和相应方法。
  3. 定义变量

    • _articles:定义的一个私有数据,只在这个store里使用,不拿到外面去。每个文章对象都有 id 和 title 属性。
    • articles:定义的一个响应式变量,用Article类型约束着,初始为空数组,在后面用来承接 _articles的数据。
  4. 定义方法

    • getArticles:定义的一个异步函数,用于获取分页的文章数据。它接受两个参数:page 和 size,分别表示要获取的页码和每页的文章数量。这个 函数返回一个 Promise,其中包含文章数据、当前页码、总数以及是否还有更多数据的信息。
  5. 方法功能

    • 使用 Promise 和 setTimeout 模拟异步操作和网络延迟。
    • 使用 slice 方法从 _articles 数组中截取指定范围的数据。
    • 将截取的数据追加到 articles 变量中,使其变为响应式状态的一部分。
    • resolve 返回一个对象,包含了当前页面的数据、页码、总数以及是否还有更多数据的信息。
  6. 返回 Store 的状态和方法

    • 最后返回一个对象,包含了 articles 状态和 getArticles 方法。这样,当其他组件使用这个 store 时,就可以访问这些状态和方法。

当前页面:App.vue

<script setup lang="ts">
import { ref, onMounted, toRefs } from 'vue'
import {useArticleStore } from './store/article.ts'
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.title}}</div>
    </article>
    <div v-if="!hasMore">
      <div>没有数据了</div>
    </div>
  </section>
</template>

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

}
</style>

  1. 声明变量及初始化:

    • articleStore: 从 useArticleStore 获取文章列表的 store。
    • articles: 从 store 中提取文章列表,在滚动页面前先加载第一面数据。
    • itemRef: 用于跟踪页面底部元素的引用。
    • hasMore: 布尔值类型,表示是否还有更多文章可以加载,开始为ture,当没有时变为false。
    • currentPage: 当前加载的页数。
  2. 加载文章:

    • 在组件挂载完成后,通过 onMounted 钩子异步加载第一页的文章。
  3. 处理加载下一页:

    • 定义 handleNextPage 函数,增加 currentPage 的值,并检查返回的数据是否有更多的文章可以加载。
    • 如果没有更多文章,及store里的hasmMore变为false时,则通过 setHasMore 函数更新 hasMore 为 false
  4. 使用 useIntersectionObserver 钩子函数:

    • 函数形参为一个节点和一个函数,就是上面代码中的itemRef为最后一组数据的最后一个节点和handleNextPage函数
    • 当传入的 DOM 引用改变时,创建或更新一个 IntersectionObserver 实例来监听该元素。
    • 如果元素进入视口,则触发加载更多数据的函数landmore(就是上面代码中的handleNextPage 函数)。
    • 使用 watch 函数监听 nodeRef 的变化,当 nodeRef 的值改变时,取消对旧元素的监听,并开始监听新元素。
    • 使用 onUnmounted 生命周期钩子,在组件卸载时取消所有监听以避免内存泄漏。
    • 使用 watch 函数监听 hasMore 的变化,其值的true或false表示是否还有更多数据可以加载,根据其值来确定是否继续监听。
    • 返回一个对象,包含 hasMore 状态和更新该状态的方法setHasMore

当前页面:hooks/useIntersectionObserver.ts

代码如下:

// 组件可以复用
// 响应式的业务也可以复用
//loadMore hooks
//useRouter 是第三方提供的hooks函数
// 我们也可以自定义hooks函数
// 组件加hooks函数式编程,让项目开发更快更简单

import { watch, onUnmounted, ref } from "vue"
import type { Ref } from "vue"
const useIntersectionObserver = (
    nodeRef: Ref<HTMLElement | null>,
    loadMore: () => void
) => {
    // IntersectionObserver实例() 联合数据类型
    let observer: IntersectionObserver | null = null

    //是否还有下一页的标志
    const hasMore =  ref(true)

    watch(nodeRef,(newNodeRef, oldNodeRef) => {

        // 取消监听旧节点
        if(oldNodeRef && observer ){
            observer.unobserve(oldNodeRef)
        }

        // 监听新节点
        if(newNodeRef){
            // 创建IntersectionObserver实例
            observer = new IntersectionObserver(([entry]) => {
                //解构获取第一个元素
                // isIntersecting表示元素是否在视口内
                if(entry.isIntersecting){
                    // 触发加载更多的数据
                    loadMore()
                }
            })
            observer.observe(newNodeRef);
        }
    })

    //组件卸载,取消监听
    onUnmounted(()=>{
        if(observer){
            observer.disconnect();
        }
    })

    // 监听hasMore变化
    watch(hasMore, (value)=>{
        if(observer){
            if(value && nodeRef.value){
                observer.observe(nodeRef.value);
            }else{
                observer.disconnect();
            }
        }
    })

    return{
        // 暴露hasMore,便于外部组件控制
        hasMore,
        // 提供设置hasMore的方法
        setHasMore: (value: boolean)=>{
            hasMore.value = value;
        },
    }

}

export default useIntersectionObserver;
  1. 样式:

    • 使用 v-for 循环遍历文章列表,并为最后一个元素设置 itemRef
    • 当 hasMore 为 false 时,显示“没有数据了”
    • 设置 .item 的高度为 20% 的视口高度,将数据隔开好方便看效果。

总结

1.ts的泛型更好约束我们变量的类型,可以确保我们在操作数据时保持类型一致性。编译器会根据你提供的类型参数来推断或强制类型,从而减少运行时错误。

2.Pinia 与 Vue 3 的 Composition API 紧密集成,使得状态管理的逻辑可以自然地融入到你的组件中,不用建额外的数据库,如果利用了 Vue 的响应式系统,可以直接在组件中使用 store 的状态,而无需显式地订阅或监听变化,并且支持模块化的状态管理,使得大型应用的状态可以清晰地组织起来,每个模块都可以独立管理自己的状态。

3.Hooks 函数式组件式开发,可以将组件的逻辑分解成单独的 Hooks 函数,这些函数可以在多个组件之间重用,减少了代码冗余,并且每个 Hooks 函数专注于单一的责任,使得组件的逻辑更加清晰和易于理解。

综上所述,TypeScript 泛型、Pinia 和 Hooks 函数式组件式开发都是现代前端开发中的重要工具和技术,它们各自解决了不同层面的问题,并且相互结合可以显著提高开发效率和代码质量。


今天的懒加载就介绍到这里,感谢各位的阅读,希望对你有所帮助!可以帮忙点点赞吗,非常感谢!