当前位置: 首页 > news >正文

可复用的 Vue 3 + TypeScript + Element Plus 的列表请求(列表页/表格)通用方案

可复用的 Vue 3 + TypeScript + Element Plus 的列表请求(列表页/表格)通用方案**。
方案包含:

  • 一个泛型 useList 可复用组合式函数(composable),负责:请求、分页、筛选、排序、加载更多、重置、刷新、取消请求、去抖等;
  • 完整类型定义与注释(每行代码都有简短注释,便于阅读);
  • axios 示例适配器和 requestFn 使用说明(你可替换为 fetch / uni.request / $http);
  • 使用示例:Element Plus 的 el-table + el-pagination 集成示例组件;
  • 设计要点与权衡说明、扩展建议和注意事项。

说明:代码注释尽量每行写短注释以便理解。你可以把 useList 放到 src/composables/useList.ts,组件示例放到页面组件里。


一、核心设计要点(简短)

  • 通用与灵活useList<T> 接受一个 requestFn,返回统一格式数据与方法,支持任何后端返回格式通过 transformResponse 适配。
  • 分页与加载:支持传统分页(page/pageSize)、“加载更多”(append)两种模式。
  • 可取消请求:使用 AbortController(或 axios cancel token)避免并发请求污染数据。
  • 防抖:可选的防抖(搜索框场景)。
  • 类型安全:通过泛型保证返回数据类型。
  • 易集成:可直接用于 Element Plus 表格 + 分页。

二、可复用组合函数:useList.ts

// src/composables/useList.ts
import {ref,reactive,computed,watch,onUnmounted,onActivated,onDeactivated,getCurrentInstance,
} from 'vue'/*** @function debounceFn* @description 防抖函数,避免频繁调用请求(如快速输入搜索框)* @param fn 要执行的函数* @param wait 延迟时间(毫秒)* @returns 一个带防抖效果的函数*/
function debounceFn<T extends (...args: any[]) => any>(fn: T, wait = 300) {let timer: ReturnType<typeof setTimeout> | null = nullreturn (...args: Parameters<T>) => {if (timer) clearTimeout(timer)timer = setTimeout(() => fn(...args), wait)}
}/*** @function useList* @description 通用列表 Hook,集成分页、搜索、防抖、取消请求、keep-alive 等特性* @template T 列表项类型* @param requestFn 列表请求函数,需返回 Promise* @param options 可选配置项*/
export function useList<T = any>(requestFn: (params: Record<string, any>, controller?: AbortController) => Promise<any>,options?: {pageKey?: string // 请求页码字段名pageSizeKey?: string // 请求页大小字段名totalKey?: string // 响应总数字段名dataKey?: string // 响应数据字段名defaultPageSize?: number // 默认每页数量immediate?: boolean // 是否自动请求一次debounce?: number // 防抖延迟(毫秒)transformResponse?: (res: any) => { list: T[]; total: number } // 自定义响应格式转换append?: boolean // 是否开启“加载更多”模式(追加数据)keepAlive?: boolean // 是否在 keep-alive 中保留状态}
) {// ✅ 默认配置项(带参数覆盖)const cfg = {pageKey: 'page',pageSizeKey: 'pageSize',totalKey: 'total',dataKey: 'data',defaultPageSize: 10,immediate: true,debounce: 0,transformResponse: undefined,append: false,keepAlive: true,...(options || {}),}/** 列表数据 */const list = ref<T[]>([])/** 加载状态 */const loading = ref(false)/** 错误对象 */const error = ref<any>(null)/** 分页数据 */const page = ref<number>(1)const pageSize = ref<number>(cfg.defaultPageSize)const total = ref<number>(0)/** 查询参数 */const params = reactive<Record<string, any>>({})/** 当前请求控制器(用于取消请求) */let currentController: AbortController | null = null/** 响应式分页元信息 */const meta = computed(() => ({ page: page.value, pageSize: pageSize.value, total: total.value }))/** keep-alive 激活状态标识 */const isActive = ref(true)/*** @function buildParams* @description 构建完整请求参数(合并分页与查询)*/function buildParams(extra: Record<string, any> = {}) {return {...params,...extra,[cfg.pageKey]: page.value,[cfg.pageSizeKey]: pageSize.value,}}/*** @function doFetch* @description 核心请求函数(带取消控制、错误处理、数据转换)* @param extra 控制是否重置或追加数据*/async function doFetch(extra: { reset?: boolean; append?: boolean } = {}) {const doAppend = extra.append ?? cfg.append// 若有上次请求未结束,先取消if (currentController) {try {currentController.abort()} catch {}currentController = null}// 新建请求控制器const controller = new AbortController()currentController = controllerloading.value = trueerror.value = nulltry {// 发起请求const res = await requestFn(buildParams(), controller)// 若用户自定义转换,则使用let normalized: { list: T[]; total: number }if (cfg.transformResponse) {normalized = cfg.transformResponse(res)} else {const rawList = res?.[cfg.dataKey] ?? resconst rawTotal = res?.[cfg.totalKey] ?? (Array.isArray(rawList) ? rawList.length : 0)normalized = {list: Array.isArray(rawList) ? (rawList as T[]) : [],total: Number(rawTotal || 0),}}// 赋值数据(是否追加)list.value = doAppend ? [...list.value, ...normalized.list] : normalized.listtotal.value = normalized.totalreturn res} catch (err: any) {// 忽略手动取消的请求if (err?.name !== 'AbortError') error.value = errthrow err} finally {loading.value = falsecurrentController = null}}/** 包装请求函数:可选防抖 */const fetch =cfg.debounce && cfg.debounce > 0 ? debounceFn(doFetch, cfg.debounce) : (p?: any) => doFetch(p)/*** @function resetAndFetch* @description 重置分页与数据后重新请求*/async function resetAndFetch() {page.value = 1list.value = []await doFetch({ reset: true, append: false })}/*** @function loadMore* @description 加载更多(分页追加)*/async function loadMore() {if (loading.value) returnif (total.value && list.value.length >= total.value) returnpage.value++await doFetch({ append: true })}/*** @function refresh* @description 手动刷新列表*/async function refresh() {await doFetch({ reset: true, append: false })}/*** @function cancel* @description 取消当前请求*/function cancel() {if (currentController) {try {currentController.abort()} catch {}currentController = null}loading.value = false}/*** @function setFilters* @description 设置筛选条件(自动触发请求)*/function setFilters(newParams: Record<string, any>, fetchNow = true) {Object.assign(params, newParams)if (fetchNow) return resetAndFetch()}/*** @function setSort* @description 设置排序参数(自动触发请求)*/function setSort(sortObj: Record<string, any>, fetchNow = true) {Object.assign(params, sortObj)if (fetchNow) return resetAndFetch()}/** 🚀 监听分页变化自动刷新(仅激活状态) */watch([page, pageSize], () => {if (isActive.value) doFetch({ reset: !cfg.append }).catch(() => {})})/** ✅ keep-alive 生命周期逻辑 */const instance = getCurrentInstance()if (instance && cfg.keepAlive) {onActivated(() => {isActive.value = true// 激活时若列表为空则请求if (list.value.length === 0 && cfg.immediate) {fetch({ reset: true })}})onDeactivated(() => {// 失活时不清空状态,只标记为非激活isActive.value = false// 可选取消请求节省资源cancel()})}/** ✅ 页面彻底卸载才重置所有状态 */onUnmounted(() => {cancel()list.value = []total.value = 0page.value = 1Object.keys(params).forEach((k) => delete params[k])})/** ✅ 首次进入立即请求 */if (cfg.immediate) {Promise.resolve().then(() => {fetch({ reset: true }).catch(() => {})})}// 🔥 暴露公共 APIreturn {list, // 列表数据loading, // 加载状态error, // 错误对象page, // 当前页pageSize, // 每页条数total, // 总数params, // 请求参数meta, // 分页信息对象fetch, // 请求方法(可带防抖)fetchDebounced: (extra?: any) => (fetch as any)(extra), // 防抖封装版本reset: resetAndFetch, // 重置并请求loadMore, // 加载更多refresh, // 刷新列表cancel, // 取消请求setFilters, // 设置筛选条件setSort, // 设置排序条件isActive, // 是否处于激活状态}
}

三、如何适配后端(axios 示例)

后端接口常见返回格式各不相同,useList 通过 transformResponsedataKey/totalKey 配置来适配。下面给出 axiosrequestFn 示例(放在 src/api/listApi.ts):

// src/api/listApi.ts
import axios from 'axios' // axios 实例
// 示例请求函数,符合 useList 要求:(params, controller?) => Promise<any>
export async function axiosListRequest(params: Record<string, any>, controller?: AbortController) {// axios 支持 signal 参数用于取消请求(v0.22+)const response = await axios.get('/api/items', {params,signal: controller?.signal, // 将 signal 传给 axios})return response.data // 返回原始数据(由 useList.transformResponse 处理)
}

如何在 useList 中使用 transformResponse:

import { useList } from '@/composables/useList'
import { axiosListRequest } from '@/api/listApi'// 假设后端返回结构为 { code:0, data:{ items:[], total:123 } }
const { list, total, reset } = useList<ItemType>(axiosListRequest, {dataKey: 'data.items', // 也可以用 transformResponse 更灵活totalKey: 'data.total',immediate: true,debounce: 200,transformResponse: (res) => {// 支持深路径解析示例(简单实现)const items = res?.data?.items ?? []const tot = res?.data?.total ?? 0return { list: items, total: tot }},
})

注意:如果你传 dataKeydata.items(包含点路径),上面的 useList 默认解析不支持点路径;建议使用 transformResponse 做精确解析或扩展 get 工具来支持路径解析。


四、Element Plus 使用示例组件(template + script)

下面是一个完整的页面示例,演示如何把 useList 与 Element Plus 的 el-tableel-pagination、搜索表单结合起来。

<!-- src/views/ItemsList.vue -->
<template><div><!-- 搜索表单 --><el-form inline><el-form-item><el-input v-model="filters.keyword" placeholder="搜索关键字" @input="onSearchInput" /></el-form-item><el-form-item><el-button type="primary" @click="onSearch">搜索</el-button><el-button @click="onReset">重置</el-button></el-form-item></el-form><!-- 表格 --><el-table :data="list" v-loading="loading" style="width:100%"><el-table-column prop="id" label="ID" width="80" /><el-table-column prop="name" label="名称" /><el-table-column prop="price" label="价格" width="120" /></el-table><!-- 底部分页 --><div class="pager" style="margin-top: 12px; text-align: right;"><el-paginationbackgroundlayout="total, sizes, prev, pager, next, jumper":total="total":page-size="pageSize":current-page="page"@size-change="onSizeChange"@current-change="onPageChange"/></div><!-- 加载更多示例(非分页) --><!-- <el-button @click="loadMore" :loading="loading">加载更多</el-button> --></div>
</template><script setup lang="ts">
import { reactive, ref } from 'vue'
import { useList } from '@/composables/useList'
import { axiosListRequest } from '@/api/listApi'// 本地搜索/筛选条件,双向绑定表单
const filters = reactive({ keyword: '' })// 创建 useList 实例,传入 axios 请求函数与解析器
const {list,loading,total,page,pageSize,fetchDebounced,fetch,reset,setFilters,loadMore,
} = useList<any>(axiosListRequest, {debounce: 300, // 搜索防抖immediate: true, // 进入页面自动请求transformResponse: (res) => {// 假设后端:{ code:0, data:{items:[], total:123} }const items = res?.data?.items ?? []const tot = res?.data?.total ?? 0return { list: items, total: tot }},
})// 搜索输入防抖:直接触发 fetchDebounced(useList 已包装防抖)
function onSearchInput() {// 同步过滤条件到 useList params 并防抖触发setFilters({ keyword: filters.keyword }, false) // 先设置 params 不立即发请求// 使用包装好的防抖请求fetchDebounced({ reset: true })
}// 点击搜索按钮立即发请求
function onSearch() {setFilters({ keyword: filters.keyword }, false)reset()
}// 重置
function onReset() {filters.keyword = ''setFilters({ keyword: '' }, false)reset()
}// 分页 change
function onPageChange(newPage: number) {page.value = newPage // watch 会触发加载
}
function onSizeChange(newSize: number) {pageSize.value = newSize // watch 会触发加载
}
</script>

五、扩展建议与注意事项

  1. 后端接口差异:强烈建议统一后端接口({data:[], total}),否则在 useList 中实现 transformResponse 以兼容。
  2. AbortController / axios:浏览器环境使用 AbortController;对于旧版 axios(不支持 signal),请用 axios CancelToken 或升级 axios。
  3. 防抖策略:搜索框使用防抖,表格分页与排序尽量不使用防抖(或短防抖)。
  4. 缓存策略:可扩展为缓存每个查询(params->结果映射)以提高体验。
  5. 服务端分页 vs 前端分页:本方案以服务端分页为主;如果返回全部数据需做前端分页,需改做 slice 操作。
  6. 错误处理useList.error 提供错误信息,页面可展示错误提示并支持重试。
  7. 并发控制currentController 能减少并发请求冲突,但如果需要更复杂队列控制,可扩展 semaphore。
  8. SSR 注意:如果项目需要 SSR,请在 immediate 为 true 时注意不要在服务器端触发请求,或改为客户端渲染时触发。

六、为什么这是“最优解”/权衡说明(简短)

  • 灵活性:通过 requestFn + transformResponse 适配各种后端返回格式,适用于多种项目。
  • 可维护性:组合式函数集中处理列表逻辑,页面仅负责展示与交互,职责清晰。
  • 性能与用户体验:支持取消并发请求、防抖,避免用户快速输入或切页触发多次请求导致闪烁。
  • 可扩展:可加入缓存、预取、离线支持或 optimistic update(乐观更新)等高级功能。

✅ 总结说明

功能点说明
🧠 防抖防止频繁触发请求(debounce)
🚀 AbortController自动取消上次未完成请求
🔁 分页/加载更多内置分页逻辑与 loadMore
🔒 keep-alive页面缓存时保留状态
🧹 onUnmounted页面卸载时清理所有状态
🔧 自定义 transformResponse兼容不同后端数据格式
💬 全类型推导完整 TypeScript 支持
http://www.dtcms.com/a/609554.html

相关文章:

  • 安装 Composer
  • 国外做名片的网站网站没有备案
  • 解决VMware Workstation虚拟机中添加硬盘时找不到U盘对应的磁盘physicaldrive3
  • 解决 “Could not locate zlibwapi.dll” 错误全流程分析
  • 第一模板ppt免费下载seo人员工作内容
  • 【高级机器学习】 7. 带噪声数据的学习:从 MLE 到 MAP
  • 横沥镇做网站北京公司注册地址出租
  • 北湖区网站建设哪个好中网互联网站建设
  • @Autowired和@Resource的区别
  • MongoDB | python操作MongoDB的基础使用
  • 【C++进阶】异常
  • 《非暴力沟通》马歇尔•卢森堡博士(美)
  • Rust 从零到精通:构建一个专业级命令行工具 greprs
  • 大足网站建设网络营销市场调研的内容
  • CSS3 分页技术解析
  • HTMLElement 与MouseEvent 事件对象属性详解
  • 建设网站都要学些什么手续拍卖网站模板下载
  • 【火语言RPA实战案例】根据ISBN 编码批量查询孔夫子书籍信息,自动导出本地 Excel(附完整脚本)
  • 从零开始理解状态机:C语言与Verilog的双重视角
  • 做软件常用的网站有哪些软件微信怎么做网站推广
  • 设计模式面试题(14道含答案)
  • [智能体设计模式] 第9章 :学习与适应
  • 肇庆市建设局网站西双版纳建设厅网站
  • LingJing(灵境)桌面级靶场平台新增:真实入侵复刻,知攻善防实验室-Linux应急响应靶机2,通关挑战
  • 融合尺度感知注意力、多模态提示学习与融合适配器的RGBT跟踪
  • 基于脚手架微服务的视频点播系统-脚手架开发部分Fast-dfs,redis++,odb的简单使用与二次封装
  • 构建高可用Redis:哨兵模式深度解析与Nacos微服务适配实践
  • Linux -- 线程同步、POSIX信号量与生产者消费者模型
  • 微服务重要知识点
  • 东莞seo建站排名昆山有名的网站建设公司