大列表
Vue3+ElementPlus大数据列表实现(虚拟滚动/分页/懒加载)
#tech / dev / frame
#type / howto
#status / growing
Vue3 + Element Plus 大数据列表实现指南
概述
在处理大量数据时,前端需要采用合适的策略来优化用户体验和性能。本指南介绍三种主要的数据展示策略:
- 传统翻页 (Pagination) - 适合结构化数据浏览
- 虚拟滚动 (Virtual Scrolling) - 适合大量数据快速浏览
- 懒加载 (Lazy Loading) - 适合无限滚动场景
1. 传统翻页实现
1.1 后端接口设计
// API 请求参数
interface PostListReqVO {
compId: string | number
pageNo: number // 页码,从1开始
pageSize: number // 每页条数,建议10-50
name?: string // 可选搜索条件
code?: string
// ... 其他搜索条件
}
// API 响应数据
interface ApiResponse<T> {
code: number
data: {
list: T[] // 当前页数据
total: number // 总记录数
}
msg: string
}
1.2 前端组件实现
<script setup lang="ts">
// 1. 数据状态定义
const loading = ref(false)
const list = ref<PostRespVO[]>([])
const total = ref(0)
// 2. 查询参数(响应式)
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
compId: undefined,
name: undefined,
// ... 其他参数
})
// 3. 获取列表数据
const getList = async () => {
loading.value = true
try {
const data = await JobDescApi.getPostList(queryParams)
list.value = data.list || []
total.value = data.total || 0
} finally {
loading.value = false
}
}
// 4. 搜索功能
const handleQuery = () => {
queryParams.pageNo = 1 // 重置到第一页
getList()
}
// 5. 重置功能
const resetQuery = () => {
queryParams.pageNo = 1
queryParams.name = undefined
// 重置其他查询条件
getList()
}
</script>
<template>
<!-- 搜索表单 -->
<ContentWrap>
<el-form :model="queryParams" :inline="true">
<el-form-item label="岗位名称" prop="name">
<el-input
v-model="queryParams.name"
placeholder="请输入岗位名称"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item>
<el-button @click="handleQuery">搜索</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</ContentWrap>
<!-- 数据表格 -->
<ContentWrap>
<el-table v-loading="loading" :data="list">
<el-table-column label="岗位名称" prop="name" />
<el-table-column label="岗位编码" prop="code" />
<el-table-column label="操作" width="120">
<template #default="scope">
<el-button @click="handleEdit(scope.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页组件 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</ContentWrap>
</template>
1.3 关键实现点
- 响应式查询参数: 使用
reactive包装查询条件 - 页码重置: 搜索时重置
pageNo = 1 - 加载状态: 使用
loading提供用户反馈 - 分页组件: 使用双向绑定
v-model:page和v-model:limit
2. 虚拟滚动实现
2.1 适用场景
- 数据量巨大 (>1000条)
- 不需要复杂搜索和筛选
- 用户需要快速浏览数据
2.2 核心原理
// 虚拟滚动核心逻辑
interface VirtualScrollConfig {
itemHeight: number // 每行高度
containerHeight: number // 容器高度
buffer: number // 缓冲区行数
}
const useVirtualScroll = (data: Ref<any[]>, config: VirtualScrollConfig) => {
const scrollTop = ref(0)
// 计算可见范围
const visibleRange = computed(() => {
const { itemHeight, containerHeight, buffer } = config
const visibleCount = Math.ceil(containerHeight / itemHeight)
const start = Math.max(0, Math.floor(scrollTop.value / itemHeight) - buffer)
const end = Math.min(data.value.length, start + visibleCount + buffer * 2)
return { start, end }
})
// 可见数据
const visibleData = computed(() => {
const { start, end } = visibleRange.value
return data.value.slice(start, end)
})
// 滚动处理
const handleScroll = (event: Event) => {
scrollTop.value = (event.target as HTMLElement).scrollTop
}
return {
visibleData,
visibleRange,
handleScroll,
totalHeight: computed(() => data.value.length * config.itemHeight)
}
}
2.3 实现示例
<script setup lang="ts">
const allData = ref<PostRespVO[]>([])
const { visibleData, handleScroll, totalHeight } = useVirtualScroll(
allData,
{
itemHeight: 50,
containerHeight: 400,
buffer: 5
}
)
</script>
<template>
<div class="virtual-list" @scroll="handleScroll">
<div :style="{ height: totalHeight + 'px' }" class="virtual-content">
<div
v-for="item in visibleData"
:key="item.id"
class="virtual-item"
>
{{ item.name }}
</div>
</div>
</div>
</template>
<style>
.virtual-list {
height: 400px;
overflow-y: auto;
}
.virtual-item {
height: 50px;
border-bottom: 1px solid #eee;
}
</style>
3. 懒加载 (无限滚动) 实现
3.1 适用场景
- 类似社交媒体的信息流
- 数据按时间或相关性排序
- 用户倾向于连续浏览
3.2 核心实现
// 懒加载 Hook
const useInfiniteScroll = <T>(
fetchFn: (page: number, pageSize: number) => Promise<{ list: T[], hasMore: boolean }>
) => {
const list = ref<T[]>([])
const loading = ref(false)
const hasMore = ref(true)
const page = ref(1)
const pageSize = 20
// 加载更多数据
const loadMore = async () => {
if (loading.value || !hasMore.value) return
loading.value = true
try {
const data = await fetchFn(page.value, pageSize)
if (page.value === 1) {
list.value = data.list
} else {
list.value.push(...data.list)
}
hasMore.value = data.hasMore
page.value++
} finally {
loading.value = false
}
}
// 重置数据
const refresh = async () => {
page.value = 1
hasMore.value = true
await loadMore()
}
// 滚动到底部检测
const handleScroll = (event: Event) => {
const target = event.target as HTMLElement
const { scrollTop, scrollHeight, clientHeight } = target
// 距离底部50px时开始加载
if (scrollTop + clientHeight >= scrollHeight - 50) {
loadMore()
}
}
return {
list,
loading,
hasMore,
loadMore,
refresh,
handleScroll
}
}
3.3 使用示例
<script setup lang="ts">
const fetchPosts = async (page: number, pageSize: number) => {
const response = await api.getPosts({ page, pageSize })
return {
list: response.data.list,
hasMore: response.data.list.length === pageSize
}
}
const { list, loading, hasMore, refresh, handleScroll } = useInfiniteScroll(fetchPosts)
onMounted(() => {
refresh()
})
</script>
<template>
<div class="infinite-scroll" @scroll="handleScroll">
<div v-for="item in list" :key="item.id" class="list-item">
{{ item.title }}
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading">
<el-loading />
</div>
<!-- 没有更多数据 -->
<div v-else-if="!hasMore" class="no-more">
没有更多数据了
</div>
</div>
</template>
4. 性能优化建议
4.1 数据处理优化
// 1. 使用 Object.freeze 冻结只读数据
const processListData = (rawData: any[]) => {
return rawData.map(item => Object.freeze({
...item,
// 预处理计算属性
displayName: `${item.name} (${item.code})`
}))
}
// 2. 防抖搜索
const debouncedSearch = debounce(() => {
handleQuery()
}, 300)
// 3. 缓存搜索结果
const searchCache = new Map<string, any>()
const getCachedData = (key: string, fetchFn: () => Promise<any>) => {
if (searchCache.has(key)) {
return Promise.resolve(searchCache.get(key))
}
return fetchFn().then(data => {
searchCache.set(key, data)
return data
})
}
4.2 用户体验优化
<template>
<!-- 1. 骨架屏 -->
<el-skeleton v-if="loading && !list.length" :rows="10" animated />
<!-- 2. 空状态 -->
<el-empty v-else-if="!loading && !list.length" description="暂无数据" />
<!-- 3. 错误状态 -->
<el-result
v-else-if="error"
icon="error"
title="加载失败"
:sub-title="error.message"
>
<template #extra>
<el-button type="primary" @click="retry">重试</el-button>
</template>
</el-result>
</template>
5. 最佳实践总结
5.1 选择合适的策略
| 场景 | 数据量 | 用户行为 | 推荐方案 |
|---|---|---|---|
| 管理后台 | <10k | 查询浏览 | 传统翻页 |
| 数据分析 | >10k | 快速浏览 | 虚拟滚动 |
| 内容平台 | 不限 | 连续浏览 | 懒加载 |
5.2 关键技术要点
- API设计: 统一的分页参数和响应格式
- 状态管理: 合理使用响应式数据
- 用户反馈: 加载状态、错误处理、空状态
- 性能优化: 防抖、缓存、预处理
- 用户体验: 骨架屏、平滑过渡、快捷操作
5.3 注意事项
- 合理设置
pageSize,通常 10-50 条 - 搜索时要重置页码
- 处理网络异常和错误状态
- 考虑 SEO 和路由状态同步
- 移动端适配和触摸优化
通过以上实现方案,可以有效处理各种规模的数据展示需求,提供良好的用户体验。