Use Typescript para encapsular el enlace de formulario de Vue3 y funciones de soporte como antivibración.

Todo el mundo está familiarizado con la transferencia de valor del componente padre-hijo de Vue3 , los datos de formulario de enlace, el empaquetado secundario de la biblioteca de la interfaz de usuario, la antivibración, etc. Este artículo presenta un método de empaquetado unificado utilizando Typescript .

Uso básico

Vue3 proporciona una forma sencilla de vincular formularios: v-model . Es muy conveniente para el usuario, muy bien.v-model="name"

hacer sus propios componentes

Pero cuando queremos hacer un componente nosotros mismos, hay un pequeño problema:

https://staging-cn.vuejs.org/guide/components/events.html#usage-with-v-model icono-predeterminado.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>

Necesitamos definir accesorios, emitir, eventos de entrada, etc.

Encapsulación secundaria de los componentes de la biblioteca de la interfaz de usuario

Si queremos encapsular la biblioteca de la interfaz de usuario, será un poco más problemático:

https://staging-cn.vuejs.org/guide/components/events.html#usage-with-v-model icono-predeterminado.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>

Dado que v-model no puede usar accesorios de componentes directamente, y el-input convierte el valor original en la forma de v-model , debe usar computated para la transferencia, por lo que el código es un poco engorroso.

Si se considera la función antivibración, el código será más complicado.

¿Por qué el código se vuelve cada vez más desordenado? ¡Porque no hubo refactorización oportuna y encapsulación necesaria!

Crear un proyecto vue3

Ahora que se ha contado la situación, pasemos a la solución.

Primero, use la última cadena de herramientas de vue3: create-vue, y cree un proyecto que admita Typescript .
https://staging-cn.vuejs.org/guide/typescript/overview.html icono-predeterminado.png?t=N4N7https://links.jianshu.com/go?to=https%3A%2F%2Fstaging-cn.vuejs.org%2Fguide%2Ftypescript %2Foverview.html

Primero use Typescript para encapsular v-model, y luego use una forma más conveniente de cumplir con los requisitos. Puede comparar los dos para ver cuál es el más adecuado.

Encapsulación del modelo v

Primero hacemos un paquete simple para v-model y emit , y luego agregamos la función antivibración.

Embalaje básico

  • 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
    Debido a que el nombre del atributo debe estar en props, use keyof T para restringir.

  • T[K]
    puede usar T[K] como tipo de retorno.

  • El valor predeterminado de la clave
    Probó varios métodos, aunque puede ejecutarse, pero TS informará un error. Tal vez sea la forma incorrecta de abrirlo.

  • ¿Por qué no se calcula el uso de customRef
    ? Porque la función antivibración se agregará en el futuro.
    Use emit para enviar en conjunto y obtenga el valor del atributo en props en get.

  • El tipo de emit
    emit: (event: any, ...args: any[]) => void , después de varios intentos, finalmente usó ninguno.

Esta simple encapsulación está completa.

La forma de apoyar anti-vibración

El código antivibración proporcionado por el sitio web oficial es fácil de usar para la entrada nativa, pero hay un pequeño problema cuando se usa en la entrada electrónica, así que tengo que modificarlo:

  • 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(() => {})
    realiza la función antivibración y retrasa el envío de datos.

  • let _value = props[key]
    define una variable interna, guarda datos cuando el usuario ingresa caracteres, los usa para vincular componentes y los envía al componente principal después de un retraso.

  • watch(() => props[key], (v1) => {})
    supervisa el cambio del valor de la propiedad y puede actualizar el contenido de visualización del componente secundario cuando el componente principal modifica el valor.
    Debido a que el valor del subcomponente corresponde a la variable interna _value, no corresponde directamente al valor del atributo de props.

De esta manera, se realiza la función antivibración.

Método para pasar el modelo directamente.

Un formulario a menudo involucra múltiples campos. Si cada campo es pasado por v-model , habrá una situación de "tránsito". El "tránsito" aquí se refiere a emit , y su código interno es más complicado.

Si el anidamiento del componente es profundo, se "transitará" varias veces, lo que no es lo suficientemente directo y engorroso.
Además, si se requiere v-for para recorrer los subcontroles del formulario, no es conveniente tratar con la situación de múltiples v-models .

Entonces, ¿por qué no pasar el objeto modelo de un formulario directamente al componente secundario? De esta manera, no importa cuántas capas de componentes estén anidadas, la dirección se opera directamente y también es conveniente manejar la situación en la que un componente corresponde a múltiples campos.

Por supuesto, también hay un pequeño problema, debe pasar un atributo más para registrar el nombre del campo que operará el componente.

El tipo de accesorios del componente es de solo lectura superficial , es decir, el nivel raíz es de solo lectura, por lo que podemos modificar las propiedades del objeto pasado.

Embalaje básico

  • ref-modelo.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
    }
  })
}

También podemos usar computado para tránsito K extends keyof To para restricciones.

Implementación de anti-vibración

  • ref-modelo-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)
      }
    }
  })
}

En comparación, encontrará que los códigos son básicamente los mismos, excepto que los lugares donde se obtienen y asignan los valores son diferentes. Uno usa emit y el otro asigna valores directamente a los atributos del modelo .

Entonces, ¿se puede combinar en una sola función? Por supuesto, es solo que los parámetros no son fáciles de nombrar y necesita hacer juicios, lo que parece un poco difícil de leer, por lo que es mejor hacer dos funciones directamente.

Prefiero pasar directamente el objeto modelo , que es muy conciso.

Método de encapsulación de valor de rango (campo múltiple)

La fecha de inicio y la fecha de finalización se pueden dividir en dos controles, o se puede usar un control. Si se usa un control, implica conversión de tipo y correspondencia de campo.

Entonces podemos encapsular otra función.

  • ref-modelo-rango.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
    define una interfaz para restringir el tipo genérico T para model[col] que no se informe ningún error.

El tema de antivibración no se considera aquí, porque la mayoría de los casos no necesitan antivibración.

Instrucciones

Una vez que se completa la encapsulación, es muy conveniente usarlo en el componente, y solo se requiere una línea.

Primero cree un componente principal, cargue varios subcomponentes para una demostración.

  • js

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

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

emitir

Hacemos un subcomponente:

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

Defina props y emit , y luego llame a la función.
También es compatible con la configuración de secuencias de comandos :

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

Defina props , defina emit , luego llame a emitRef .

modelo

hacemos un subcomponente

  • 20-modelo.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>

Defina accesorios y llámelos.
Aunque hay un parámetro adicional que describe el nombre del campo, no es necesario definir y pasar emit.

valor de rango

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

Cuando el componente el-date-picker está en type="daterange" , el modelo v es una matriz, y la configuración de la base de datos de back-end es generalmente dos campos, como startDate y endDate, que deben enviarse en la forma de los objetos, por lo que debe estar en Convertir entre matrices y objetos.

Y el rangeRef que encapsulamos puede hacer tal conversión.

vergüenza transexual

Puede notar que el ejemplo anterior no usa el atributo colName , sino que pasa directamente los parámetros de la capa de caracteres.

Dado que TS solo puede realizar comprobaciones estáticas, no comprobaciones dinámicas, la escritura directa de cadenas es una forma estática y TS puede comprobarlas.

Sin embargo, si se usa el atributo colName , es un método dinámico.La verificación de TS no admite dinámicas y luego da directamente un mensaje de error.

Aunque puede funcionar normalmente, sigue siendo muy molesto mirar la línea roja, así que finalmente encapsulé una soledad.

Comparar

Compara artículos emitir modelo
tipo claro dificultad muy claro
parámetros (usados) uno dos
eficiencia Se requiere tránsito dentro de emit Modificar directamente usando la dirección del objeto
Dificultad de embalaje un pequeño problema fácil
utilizado en componentes Necesidad de definir emitir No es necesario definir emitir
Multicampo (encapsulación) No hay necesidad de embalaje por separado necesita ser empaquetado por separado
multicampo (usando) Necesidad de escribir varios modelos v No es necesario aumentar el número de parámetros.
Multicampo (formulario v-for) difícil de manejar fácil

Si quiere usar v-for para recorrer los subcomponentes en el formulario, obviamente el método del modelo es más fácil de implementar, porque no necesita escribir varios modelos v para un componente.

おすすめ

転載: blog.csdn.net/qq_48652579/article/details/130838128