Redian新闻
>
在 Vue3 这样子写页面更快更高效

在 Vue3 这样子写页面更快更高效

公众号新闻

推荐关注↓

作者:_island

https://juejin.cn/post/7172889961446768670

前言

在开发管理后台过程中,一定会遇到不少了增删改查页面,而这些页面的逻辑大多都是相同的,如获取列表数据,分页,筛选功能这些基本功能。而不同的是呈现出来的数据项。还有一些操作按钮。


对于刚开始只有 1,2 个页面的时候大多数开发者可能会直接将之前的页面代码再拷贝多一份出来,而随着项目的推进类似页面数量可能会越来越多,这直接导致项目代码耦合度越来越高。

这也是为什么在项目中一些可复用的函数或组件要抽离出来的主要原因之一

下面,我们封装一个通用的useList,适配大多数增删改查的列表页面,让你更快更高效的完成任务,准点下班 ~

前置知识

  • Vue
  • Vue Composition Api[1]

封装

我们需要将一些通用的参数和函数抽离出来,封装成一个通用hook,后续在其他页面复用相同功能更加简单方便。

定义列表页面必不可少的分页数据

export default function useList({
  // 加载态
  const loading = ref(false);
  // 当前页
  const curPage = ref(1);
  // 总数量
  const total = ref(0);
  // 分页大小
  const pageSize = ref(10);
}

如何获取列表数据

思考一番,让useList函数接收一个listRequestFn参数,用于请求列表中的数据。

定义一个list变量,用于存放网络请求回来的数据内容,由于在内部无法直接确定列表数据类型,通过泛型的方式让外部提供列表数据类型。

export default function useList<ItemType extends Object>(
  listRequestFn: Function
{
  // 忽略其他代码
  const list = ref<ItemType[]>([]);
}

useList中创建一个loadData函数,用于调用获取数据函数,该函数接收一个参数用于获取指定页数的数据(可选,默认为curPage的值)。

  • 执行流程
  1. 设置加载状态
  2. 调用外部传入的函数,将获取到的数据赋值到listtotal
  3. 关闭加载态

这里使用了 async/await 语法,假设请求出错、解构出错情况会走 catch 代码块,再关闭加载态

这里需要注意,传入的 listRequestFn 函数接收的参数数量和类型是否正常对应上 请根据实际情况进行调整

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 函数监听数据,当curPagepageSize的值发生改变时调用loadData函数获取新的数据。

export default function useList<ItemType extends Object>(
  listRequestFn: Function
{
  // 忽略其他代码
  // 监听分页数据改变
  watch([curPage, pageSize], () => {
    loadData(curPage.value);
  });
}

现在实现了基本的列表数据获取

实现数据筛选器

在庞大的数据列表中,数据筛选是必不可少的功能

通常,我会将筛选条件字段定义在一个ref中,在请求时将ref丢到请求函数即可。

在 useList 函数中,第二个参数接收一个filterOption对象,对应列表中的筛选条件字段。

调整一下loadData函数,在请求函数中传入filterOption对象即可

注意,传入的 listRequestFn 函数接收的参数数量和类型是否正常对应上 请根据实际情况进行调整

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,再重新请求一次数据。

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函数调用它。

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 类型

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默认值

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
{
  // ...
}

在没有传递钩子的情况霞,推荐设置默认的失败时信息显示

优化loadDataexportFile函数

基于 elementui 封装 message 方法

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 函数

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 函数

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 使用方法

<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>

本文useList的完整代码在 github.com/QC2168/snip…[2]

💡 如果您对该hook有更好的建议,欢迎pr或者在评论区留言哦

另外,为了在日常开发中节省找封装代码片段的时间和提高工作效率(摸 🐟 时间++),该仓库[3]还存放一些第三方封装的代码片段 ✨,方便大家拿取 😄😄

参考资料

[1]

Vue Composition Api: https://cn.vuejs.org/guide/introduction.html#api-styles

[2]

github.com/QC2168/snip…: https://github.com/QC2168/snippets/tree/main/useList

[3]

仓库: https://github.com/QC2168/snippets


- EOF -



推荐阅读  点击标题可跳转

0、极客专属:几十款程序员秒懂的卫衣

1、往代码里掺东西,你还敢用?再见,Notepad++

2、世界上最伟大最邪恶的软件发明

3、ChatGPT 连夜迭代:你老婆不好使了


关注「程序员的那些事」加星标,不错过圈内事

点赞和在看就是最大的支持❤️

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
重磅利好!驻美使馆宣布签证新规:回国更快更方便!驻加领馆也官宣好消息!SpringBoot+Vue 实现网页版人脸登录、人脸识别,逼格很高!!!华州独栋房屋社区恐终结!Bellevue办华人社区专场综合规划信息会,对话市长、议员、规划师,机会难得!微软裁员5%,3月20日Puget Sound地区裁878人;不续租Bellevue市中心大楼,专卖店改NikeNike搬离西雅图,即将在Bellevue Square 开设东区第一家店!就在Microsoft Store原址在最美风景的阿尔卑斯高山公路自驾小灾小祸避其后之大难8 个很棒的 Vue 开发技巧Bellevue2044 综合规划华裔社区专场,工作、住房、交通、环境,还有哪些你最关心的话题?OpenAI新生成模型开源炸场!比Diffusion更快更强,清华校友宋飏一作Bellevue学区计划合并3所小学!7场公开听证会本周举行CVPR 2023 | 何恺明团队开源FLIP:MAE助力CLIP更快更高精度!针对 jar 和 vue 的一键自动化部署工具,界面友好,操作简单(已开源)加州罕见暴风雪肆虐,Bellevue学生团体滞留机场打地铺,男子1小时遭遇4次车祸,几百名学生被困风雪多日让大模型的训练和推理,比更快还更快!谷歌2022年终总结第四弹美国买房避雷系列(一)(西雅图Bellevue的Somerset小学附近)无缝切换?从Vue到React尤雨溪Vue 2022回顾及2023展望让部署更快更安全,GitHub 无密码部署现已上线《丑陋的中国人》帽子必须摘掉你们可以不服老,但是我要服老!【要闻】加快建设更高水平、更高质量的法治政府!市法治政府建设工作领导小组会议今天召开婚后第一次出轨生源不足 Bellevue提议将合并部分小学为什么选这里?Bellevue将建无家可归者安全停车试点,预计第三季度启动,年投45万诈骗警报!仅今年一月Bellevue就报告五起案件,被骗金额超过460万美金今天 Bellevue Square 庆祝农历新年,赏舞龙舞狮、武术歌舞乐器表演,看工艺品还有新年红包Bellevue 124th Avenue NE施工警报,禁止双向停车,施工影响和绕行路线都在这Gucci罕见全品牌9折!VUE电影票£3.8起!心累!美国生活成本最高十大城市 | Seattle-Tacoma-Bellevue排第五,不赚这么多别想过舒服​福建:坚定信心稳中有进 高质量发展迈出更快更稳步伐名企面试进度曝光!春招来了,Hirevue、OA全都回来了!Bellevue轻轨出没请小心!底座缺陷、先开路线之争,一波三折的东线终于恢复测试题型再变!大摩、美银Hirevue逼疯留学生Bellevue小学合并新进展!买学区房需谨慎,西雅图等学区都在考虑合并、裁员
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。