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

基础使用方法

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

自己做组件

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

https://staging-cn.vuejs.org/guide/components/events.html#usage-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-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.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 里的属性值。

  • emit 的 type

    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 = v1
trigger()
}) 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 = v1
trigger()
}) 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>

定义一下 props 和 emit,然后调用函数即可。

也支持 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-picker
v-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的检查不支持动态,然后直接给出错误提示。

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

对比一下

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

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

源码

https://gitee.com/naturefw-code/nf-rollup-ui-controller

用Typescript 的方式封装Vue3的表单绑定,支持防抖等功能。的更多相关文章

  1. Servlet的5种方式实现表单提交(注册小功能)

    Servlet的5种方式实现表单提交(注册小功能),后台获取表单数据   用servlet实现一个注册的小功能 ,后台获取数据. 注册页面: 注册页面代码 : <!DOCTYPE html> ...

  2. Vue + Element-ui实现后台管理系统(5)---封装一个Form表单组件和Table表格组件

    封装一个Form表单组件和Table组件 有关后台管理系统之前写过四遍博客,看这篇之前最好先看下这四篇博客.另外这里只展示关键部分代码,项目代码放在github上: mall-manage-syste ...

  3. 通过jQuery的Ajax方式来提交Form表单

    通过jQuery的Ajax方式来提交Form表单 $.ajax({ url:ajaxCallUrl, type:"POST", cache:true, async:false, d ...

  4. VUE3 之 表单元素

    1. 概述 老话说的好:行动起来,原地观望是没有用的. 言归正传,今天我们来聊聊 VUE3 的 表单元素. 2. 表单元素 2.1 文本框与数据绑定 <body> <div id=& ...

  5. Knockout学习之表单绑定器(上)

    表单绑定器 “click”绑定 Click 绑定器可以将javascript函数绑定到指定的dom元素,并且再该元素被点击时将触发绑定的函数,大多数情况下都会使用button.input和a元素,当然 ...

  6. 【js类库AngularJs】学习angularJs的指令(包括常见表单验证,隐藏等功能)

    [js类库AngularJs]学习angularJs的指令(包括常见表单验证,隐藏等功能) AngularJS诞生于2009年,由Misko Hevery 等人创建,后为Google所收购.是一款优秀 ...

  7. Spirng MVC +Velocity 表单绑定命令对象

    通常,表单中的数据在提交之后可以通过Spring MVC的@RequestParam注解在控制器函数的参数列表中中提取出来,但是一旦表单数据过多的话,参数列表将会变得非常长,最好的解决方案是将表单中的 ...

  8. Vue表单绑定(单选按钮,选择框(单选时,多选时,用 v-for 渲染的动态选项)

    <!DOCTYPE html><html>    <head>        <meta charset="utf-8">      ...

  9. vue -- v-model 表单绑定

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

随机推荐

  1. FSB—QPI—DMI总线的发展

    intel CPU有的是前端总线(FSB),有的是QPI总线,有的又是DMI总线 FSB总线(由于cpu的发展,fsb总线制约了cpu的发展,所以该总线已经渐渐淡出历史舞台) FSB即Front Si ...

  2. openoffice将word转pdf中文乱码或消失的坑

    将windows系统下的中文字体文件(C:\Windows\Fonts),放到/usr/share/fonts下,必须重启openoffice.

  3. 【GPLT】 集合相似度(c++)

    题目如下: 这题主要用来练习stl的使用,是一道比较简单的题目 AC代码如下 #include<iostream> #include<cmath> #include<ma ...

  4. CoAP调试工具Mozi.IoT.CoAP应用详解

    前言 CoAP是一种类HTTP协议的物联网专用协议,其数据包为人类不可阅读的字节流形式,在开发相关应用的时候往往不能准确的了解数据包的内容.故需要专用的调试工具对数据和通讯进行调试.这篇文章是为了让开 ...

  5. i2c调试工具分享

    i2c-tools简介 在嵌入式开发仲,有时候需要确认硬件是否正常连接,设备是否正常工作,设备的地址是多少等等,这里我们就需要使用一个用于测试I2C总线的工具--i2c-tools. i2c-tool ...

  6. MKL与VS2019配置方法

    VS2019配置oneAPI并调用MKL库 oneAPI oneAPI是一个跨架构的编程工具,旨在简化跨GPU.CPU.FPGA和AI加速器之间的编程,可以与英特尔自身设备,或其他厂商的芯片配合使用, ...

  7. 攻防世界-MISC:掀桌子

    这是攻防世界新手练习区的第八题,题目如下: 就给了一串16进制的字符串.哎,又是不懂,看了一下官方WP,说是将每两位16进制数转换为10进制,再减去128再转换为ASCII码.直接上脚本 str1 = ...

  8. ucore lab5 用户进程管理 学习笔记

    近几日睡眠质量不佳,脑袋一困就没法干活,今天总算时补完了.LAB5难度比LAB4要高,想要理解所有细节时比较困难.但毕竟咱不是要真去写一个OS,所以一些个实现细节就当成黑箱略过了. 这节加上了用户进程 ...

  9. 如何在同一Linux服务器上创建多站点

    在没有域名的情况下,怎样才能创建出多站点访问?这个问题困扰我许久,之后阅读了<http权威指南>,这本让我恍然大悟.这里说明了从浏览器如何解析域名,再请求服务器,服务器收到请求后是如何处理 ...

  10. Nginx中FastCGI参数的优化配置实例

    在配置完成Nginx+FastCGI之后,为了保证Nginx下PHP环境的高速稳定运行,需要添加一些FastCGI优化指令.下面给出一个优化实例,将下面代码添加到Nginx主配置文件中的HTTP层级. ...