前言
vue 3 的出现,引入了一系列的新特性和改进,其中最引人注目的就是 Composition API 啦,Vue 3 不仅带来了性能上的提升,还通过 Composition API 引入了类似 React Hooks 的功能,极大地改善了状态管理 和组件复用 的方式。而 TypeScript 作为一种静态 类型检查的语言,能够帮助开发者在编写代码时发现潜在的错误,提高代码质量的同时也增强了团队协作的能力。Composition API 是一种全新的编程模型,它允许开发者以函数的形式组织和复用组件逻辑。下面通过Composition API 中"Hooks"来实现landmore(懒加载),和我一起往下看看吧~
正文
准备工作
创建项目
- 打开终端或者cmd输入
npm init vite
- 输入自己的项目名称(
v3-ts-hooks-landmore
) - 选择
vue
- 选择
typescript
- 打开创建的文件夹
安装依赖
npm i
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
)并将其抛出:
useArticleStore
是一个函数,返回一个store对象article
是store的唯一标识符,在其他地方可以通过这个ID来访问store
javascript
export const useArticleStore = defineStore("article", () => {})
下面来介绍这个useArticleStore
函数:
- 定义一个私有变量
_articles
(私有变量就是只能在它的作用域内访问和修改它),该变量是一个数组,里面包含多个文章对象
ini
const _articles = [
// ... 多个文章对象
];
- 定义一个响应式数据
articles
,它初始为一个空数组,它可被组件和其他的store使用,<article[]>
这个为泛型,用来对类型进行约束,提供了类型的安全性
css
const articles = ref<article[]>([]);
- 定义一个获取文章列表的方法
getArticles
,该方法是一个异步方法:
- 接收两个参数
page
(页码)和size
(每页的数量) - 使用
Promise
来模拟数据的异步加载 - 对resolve里的数据类型进行约束
ini
<{ // resolve里数据的类型
2 data: article[];
3 page: number;
4 total: number;
5 hasMore: boolean;
6 }>
下面介绍异步加载数据的逻辑:
- Promise的
resolve
函数返回一个对象,对象中包含当页的数据、页码、总条数和是否还有更多的文章数据信息 - 使用
setTimeout
来模拟网络的延迟 - 使用
slice
方法来分页获取_articles数组中的文章数据 - 将获取到的文章数据追加到
articles
数组中 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
,定义两个形参
nodeRef
:是一个Ref,它指向页面上的一个DOM元素或nulllandmore
:一个函数,当目标元素进入视口后会被调用,用于加载更多的数据
- 创建
IntersectionObserver
实例:observer
为一个IntersectionObserver实例,初始为null - 通过
watch
来观察nodeRef
的变化
- 当
nodeRef
的值发生变化时,会执行回调函数 - 如果
oldNodeRef
存在且observer
已经创建,那么取消对旧节点的监听 - 如果
newNodeRef
存在,则创建一个新的IntersectionObserver
实例,开始监听新节点
- 创建
IntersectionObserver
实例,传入一个回调函数,当被观察的元素进入视口时,这个回调函数会被调用,通过isIntersecting
属性来判断元素是否进入了视口 - 组件卸载时取消监听,当组件挂载时,如果
observer
存在,则取消所有监听,防止内存泄漏 - 监听
hasMore
的变化,hasMore为一个响应式数据,初始值为true
,当hasMore发生变化时,根据新值来决定是否继续监听nodeRef
- 返回一个对象:
hasMore
setHasMore
方法:访问和更新hasMore的值
App.vue
这里介绍一下ts的部分主要实现的功能有:
- 初始化文章数据
- 滚动加载更多
- 导入库和自定义的
hook
- 初始化store和状态:
- 初始化store实例
articleStore
,它包含文章数据和相关的方法 onMounted
在组件挂载后调用,用于异步加载第一页的文章数据- 定义一个变量
articles
,为ref
类型(toRefs
是 Vue 3 中 Composition API 提供的一个实用函数,它用于将一个响应式对象(通常是 store 中的状态)转换为一个包含多个ref
的对象),包含文章数据
- 定义DOM引用和状态
itemRef
为一个DOM元素,用于IntersectionObserver
的监听hasMore
来表示是否还有更多的文章数据可以加载currentPage
为一个数字类型的ref类型,表示当前页数
- 定义加载下一页��函数
handleNextPage
:
- 为一个异步函数,用于加载下一页的数据
- 接收一个
setHasMore
函数作为参数,来更新hasMore
的值 currentPage
递增,表示加载下一页- 调用
articleStore.getArticles
方法来获取下一页的文章数据 - 如果没有更多的文章数据,也就是hasMore为false,通过
setHasMore
来更新hasMore的状态,来表示没有更多数据来加载
- 使用自定义的
IntersectionObserver Hook
useIntersectionObserver
是一个自定义的 Hook,它接收itemRef
和一个回调函数作为参数- 回调函数会在目标元素进入视口时被调用,这里调用了
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>