在 Vue3 这样子写页面更快更高效
前言
在开发管理后台过程中,一定会遇到不少了增删改查页面,而这些页面的逻辑大多都是相同的,如获取列表数据,分页,筛选功能这些基本功能。而不同的是呈现出来的数据项。还有一些操作按钮。
对于刚开始只有 1,2 个页面的时候大多数开发者可能会直接将之前的页面代码再拷贝多一份出来,而随着项目的推进类似页面数量可能会越来越多,这直接导致项目代码耦合度越来越高。
这也是为什么在项目中一些可复用的函数或组件要抽离出来的主要原因之一
下面,我们封装一个通用的 useList,适配大多数增删改查的列表页面,让你更快更高效的完成任务,准点下班 ~
前置知识
封装
我们需要将一些通用的参数和函数抽离出来,封装成一个通用 hook,后续在其他页面复用相同功能更加简单方便。
定义列表页面必不可少的分页数据
1 2 3 4 5 6 7 8 9 10
| export default function useList() { const loading = ref(false); const curPage = ref(1); const total = ref(0); const pageSize = ref(10); }
|
如何获取列表数据
思考一番,让 useList 函数接收一个 listRequestFn 参数,用于请求列表中的数据。
定义一个 list 变量,用于存放网络请求回来的数据内容,由于在内部无法直接确定列表数据类型,通过泛型的方式让外部提供列表数据类型。
1 2 3 4 5 6
| export default function useList<ItemType extends Object>( listRequestFn: Function ) { const list = ref<ItemType[]>([]); }
|
在 useList 中创建一个 loadData 函数,用于调用获取数据函数,该函数接收一个参数用于获取指定页数的数据(可选,默认为 curPage 的值)。
- 执行流程
- 设置加载状态
- 调用外部传入的函数,将获取到的数据赋值到 list 和 total 中
- 关闭加载态
这里使用了 async/await 语法,假设请求出错、解构出错情况会走 catch 代码块,再关闭加载态。
这里需要注意,传入的 listRequestFn 函数接收的参数数量和类型是否正常对应上,请根据实际情况进行调整
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| export default function useList<ItemType extends Object>( listRequestFn: Function ) { const list = ref<ItemType[]>([]); const loadData = async (page = curPage.value) => { loading.value = true; try { const { data, meta: { total: count }, } = await listRequestFn(pageSize.value, page); list.value = data; total.value = count; } catch (error) { console.log("请求出错了", "error"); } finally { loading.value = false; } }; }
|
别忘了,还有切换分页要处理
使用 watch 函数监听数据,当 curPage,pageSize 的值发生改变时调用 loadData 函数获取新的数据。
1 2 3 4 5 6 7 8 9
| export default function useList<ItemType extends Object>( listRequestFn: Function ) { watch([curPage, pageSize], () => { loadData(curPage.value); }); }
|
现在实现了基本的列表数据获取
实现数据筛选器
在庞大的数据列表中,数据筛选是必不可少的功能
通常,我会将筛选条件字段定义在一个 ref 中,在请求时将 ref 丢到请求函数即可。
在 useList 函数中,第二个参数接收一个 filterOption 对象,对应列表中的筛选条件字段。
调整一下 loadData 函数,在请求函数中传入 filterOption 对象即可
注意,传入的 listRequestFn 函数接收的参数数量和类型是否正常对应上,请根据实际情况进行调整
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| export default function useList< ItemType extends Object, FilterOption extends Object >(listRequestFn: Function, filterOption: Ref<Object>) { const loadData = async (page = curPage.value) => { loading.value = true; try { const { data, meta: { total: count }, } = await listRequestFn(pageSize.value, page, filterOption.value); list.value = data; total.value = count; } catch (error) { console.log("请求出错了", "error"); } finally { loading.value = false; } }; }
|
注意,这里 filterOption 参数类型需要的是 ref 类型,否则会丢失响应式 无法正常工作
清空筛选器字段
在页面中,有一个重置的按钮,用于清空筛选条件。这个重复的动作可以交给 reset 函数处理。
通过使用 Reflect 将所有值设定为 undefined,再重新请求一次数据。
什么是 Reflect?看看这一篇文章 Reflect 映射对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| export default function useList< ItemType extends Object, FilterOption extends Object >(listRequestFn: Function, filterOption: Ref<Object>) { const reset = () => { if (!filterOption.value) return; const keys = Reflect.ownKeys(filterOption.value); filterOption.value = {} as FilterOption; keys.forEach((key) => { Reflect.set(filterOption.value!, key, undefined); }); loadData(); }; }
|
导出功能
除了对数据的查看,有些界面还需要有导出数据功能(例如导出 csv,excel 文件),我们也把导出功能写到 useList 里
通常,导出功能是调用后端提供的导出 Api 获取一个文件下载地址,和 loadData 函数类似,从外部获取 exportRequestFn 函数来调用 Api
在函数中,新增一个 exportFile 函数调用它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| export default function useList< ItemType extends Object, FilterOption extends Object >( listRequestFn: Function, filterOption: Ref<Object>, exportRequestFn?: Function ) { const exportFile = async () => { if (!exportRequestFn) { throw new Error("当前没有提供 exportRequestFn 函数"); } if (typeof exportRequestFn !== "function") { throw new Error("exportRequestFn 必须是一个函数"); } try { const { data: { link }, } = await exportRequestFn(filterOption.value); window.open(link); } catch (error) { console.log("导出失败", "error"); } }; }
|
注意,传入的 exportRequestFn 函数接收的参数数量和类型是否正常对应上,请根据实际情况进行调整
优化
现在,整个 useList 已经满足了页面上的需求了,拥有了获取数据,筛选数据,导出数据,分页功能
还有一些细节方面,在上面所有代码中的 try..catch 中的 catch 代码片段并没有做任何的处理,只是简单的 console.log 一下
提供钩子
在 useList 新增一个 Options 对象参数,用于函数成功、失败时执行指定钩子函数与输出消息内容。
定义 Options 类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| export interface MessageType { GET_DATA_IF_FAILED?: string; GET_DATA_IF_SUCCEED?: string; EXPORT_DATA_IF_FAILED?: string; EXPORT_DATA_IF_SUCCEED?: string; } export interface OptionsType { requestError?: () => void; requestSuccess?: () => void; message: MessageType; }
export default function useList< ItemType extends Object, FilterOption extends Object >( listRequestFn: Function, filterOption: Ref<Object>, exportRequestFn?: Function, options? :OptionsType ) { }
|
设置 Options 默认值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const DEFAULT_MESSAGE = { GET_DATA_IF_FAILED: "获取列表数据失败", EXPORT_DATA_IF_FAILED: "导出数据失败", };
const DEFAULT_OPTIONS: OptionsType = { message: DEFAULT_MESSAGE, };
export default function useList< ItemType extends Object, FilterOption extends Object >( listRequestFn: Function, filterOption: Ref<Object>, exportRequestFn?: Function, options = DEFAULT_OPTIONS ) { }
|
在没有传递钩子的情况下,推荐设置默认的失败时信息显示
优化 loadData,exportFile 函数
基于 elementui 封装 message 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import { ElMessage, MessageOptions } from "element-plus";
export function message(message: string, option?: MessageOptions) { ElMessage({ message, ...option }); } export function warningMessage(message: string, option?: MessageOptions) { ElMessage({ message, ...option, type: "warning" }); } export function errorMessage(message: string, option?: MessageOptions) { ElMessage({ message, ...option, type: "error" }); } export function infoMessage(message: string, option?: MessageOptions) { ElMessage({ message, ...option, type: "info" }); }
|
loadData 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149
| const loadData = async (page = curPage.value) => { loading.value = true; try { const { data, meta: { total: count }, } = await listRequestFn(pageSize.value, page, filterOption.value); list.value = data; total.value = count; options?.message?.GET_DATA_IF_SUCCEED && message(options.message.GET_DATA_IF_SUCCEED); options?.requestSuccess?.(); } catch (error) { options?.message?.GET_DATA_IF_FAILED && errorMessage(options.message.GET_DATA_IF_FAILED); options?.requestError?.(); } finally { loading.value = false; } }; ```
exportFile 函数
```typescript const exportFile = async () => { if (!exportRequestFn) { throw new Error("当前没有提供 exportRequestFn 函数"); } if (typeof exportRequestFn !== "function") { throw new Error("exportRequestFn 必须是一个函数"); } try { const { data: { link }, } = await exportRequestFn(filterOption.value); window.open(link); options?.message?.EXPORT_DATA_IF_SUCCEED && message(options.message.EXPORT_DATA_IF_SUCCEED); options?.exportSuccess?.(); } catch (error) { options?.message?.EXPORT_DATA_IF_FAILED && errorMessage(options.message.EXPORT_DATA_IF_FAILED); options?.exportError?.(); } }; ```
### useList 使用方法
```vue <template> <el-collapse class="mb-6"> <el-collapse-item title="筛选条件" name="1"> <el-form label-position="left" label-width="90px" :model="filterOption"> <el-row :gutter="20"> <el-col :xs="24" :sm="12" :md="8" :lg="8" :xl="8"> <el-form-item label="用户名"> <el-input v-model="filterOption.name" placeholder="筛选指定签名名称" /> </el-form-item> </el-col> <el-col :xs="24" :sm="12" :md="8" :lg="8" :xl="8"> <el-form-item label="注册时间"> <el-date-picker v-model="filterOption.timeRange" type="daterange" unlink-panels range-separator="到" start-placeholder="开始时间" end-placeholder="结束时间" format="YYYY-MM-DD HH:mm" value-format="YYYY-MM-DD HH:mm" /> </el-form-item> </el-col> <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24"> <el-row class="flex mt-4"> <el-button type="primary" @click="filter">筛选</el-button> <el-button type="primary" @click="reset">重置</el-button> </el-row> </el-col> </el-row> </el-form> </el-collapse-item> </el-collapse> <el-table v-loading="loading" :data="list" border style="width: 100%"> <el-table-column label="用户名" min-width="110px"> <template #default="scope"> {{ scope.row.name }} </template> </el-table-column> <el-table-column label="手机号码" min-width="130px"> <template #default="scope"> {{ scope.row.mobile || "未绑定手机号码" }} </template> </el-table-column> <el-table-column label="邮箱地址" min-width="130px"> <template #default="scope"> {{ scope.row.email || "未绑定邮箱地址" }} </template> </el-table-column> <el-table-column prop="createAt" label="注册时间" min-width="220px" /> <el-table-column width="200px" fixed="right" label="操作"> <template #default="scope"> <el-button type="primary" link @click="detail(scope.row)" >详情</el-button > </template> </el-table-column> </el-table> <div v-if="total > 0" class="flex justify-end mt-4"> <el-pagination v-model:current-page="curPage" v-model:page-size="pageSize" background layout="sizes, prev, pager, next" :total="total" :page-sizes="[10, 30, 50]" /> </div> </template> <script setup lang="ts"> import { UserInfoApi } from "@/network/api/User"; import useList from "@/lib/hooks/useList/index"; const filterOption = ref<UserInfoApi.FilterOptionType>({}); const { list, loading, reset, filter, curPage, pageSize, reload, total, loadData, } = useList<UserInfoApi.UserInfo[], UserInfoApi.FilterOptionType>( UserInfoApi.list, filterOption ); </script>
|