前言
今天给大家介绍的是如何用ts
+hooks
+pinia
手写一个懒加载
,让其可以成为我们项目之中的一大亮点,那么什么是懒加载
呢,所谓的懒加载
就是一种优化技术,它允许开发者推迟加载某些资源,直到这些资源被实际需要时才加载。这种方法可以显著提高网页和应用的加载速度和性能,因为它减少了初始加载时必须下载的数据量。我们可以通过下面的视频更好的理解。
下面我们通过代码来实现这个功能。
正文
准备工作
首先先做好准备工作,我们先通过在终端选择创建一个vue
项目并选择我们后面要用的TypeScript
,然后在用npm i pinia
装一下pinia库
,操作如下:
开始动手
我们现在进入正题,首先我们在src
目录下创建我们需要用的文件,分别是hooks
文件夹里的useIntersectionObserver.ts
、store
文件夹里的article.ts
和types
文件夹里的article.ts
。如下图:
当前页面:type/article.ts
然后我们来到type
文件夹的article.ts
文件,对我们要在创建的仓库中的Article
类型的对象进行泛型约束,任何符合 Article
类型的对象必须具有 id
和 title
这两个属性,并且 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
}
}
)
-
导入必要的依赖:
- 从
pinia
导入defineStore
,用于创建Pinia store
。 - 从
vue
导入ref
,用于创建响应式引用。 - 从
../types/article
导入Article
类型,用于定义文章对象的结构。
- 从
-
定义 Store:
- 使用
defineStore
创建一个名为'article'
的store
,这个store
里有我们的数据和相应方法。
- 使用
-
定义变量:
_articles
:定义的一个私有数据,只在这个store
里使用,不拿到外面去。每个文章对象都有id
和title
属性。articles
:定义的一个响应式变量,用Article
类型约束着,初始为空数组,在后面用来承接_articles
的数据。
-
定义方法:
getArticles
:定义的一个异步函数,用于获取分页的文章数据。它接受两个参数:page
和size
,分别表示要获取的页码和每页的文章数量。这个 函数返回一个Promise
,其中包含文章数据、当前页码、总数以及是否还有更多数据的信息。
-
方法功能:
- 使用
Promise
和setTimeout
模拟异步操作和网络延迟。 - 使用
slice
方法从_articles
数组中截取指定范围的数据。 - 将截取的数据追加到
articles
变量中,使其变为响应式状态的一部分。 resolve
返回一个对象,包含了当前页面的数据、页码、总数以及是否还有更多数据的信息。
- 使用
-
返回 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>
-
声明变量及初始化:
articleStore
: 从useArticleStore
获取文章列表的 store。articles
: 从 store 中提取文章列表,在滚动页面前先加载第一面数据。itemRef
: 用于跟踪页面底部元素的引用。hasMore
: 布尔值类型,表示是否还有更多文章可以加载,开始为ture,当没有时变为false。currentPage
: 当前加载的页数。
-
加载文章:
- 在组件挂载完成后,通过
onMounted
钩子异步加载第一页的文章。
- 在组件挂载完成后,通过
-
处理加载下一页:
- 定义
handleNextPage
函数,增加currentPage
的值,并检查返回的数据是否有更多的文章可以加载。 - 如果没有更多文章,及
store
里的hasmMore
变为false
时,则通过setHasMore
函数更新hasMore
为false
。
- 定义
-
使用
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;
-
样式:
- 使用
v-for
循环遍历文章列表,并为最后一个元素设置itemRef
。 - 当
hasMore
为false
时,显示“没有数据了” - 设置
.item
的高度为 20% 的视口高度,将数据隔开好方便看效果。
- 使用
总结
1.ts
的泛型更好约束我们变量的类型,可以确保我们在操作数据时保持类型一致性。编译器会根据你提供的类型参数来推断或强制类型,从而减少运行时错误。
2.Pinia
与 Vue 3 的 Composition API 紧密集成,使得状态管理的逻辑可以自然地融入到你的组件中,不用建额外的数据库,如果利用了 Vue 的响应式系统,可以直接在组件中使用 store 的状态,而无需显式地订阅或监听变化,并且支持模块化的状态管理,使得大型应用的状态可以清晰地组织起来,每个模块都可以独立管理自己的状态。
3.Hooks
函数式组件式开发,可以将组件的逻辑分解成单独的 Hooks 函数,这些函数可以在多个组件之间重用,减少了代码冗余,并且每个 Hooks 函数专注于单一的责任,使得组件的逻辑更加清晰和易于理解。
综上所述
,TypeScript 泛型、Pinia 和 Hooks 函数式组件式开发都是现代前端开发中的重要工具和技术,它们各自解决了不同层面的问题,并且相互结合可以显著提高开发效率和代码质量。
今天的懒加载就介绍到这里,感谢各位的阅读,希望对你有所帮助!可以帮忙点点赞吗,非常感谢!