前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >用Typescript 的方式封装Vue3的表单绑定,支持防抖等功能。

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

作者头像
用户1174620
发布于 2022-06-27 01:14:33
发布于 2022-06-27 01:14:33
1.2K00
代码可运行
举报
运行总次数:0
代码可运行

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

基础使用方法

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

自己做组件

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

https://staging-cn.vuejs.org/guide/components/events.html#usage-with-v-model

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// <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
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  // v-model 、 emit 的封装
  const emitVal = ref('')
  // 传递 对象
  const person = reactive({name: '测试', age: 111})
  // 范围,分为两个属性
  const date = reactive({d1: '2012-10-11', d2: '2012-11-11'})
  • template
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  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
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// <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
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<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
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<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 了。

范围取值

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
<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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-06-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
严选 | Elastic中文社区201903错题本
开发最懊悔的事莫过于:自己费尽脑汁、花费了很长时间解决了问题,原来别人在社区或者别的地方早已经给出了更优化的方案。
铭毅天下
2019/05/08
1.7K0
Elasticsearch7.6学习笔记1 Getting start with Elasticsearch
安装方法见: https://www.cnblogs.com/woshimrf/p/docker-es7.html
Ryan-Miao
2020/04/12
1.6K0
ElasticSearch-7.10 参考手册
https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html
马说
2021/06/30
5.7K0
ElasticSearch-7.10 参考手册
干货 | Elasticsearch 趋势科技实战分享笔记
单一索引的问题: 1)不能更新Mapping。 比如:主分片数不可以修改(除非reindex)。 2)无法灵活、快速地扩展。 3)更适合固定、小型数据集。
铭毅天下
2018/07/26
8780
干货 | Elasticsearch 趋势科技实战分享笔记
Elasticsearch基本使用
可以在https://www.elastic.co/cn/downloads/elasticsearch这个页面找到elasticsearch对应系统的安装包,elasticsearch用java开发的, 最新的版本内置了对应的jdk, 通过下面的方式能快速启动:
良辰美景TT
2020/12/14
6630
乐优项目:Elasticsearch介绍和安装及使用-(六)
而商品的数量非常多,而且分类繁杂。如何能正确的显示出用户想要的商品,并进行合理的过滤,尽快促成交易,是搜索系统要研究的核心。
用户4396583
2024/08/14
3860
Elasticsearch性能优化实战指南
在当今世界,各行各业每天都有海量数据产生,为了从这些海量数据中获取想要的分析结果,需要对数据进行提取、转换,存储,维护,管理和分析。 这已然远远超出了普通处理工具、数据库等的实现能力,只有基于的分布式架构和并行处理机制的大数据工具所才能实现这些功能。Elasticsearch是响应如前所述大多数用例的最热门的开源数据存储引擎之一。
程序员追风
2019/08/02
9210
Elasticsearch性能优化实战指南
ElasticSearch 6.x 学习笔记:14.mapping参数
官方文档 https://www.elastic.co/guide/en/elasticsearch/reference/6.1/mapping-params.html ElasticSearch提供了丰富的映射参数对字段的映射进行参数设计,比如字段的分词器、字段权重、日期格式、检索模型等等。
程裕强
2022/05/06
1.3K0
elasticsearch 学习笔记01
ES 对它的最小词源(Term) 维护了一个“倒排索引”,即 “从 最小词源 到文档ID 的映射”。 在文档入库时会先分词,完成后可查询。当查询时,比如 中国,人民 这样 的词,在查找时它所对应的 数据记录的ID有,1,14,1001 这样的数据ID。es 把这些ID的记录包含组成结果返回就是查询结果了。
张云飞Vir
2022/09/29
8430
Elasticsearch:透彻理解 Elasticsearch 中的 Bucket aggregation
Elasticsearch 除了在搜索方面非常之快,对数据分析也是非常重要的一面。正确理解 Bucket aggregation 对我们使用 Kibana 非常重要。Elasticsearch 提供了非常多的 aggregation 可以供我们使用。其中 Bucket aggregation 对于初学者来说也是比较不容易理解的一个。在今天的这篇文章中,我来重点讲述这个。
腾讯云大数据
2020/10/13
2.8K0
Elasticsearch:透彻理解 Elasticsearch 中的 Bucket aggregation
Elasticsearch7教程
Elasticsearch是一个基于Lucene的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于RESTful web接口。Elasticsearch是用Java语言开发的,并作为Apache许可条款下的开放源码发布,是一种流行的企业级搜索引擎。Elasticsearch用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。官方客户端在Java、.NET(C#)、PHP、Python、Apache Groovy、Ruby和许多其他语言中都是可用的。根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr,也是基于Lucene。
Remember_Ray
2021/04/05
4.2K0
数万字长文带你入门elasticsearch
es会根据创建的文档动态生成映射,可以直接将动态生成的映射直接复制到需要自定义的mapping中
没有故事的陈师傅
2022/04/05
1.8K0
【Elasticsearch系列十二】聚合-电视案例
hits.hits:我们指定了 size 是 0,所以 hits.hits 就是空的
kwan的解忧杂货铺
2024/09/18
1010
Elasticsearch 聚合性能优化六大猛招
默认情况下,Elasticsearch 已针对大多数用例进行了优化,确保在写入性能和查询性能之间取得平衡。我们将介绍一些聚合性能优化的可配置参数,其中部分改进是以牺牲写入性能为代价的。目标是将聚合优化招数汇总到一个易于消化的短文中,为大家的 Elasticsearch 集群聚合性能优化提供一些指导。
铭毅天下
2021/02/03
4.2K0
Elasticsearch 聚合性能优化六大猛招
Elasticsearch快速入门,掌握这些刚刚好!
Elasticsearch是一个基于Lucene的搜索服务器。它提供了一个分布式的全文搜索引擎,基于restful web接口。Elasticsearch是用Java语言开发的,基于Apache协议的开源项目,是目前最受欢迎的企业搜索引擎。Elasticsearch广泛运用于云计算中,能够达到实时搜索,具有稳定,可靠,快速的特点。
macrozheng
2020/04/10
8040
Elasticsearch快速入门,掌握这些刚刚好!
Elasticsearch使用:Bucket aggregation
Elasticsearch 除了在搜索方面非常之快,对数据分析也是非常重要的一面。正确理解 Bucket aggregation 对我们使用 Kibana 非常重要。Elasticsearch 提供了非常多的 aggregation  [ˌæɡrɪˈɡeɪʃn] 可以供我们使用。其中 Bucket aggregation 对于初学者来说也是比较不容易理解的一个。在今天的这篇文章中,我来重点讲述这个。
HLee
2021/01/08
3.3K0
Elasticsearch使用:Bucket aggregation
Elasticsearch初检索及高级
PUT customer/external/1 :在 customer 索引下的 external 类型下保存 1号数据
乐心湖
2021/01/18
1.1K0
Elasticsearch初检索及高级
ElasticSearch 6.x 学习笔记:13.mapping元字段
mapping元字段官网文档 https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-fields.html#_document_source_meta_fields
程裕强
2022/05/06
5170
ElasticSearch教程_Elasticsearch原理
Elasticsearch 是一个分布式的 RESTful 风格的搜索和数据分析引擎。
全栈程序员站长
2022/09/19
1.8K0
ElasticSearch教程_Elasticsearch原理
Elasticsearch 快速开始
本文非完全直译译文,主要参考的的是 elasticsearch 6.5 版的官网文档 Getting Started,可以把这篇文章理解为个人学习笔记,我力求详略得当吧。
波罗学
2019/08/26
1.8K0
相关推荐
严选 | Elastic中文社区201903错题本
更多 >
LV.1
HTML5学堂(码匠)创始人
目录
  • 基础使用方法
    • 自己做组件
    • 对UI库的组件进行二次封装
  • 建立 vue3 项目
  • v-model 的封装
    • 基本封装方式
    • 支持防抖的方式
  • 直接传递 model 的方法。
    • 基础封装方式
    • 防抖的实现方式
    • 范围取值(多字段)的封装方式
  • 使用方法
    • emit
    • model
    • 范围取值
  • TS 的尴尬
  • 对比一下
  • 源码
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档