用Typescript 的方式封装Vue3的表单绑定,支持防抖等功能。

news/2024/5/20 23:51:52/文章来源:https://blog.csdn.net/qq_48652579/article/details/130838128

Vue3 的父子组件传值、绑定表单数据、UI库的二次封装、防抖等,想来大家都很熟悉了,本篇介绍一种使用 Typescript 的方式进行统一的封装的方法。

基础使用方法

Vue3对于表单的绑定提供了一种简单的方式:v-model。对于使用者来说非常方便,v-model="name" 就可以了。

自己做组件

但是当我们要自己做一个组件的时候,就有一点麻烦:

https://staging-cn.vuejs.org/guide/components/events.html#usage-with-v-modelicon-default.png?t=N4N7https://links.jianshu.com/go?to=https%3A%2F%2Fstaging-cn.vuejs.org%2Fguide%2Fcomponents%2Fevents.html%23usage-with-v-model

<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script><template><input:value="modelValue"@input="$emit('update:modelValue', $event.target.value)"/>
</template>

需要我们定义 props、emit、input 事件等。

对UI库的组件进行二次封装

如果我们想对UI库进行封装的话,就又麻烦了一点点:

https://staging-cn.vuejs.org/guide/components/events.html#usage-with-v-modelicon-default.png?t=N4N7https://links.jianshu.com/go?to=https%3A%2F%2Fstaging-cn.vuejs.org%2Fguide%2Fcomponents%2Fevents.html%23usage-with-v-model

// <script setup>
import { computed } from 'vue'const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])const value = computed({get() {return props.modelValue},set(value) {emit('update:modelValue', value)}
})
// </script><template><el-input v-model="value" />
</template>

由于 v-model 不可以直接用组件的 props,而 el-input 又把原生的 value 变成了 v-model 的形式,所以需要使用 computed 做中转,这样代码就显得有点繁琐。

如果考虑防抖功能的话,代码会更复杂一些。

代码为啥会越写越乱?因为没有及时进行重构和必要的封装!

建立 vue3 项目

情况讲述完毕,我们开始介绍解决方案。

首先采用 vue3 的最新工具链:create-vue, 建立一个支持 Typescript 的项目。
https://staging-cn.vuejs.org/guide/typescript/overview.htmlicon-default.png?t=N4N7https://links.jianshu.com/go?to=https%3A%2F%2Fstaging-cn.vuejs.org%2Fguide%2Ftypescript%2Foverview.html

先用 Typescript 的方式封装一下 v-model,然后再采用一种更方便的方式实现需求,二者可以对照看看哪种更适合。

v-model 的封装

我们先对 v-model、emit 做一个简单的封装,然后再加上防抖的功能。

基本封装方式

  • ref-emit.ts

import { customRef } from 'vue'/*** 控件的直接输入,不需要防抖。负责父子组件交互表单值* @param props 组件的 props* @param emit 组件的 emit* @param key v-model 的名称,用于 emit*/
export default function emitRef<T, K extends keyof T & string>
(props: T,emit: (event: any, ...args: any[]) => void,key: K
) {return customRef<T[K]>((track: () => void, trigger: () => void) => {return {get(): T[K] {track()return props[key] // 返回 modelValue 的值},set(val: T[K]) {trigger()// 通过 emit 设置 modelValue 的值emit(`update:${key.toString()}`, val) }}})
}
  • K keyof T
    因为属性名称应该在 props 里面,所以使用 keyof T 的方式进行约束。

  • T[K]
    可以使用 T[K] 作为返回类型。

  • key 的默认值
    尝试了各种方式,虽然可以运行,但是TS会报错。可能是我打开的方式不对吧。

  • customRef
    为啥没有用 computed?因为后续要增加防抖功能。
    在 set 里面使用 emit 进行提交,在 get 里面获取 props 里的属性值。

  • emittype
    emit: (event: any, ...args: any[]) => void,各种尝试,最后还是用了any。

这样简单的封装就完成了。

支持防抖的方式

官网提供的防抖代码,对应原生 input 是好用的,但是用在 el-input 上面就出了一点小问题,所以只好修改一下:

  • ref-emit-debounce.ts

import { customRef, watch } from 'vue'/*** 控件的防抖输入,emit的方式* @param props 组件的 props* @param emit 组件的 emit* @param key v-model的名称,默认 modelValue,用于emit* @param delay 延迟时间,默认500毫秒*/
export default function debounceRef<T, K extends keyof T> 
(props: T,emit: (name: any, ...args: any[]) => void,key: K,delay = 500
) {// 计时器let timeout: NodeJS.Timeout// 初始化设置属性值let _value = props[key]return customRef<T[K]>((track: () => void, trigger: () => void) => {// 监听父组件的属性变化,然后赋值,确保响应父组件设置属性watch(() => props[key], (v1) => {_value = v1trigger()})return {get(): T[K] {track()return _value},set(val: T[K]) {_value = val // 绑定值trigger() // 输入内容绑定到控件,但是不提交clearTimeout(timeout) // 清掉上一次的计时// 设置新的计时timeout = setTimeout(() => {emit(`update:${key.toString()}`, val) // 提交}, delay)}}})
}
  • timeout = setTimeout(() => {})
    实现防抖功能,延迟提交数据。

  • let _value = props[key]
    定义一个内部变量,在用户输入字符的时候保存数据,用于绑定组件,等延迟后再提交给父组件。

  • watch(() => props[key], (v1) => {})
    监听属性值的变化,在父组件修改值的时候,可以更新子组件的显示内容。
    因为子组件的值对应的是内部变量 _value,并没有直接对应props的属性值。

这样就实现了防抖的功能。

直接传递 model 的方法。

一个表单里面往往涉及多个字段,如果每个字段都使用 v-model 的方式传递的话,就会出现“中转”的情况,这里的“中转”指的是 emit,其内部代码比较复杂。

如果组件嵌套比较深的话,就会多次“中转”,这样不够直接,也比较繁琐。
另外如果需要 v-for 遍历表单子控件的话,也不方便处理多 v-model 的情况。

所以为什么不把一个表单的 model 对象直接传入子组件呢?这样不管嵌套多少层组件,都是直接对地址进行操作,另外也方便处理一个组件对应多个字段的情况。

当然,也有一点麻烦的地方,需要多传入一个属性,记录组件要操作的字段名称。

组件的 props 的类型是 shallowReadonly,即根级只读,所以我们可以修改传入的对象的属性。

基础封装方式

  • ref-model.ts

import { computed } from 'vue'/*** 控件的直接输入,不需要防抖。负责父子组件交互表单值。* @param model 组件的 props 的 model* @param colName 需要使用的属性名称*/
export default function modelRef<T, K extends keyof T> (model: T, colName: K) {return computed<T[K]>({get(): T[K] {// 返回 model 里面指定属性的值return model[colName]},set(val: T[K]) {// 给 model 里面指定属性赋值model[colName] = val}})
}

我们也可以使用 computed 来做中转,还是用 K extends keyof T做一下约束。

防抖的实现方式

  • ref-model-debounce.ts

import { customRef, watch } from 'vue'import type { IEventDebounce } from '../types/20-form-item'/*** 直接修改 model 的防抖* @param model 组件的 props 的 model* @param colName 需要使用的属性名称* @param events 事件集合,run:立即提交;clear:清空计时,用于汉字输入* @param delay 延迟时间,默认 500 毫秒*/
export default function debounceRef<T, K extends keyof T> (model: T,colName: K,events: IEventDebounce,delay = 500
) {// 计时器let timeout: NodeJS.Timeout// 初始化设置属性值let _value: T[K] = model[colName]return customRef<T[K]>((track: () => void, trigger: () => void) => {// 监听父组件的属性变化,然后赋值,确保响应父组件设置属性watch(() => model[colName], (v1) => {_value = v1trigger()})return {get(): T[K] {track()return _value},set(val: T[K]) {_value = val // 绑定值trigger() // 输入内容绑定到控件,但是不提交clearTimeout(timeout) // 清掉上一次的计时// 设置新的计时timeout = setTimeout(() => {model[colName] = _value // 提交}, delay)}}})
}

对比一下就会发现,代码基本一样,只是取值、赋值的地方不同,一个使用 emit,一个直接给model的属性赋值。

那么能不能合并为一个函数呢?当然可以,只是参数不好起名,另外需要做判断,这样看起来就有点不易读,所以还是做两个函数直接一点。

我比较喜欢直接传入 model 对象,非常简洁。

范围取值(多字段)的封装方式

开始日期、结束日期,可以分为两个控件,也可以用一个控件,如果使用一个控件的话,就涉及到类型转换,字段对应的问题。

所以我们可以再封装一个函数。

  • ref-model-range.ts

import { customRef } from 'vue'interface IModel {[key: string]: any
}/*** 一个控件对应多个字段的情况,不支持 emit* @param model 表单的 model* @param arrColName 使用多个属性,数组*/
export default function range2Ref<T extends IModel, K extends keyof T>
(model: T,...arrColName: K[]
) {return customRef<Array<any>>((track: () => void, trigger: () => void) => {return {get(): Array<any> {track()// 多个字段,需要拼接属性值const tmp: Array<any> = []arrColName.forEach((col: K) => {// 获取 model 里面指定的属性值,组成数组的形式tmp.push(model[col])})return tmp},set(arrVal: Array<any>) {trigger()if (arrVal) {arrColName.forEach((col: K, i: number) => {// 拆分属性赋值,值的数量可能少于字段数量if (i < arrVal.length) {model[col] = arrVal[i]} else {model[col] = ''}})} else {// 清空选择arrColName.forEach((col: K) => {model[col] = '' // undefined})}}}})
}
  • IModel
    定义一个接口,用于约束泛型 T,这样 model[col] 就不会报错了。

这里就不考虑防抖的问题了,因为大部分情况都不需要防抖。

使用方法

封装完毕,在组件里面使用就非常方便了,只需要一行即可。

先做一个父组件,加载各种子组件做一下演示。

  • js

  // v-model 、 emit 的封装const emitVal = ref('')// 传递 对象const person = reactive({name: '测试', age: 111})// 范围,分为两个属性const date = reactive({d1: '2012-10-11', d2: '2012-11-11'})
  • template

  emit 的封装<input-emit v-model="emitVal"/><input-emit v-model="person.name"/>model的封装<input-model :model="person" colName="name"/><input-model :model="person" colName="age"/>model 的范围取值<input-range :model="date" colName="d1_d2"/>

emit

我们做一个子组件:

  • 10-emit.vue

// <template><!--测试 emitRef--><el-input v-model="val"></el-input>
// /template>// <script lang="ts">import { defineComponent } from 'vue'import emitRef from '../../../../lib/base/ref-emit'export default defineComponent({name: 'nf-demo-base-emit',props: {modelValue: {type: [String, Number, Boolean, Date]}},emits: ['update:modelValue'],setup(props, context) {const val = emitRef(props, context.emit, 'modelValue')return {val}}})
// </script>

定义一下 propsemit,然后调用函数即可。
也支持 script setup 的方式:

  • 12-emit-ss.vue

<template><el-input v-model="val" ></el-input>
</template><script setup lang="ts">import emitRef from '../../../../lib/base/ref-emit'const props = defineProps<{modelValue: string}>()const emit = defineEmits<{(e: 'update:modelValue', value: string): void}>()const val = emitRef(props, emit, 'modelValue')</script>

定义props,定义emit,然后调用 emitRef

model

我们做一个子组件

  • 20-model.vue

<template><el-input v-model="val2"></el-input>
</template><script lang="ts">import { defineComponent } from 'vue'import type { PropType } from 'vue'import modelRef from '../../../../lib/base/ref-model'interface Person {name: string,age: 12}export default defineComponent({name: 'nf-base-model',props: {model: {type: Object as PropType<Person>},colName: {type: String},setup(props, context) {const val2 = modelRef(props.model, 'name')return {val2}}})
</script>

定义 props,然后调用即可。
虽然多了一个描述字段名称的参数,但是不用定义和传递 emit 了。

范围取值

<template><el-date-pickerv-model="val2"type="daterange"value-format="YYYY-MM-DD"range-separator="-"start-placeholder="开始日期"end-placeholder="结束日期"/>
</template><script lang="ts">import { defineComponent } from 'vue'import type { PropType } from 'vue'import rangeRef from '../../../../lib/base/ref-model-range2'interface DateRange {d1: string,d2: string}export default defineComponent({name: 'nf-base-range',props: {model: {type: Object as PropType<DateRange>},colName: {type: [String]}},setup(props, context) {const val2 = rangeRef<DateRange>(props.model, 'd1', 'd2')return {val2}}})
</script>

el-date-picker 组件在 type="daterange" 的时候,v-model 是一个数组,而后端数据库的设置,一般是两个字段,比如 startDate、endDate,需要提交的也是对象形式,这样就需要在数组和对象之间做转换。

而我们封装的 rangeRef 就可以做这样的转换。

TS 的尴尬

可能你会注意到,上面的例子没有使用 colName 属性,而是直接传递字符层的参数。

因为 TS 只能做静态检查,不能做动态检查,直接写字符串是静态的方式,TS可以检查。

但是使用 colName 属性的话,是动态的方式,TS的检查不支持动态,然后直接给出错误提示。

虽然可以正常运行,但是看着红线,还是很烦的,所以最后封装了个寂寞。

对比一下

对比项目emitmodel
类型明确困难很明确
参数(使用)一个两个
效率emit内部需要中转直接使用对象地址修改
封装难度有点麻烦轻松
组件里使用需要定义emit不需要定义emit
多字段(封装)无需单独封装需要单独封装
多字段(使用)需要写多个v-model不需要增加参数的数量
多字段(表单v-for)不好处理容易

如果表单里的子组件,想采用 v-for 的方式遍历出来的话,显然 model 的方式更容易实现,因为不用考虑一个组件需要写几个 v-model。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_306462.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

镜像二叉树和求二叉树最大深度(java)

镜像二叉树和求二叉树最大深度 镜像二叉树。有些题目叫翻转二叉树。是同一个题。二叉树的最大深度 镜像二叉树。有些题目叫翻转二叉树。是同一个题。 题目描述&#xff1a;给你一棵二叉树的根节点 root &#xff0c;翻转这棵二叉树&#xff0c;并返回其根节点。 示例&#xff1…

基于LLMs的多模态大模型(Visual ChatGPT,PICa,MM-REACT,MAGIC)

当LLMs已经拥有了极强的对话能力后&#xff0c;如何使其拥有视觉和语音等多模态能力是紧接而来的热点&#xff08;虽然GPT4已经有了&#xff09;&#xff0c;这个系列将不定期更新一些利用LLMs做多模态任务的文章。 直觉上&#xff0c;如果直接训练一个类似chatgpt架构的多模态…

sql查询指定数据的函数(等于、and、or、in、find_in_set、like)

sql查询指定数据的函数&#xff08;等于、and、or、in、find_in_set、like&#xff09;&#xff1a; 1.查询指定单字段的指定数据&#xff1a; 举例&#xff1a;查询user表中address字段数据等于aa的数据&#xff1b; select * from user where address aa 2.查询指定多字段…

【2023 · CANN训练营第一季】MindSpore模型快速调优攻略 第一章——调试调优背景介绍

1.模型迁移、模型调试调优背景介绍 模型训练\推理过程中可能遇到的问题&#xff1a; • 代码编写错误&#xff0c;问题难以定位&#xff1b; • 模型结构错误&#xff1b; • 权重更新错误&#xff1b; • 损失函数设计错误&#xff1b; • 半精度下计算溢出&#xff1b; • L…

Kali-linux Gerix Wifi Cracker破解无线网络

Gerix Wifi Cracker是另一个aircrack图形用户界面的无线网络破解工具。本节将介绍使用该工具破解无线网络及创建假的接入点。 9.3.1 Gerix破解WEP加密的无线网络 在前面介绍了手动使用Aircrack-ng破解WEP和WPA/WPA2加密的无线网络。为了方便&#xff0c;本小节将介绍使用Geri…

​路由器端口映射怎么设置?内网IP不能映射怎么办?​

使用路由器后&#xff0c;Internet用户无法访问到局域网内的主机&#xff0c;因此不能访问内网搭建的Web、FTP、Mail等服务器。路由器 端口映射功能可以实现将内网的服务器映射到Internet&#xff0c;从而实现服务器对外开放。 建议在设置路由器端口映射之前&#xff0c;确认以…

基于北斗+LoRa的落水报警定位方案一 -实现无人区,弱信号地区人员 位置安全监控

人员安全一直是企业管理的重中之重。无人区工地是一个安全事故多发的地方&#xff0c;在施工环境复杂且危险的情况下&#xff0c;工地人员位置监控成为管理工作中的一个难点&#xff0c;一直以来采用粗放的方式&#xff0c;现有的工地项目存在施工人员实时督查难等问题&#xf…

【程序员面试金典】面试题 08.02. 迷路的机器人

【程序员面试金典】面试题 08.02. 迷路的机器人 题目描述解题思路 题目描述 描述&#xff1a;设想有个机器人坐在一个网格的左上角&#xff0c;网格 r 行 c 列。机器人只能向下或向右移动&#xff0c;但不能走到一些被禁止的网格&#xff08;有障碍物&#xff09;。设计一种算…

c++ 11标准模板(STL) std::map(四)

定义于头文件<map> template< class Key, class T, class Compare std::less<Key>, class Allocator std::allocator<std::pair<const Key, T> > > class map;(1)namespace pmr { template <class Key, class T, clas…

尚硅谷Docker实战教程-笔记01【理念简介、官网介绍、平台入门图解、平台架构图解】

尚硅谷大数据技术-教程-学习路线-笔记汇总表【课程资料下载】视频地址&#xff1a;尚硅谷Docker实战教程&#xff08;docker教程天花板&#xff09;_哔哩哔哩_bilibili 尚硅谷Docker实战教程-笔记01【理念简介、官网介绍、平台入门图解、平台架构图解】尚硅谷Docker实战教程-笔…

order by排序语句的用法

文章目录 学习连接语法用法示例1、按单个列的值排序2、按多个列的值排序3、按指定的规则排序4、按中文拼音字母顺序5、Order by和where条件共用 数据库中常用order by关键字对结果集进行排序&#xff0c;又可使用desc和asc来进行指定规则的排序。 学习连接 数据库&#xff1a;…

办公技巧:学会这 7 种 PPT 制作技巧,让 PPT 制作效率飙升

F4 键&#xff1a;重复上一步操作 例如需要你在一分钟内完成 8 个形状的排版&#xff0c;你会怎么做&#xff1f; 如果是最基础的方式&#xff0c;可能得画出一个之后&#xff0c;慢慢的按住 Ctrl 复制新的出来&#xff0c;但这样实在是太慢了&#xff01;&#xff08;你是这样…

重磅发布 |《2023年消费品企业数字化转型调研白皮书》

数字经济时代&#xff0c;数字化转型对于消费品企业越来越重要&#xff0c;已成为消费品企业增加经济效益、提升竞争力的重要策略之一。随着数字化转型进入深水区&#xff0c;越来越多涉及到商业模式、业务模式和产业链生态的创新变革开始出现&#xff0c;给消费品企业带来了一…

【UE】windows包蓝图分辨率设置

【UE】windows包蓝图分辨率设置 屏幕模式&#xff08;SetFullscreenMode&#xff09; 模式说明全屏可以任意修改分辨率的全屏窗口化全屏适配显示器分辨率的全屏窗口化窗口化 分辨率修改方法(SetScreenResolution) 控制台修改分辨率方法(ExecuteConsoleCommand) 命令 &#xf…

Android深入源码分析事件分发机制流程

前言 对于Android中的触摸事件即指手指触摸到屏幕时产生的点击事件&#xff1b; 类型如下&#xff1a; MotionEvent.ACTION_DOWNMotionEvent.ACTION_UPMotionEvent.ACTION_MOVEMotionEvent.ACTION_CANCEL Android事件处理流程 主要涉及三个流程&#xff1a;事件采集、事件中…

@Qualifier注解详解

本文来说下Qualifier注解使用 文章目录 Autowired注解Qualifier注解Primary注解 Resource注解 Autowired注解 spring Autowire 的注解默认是按类型注入bean 如果存在多个实现类&#xff0c;可以使用 Qualifier注解 Qualifier注解 Primary注解 存在多个实现类&#xff0c;也可以…

CCF-CSP 201903-1 小中大

该题比较简单&#xff0c;因为所给数据都是排好序的了&#xff0c;对于最大值与最小值我们只需判断第一个元素与最后一个元素的关系即可&#xff0c;而中位数的判断则与n的大小有关&#xff0c;如果n为偶数&#xff08;下标从1开始&#xff09;&#xff0c;那么中间会存在两个数…

机器学习:基于Apriori算法对中医病症辩证关联规则分析

系列文章目录 作者&#xff1a;i阿极 作者简介&#xff1a;Python领域新星作者、多项比赛获奖者&#xff1a;博主个人首页 &#x1f60a;&#x1f60a;&#x1f60a;如果觉得文章不错或能帮助到你学习&#xff0c;可以点赞&#x1f44d;收藏&#x1f4c1;评论&#x1f4d2;关注…

针对UDP协议的攻击与防御

一、UDP协议概述 UDP&#xff08;User Datagram Protocol&#xff0c;用户数据报协议&#xff09;是TCP/IP协议栈中的一种无连接的传输协议&#xff0c;能够提供面向事务的简单不可靠数据传输服务。 1&#xff0e;UDP的报文格式 UDP的报文格式如图1所示。 图1 UDP报文格式 …

探索可视化大屏:引领信息时代的视觉革命

可视化大屏是一种利用先进的数据可视化技术和交互技术&#xff0c;将大量的数据和信息以直观、易于理解的方式展示在大屏幕上的解决方案。可视化大屏通常由高分辨率的显示屏、强大的计算和处理设备以及专业的可视化软件组成&#xff0c;它通过图表、图形、动画等可视化元素&…