Vue3: 二次封装组件的原则与方法

原则

  • 保持单一职责
    原则:每个组件应专注于完成一种特定功能,不要添加与组件主要功能无关的逻辑。
    举例:封装一个按钮组件时,应该专注于样式和事件绑定,而不是处理复杂的表单逻辑。
  • 尽量减少重复代码
    原则:通过封装复用逻辑,减少多处使用同样代码的情况。
    举例:可以对常用的输入框组件进行封装,将其通用功能提取为单一组件。
  • 提供清晰的接口
    原则:定义合理的 props 和 events,避免过多的参数,保持易用性和清晰度。
    举例:组件需要的所有配置项应通过 props 提供,事件通过 $emit 通知父组件。
  • 支持扩展性
    原则:为封装组件提供插槽、动态样式或回调接口,让开发者可以扩展功能。
    举例:提供 slot 和 props,使组件可以适应不同场景。
  • 遵循团队规范
    原则:封装组件时应符合团队的命名、代码风格和功能设计规范。
    举例:Vue 组件的 props 使用驼峰命名,事件使用 update:modelValue 风格

方法

  • 继承属性:使用 v-bind="$attrs" 来接收和传递父组件传递的属性。常用于向原组件传递未声明的属性。
  • 继承事件:使用 emits 配置声明事件,或者通过 $emit 进行事件的触发和传播。
  • 继承方法:使用 ref 来暴露内部方法给父组件,通过 defineExpose 明确暴露。
  • 继承插槽:使用 slots 对象来接收和转发插槽内容。
  • vue2与vue3的区别:在 vue3 中,取消了listeners这个组件实例的属性,将其事件的监听都整合到了attrs上,因此直接通过v-bind=$attrs属性就可以进行props属性和event事件的透传

在 vue2 中,需要用到 $slots(插槽) 和 $scopedSlots(作用域插槽)

在 vue3 中,取消了作用域插槽 $scopedSlots,将所有插槽都统一在 $slots 当中

v-model

大家应该都知道v-model只是一个语法糖,实际就是给组件定义了modelValue属性和监听update:modelValue事件,所以我们以前要实现数据双向绑定需要给子组件定义一个modelValue属性,并且在子组件内要更新modelValue值时需要emit出去一个update:modelValue事件,将新的值作为第二个字段传出去。

原因是因为从vue2开始就已经是单向数据流,在子组件中是不能直接修改props中的值。而是应该由子组件中抛出一个事件,由父组件去监听这个事件,然后去修改父组件中传递给props的变量。如果这里我们给input输入框直接加一个v-model="props.modelValue",那么其实是在子组件内直接修改props中的modelValue

如果父组件和子组件中都使用了 v-model,并且绑定的是同一个变量,这个时候就会出问题了,因为子组件直接更改了父组件的数据,违背了单向数据流,这样会导致如果出现数据问题不好调试,无法定位出现问题的根源。

第一种方法:将 v-model 拆开,通过 emit 让父组件去修改数据

第二种方法:使用计算属性的 get set 方法

但是如果子组件中有多个表单项,不管是上面哪种方法,都要写很多重复的代码

xml 复制代码
<!-- 父组件 -->
<template>
  <my-input v-model="formList"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formList = ref({
  text: '',
  password: '',
  name: ''
})
</script>

<!-- 子组件 -->
<template>
  <el-input v-model="name"></el-input>
  <el-input v-model="text"></el-input>
  <el-input v-model="password"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
  modelValue: {
    type: Object,
    default: () => {},
  }
});

const emit = defineEmits(['update:modelValue']);

const name = computed(() => {
  get() {
    return props.modelValue.name
  },
  set(val) {
    emit('update:modelValue', {
      ...props.modelValue,
      name: val
    })
  }
})

const text = computed(() => {
  get() {
    return props.modelValue.text
  },
  set(val) {
    emit('update:modelValue', {
      ...props.modelValue,
      text: val
    })
  }
})

const password = computed(() => {
  get() {
    return props.modelValue.password
  },
  set(val) {
    emit('update:modelValue', {
      ...props.modelValue,
      password: val
    })
  }
})
</script>

上面使用计算属性监听单个属性,所以需要每个属性都写一遍,我们可以考虑在计算属性中监听整个对象:

xml 复制代码
<!-- 父组件 -->
<template>
  <my-input v-model="formList"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formList = ref({
  text: '',
  password: '',
  name: ''
})
</script>

<!-- 子组件 -->
<template>
  <el-input v-model="modelList.name"></el-input>
  <el-input v-model="modelList.text"></el-input>
  <el-input v-model="modelList.password"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
  modelValue: {
    type: Object,
    default: () => {},
  }
});

const emit = defineEmits(['update:modelValue']);

const modelList = computed(() => {
  get() {
    return props.modelValue
  },
  set(val) {
    emit('update:modelValue', val)
  }
})
</script>

读取属性的时候能正常调用 get,但是设置属性的时候却无法触发 set,原因是 modelList.value = xxx,才会触发 set,而 modelList.value.name = xxx,无法触发。这个时候,Proxy 代理对象可以完美的解决这个问题:

xml 复制代码
<!-- 父组件 -->
<template>
  <my-input v-model="formList"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formList = ref({
  text: '',
  password: '',
  name: ''
})
</script>

<!-- 子组件 -->
<template>
  <el-input v-model="modelList.name"></el-input>
  <el-input v-model="modelList.text"></el-input>
  <el-input v-model="modelList.password"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
  modelValue: {
    type: Object,
    default: () => {},
  }
});

const emit = defineEmits(['update:modelValue']);

const modelList = computed(() => {
  get() {
    return new Proxy(props.modelValue, {
      get(target, key) {
        return Reflect.get(target, key)
      },
      set(target, key, value) {
        emit('update:modelValue',{
          ...target,
          [key]: value
        })
        return true
      }
    })
  },
  set(val) {
    emit('update:modelValue', val)
  }
})
</script>

我们还可以考虑把这段代码进行封装,可以在多处引入进行使用: useVModel.ts,其实 vueuse 里面有提供了这么一个方法,基本的逻辑是一样的。

useVModel

xml 复制代码
<!-- 父组件 -->
<template>
  <my-input v-model="formList"></my-input>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const formList = ref({
  text: '',
  password: '',
  name: ''
})
</script>

<!-- 子组件 -->
<template>
  <el-input v-model="modelList.name"></el-input>
  <el-input v-model="modelList.text"></el-input>
  <el-input v-model="modelList.password"></el-input>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { useVModel } from './useVModel.ts'

const props = defineProps({
  modelValue: {
    type: Object,
    default: () => {},
  }
});

const emit = defineEmits(['update:modelValue']);

const modelList = useVModel(props, 'modelValue', emit)
</script>

使用defineModel实现数据双向绑定

defineModel是一个宏,所以不需要从vue中import导入,直接使用就可以了。这个宏可以用来声明一个双向绑定 prop,通过父组件的 v-model 来使用

xml 复制代码
<template>
  <CommonInput v-model="inputValue" />
</template>

<script setup lang="ts">
import { ref } from "vue";

const inputValue = ref();
</script>
ini 复制代码
<template>
  <input v-model="model" />
</template>

<script setup lang="ts">
const model = defineModel();
model.value = "xxx";
</script>

在上面的例子中我们直接将defineModel的返回值使用v-model绑定到input输入框上面,无需定义 modelValue 属性和监听 update:modelValue 事件,代码更加简洁。defineModel的返回值是一个ref,我们可以在子组件中修改model变量的值,并且父组件中的inputValue变量的值也会同步更新,这样就可以实现双向绑定。

原理:defineModel其实就是在子组件内定义了一个叫model的ref变量和modelValue的props,并且watch了props中的modelValue。当props中的modelValue的值改变后会同步更新model变量的值。并且当在子组件内改变model变量的值后会抛出update:modelValue事件,父组件收到这个事件后就会更新父组件中对应的变量值。

例子

xml 复制代码
<template>
  <my-input v-model="inputValue" ref="myInputRef" :name="'lyw'" :year="18" @input="onInput">
    <!-- 动态插槽定义 -->
    <template #prefix>
      <span>Prefix Content</span>
    </template>
    <template #suffix="{ year }">
      <span>END: {{ year }}</span>
    </template>
    <template #default="{ name }">
      <p>Default Content: {{ name }}</p>
    </template>
  </my-input>
  <div>当前输入的值是:{{ inputValue }}</div>
</template>

<script lang="ts" setup>
import MyInput from '@/components/MyInput.vue'
import { onMounted } from 'vue'
import { ref } from 'vue'

const inputValue = ref('')
const myInputRef = ref()
function onInput(value: string) {
  console.log('Input value:', value)
}
onMounted(() => {
  // 光标聚焦
  // 避免ref 链式调用,比如 this.$refs.tableRef.$refs.table.clearSort()
  // 前提:子组件把方法暴露
  myInputRef.value.focus()
})
</script>
xml 复制代码
<template>
  <!-- 简化:直接使用 computed 绑定到 v-bind -->
  <el-input v-model="localValue" v-bind="$attrs" ref="inputRef">
    <!-- 动态插槽 -->
    <template v-for="(_slot, slotName) in $slots" #[slotName]="slotProps">
      <slot :name="slotName" v-bind="slotProps"></slot>
    </template>
  </el-input>
</template>

<script lang="ts" setup>
import { onMounted } from 'vue'
import { computed, ref } from 'vue'

// 定义 props 和 emits
const props = defineProps({
  modelValue: {
    type: String,
    default: ''
  }
})
const emit = defineEmits(['update:modelValue'])

// 使用 computed 来同步 props 和 emits
const localValue = computed({
  get: () => props.modelValue,
  set: (value) => emit('update:modelValue', value)
})
const inputRef = ref('')
let exposeObj: Record<string, any> = {}
const getMethod = () => {
  const entries = Object.entries(inputRef.value)
  for (const [method, fn] of entries) {
    exposeObj[method] = fn
  }
}
onMounted(getMethod)
defineExpose(exposeObj)
</script>
相关推荐
yqcoder18 分钟前
electron 打包后的 exe 文件,运行后是空白窗口
前端·javascript·electron
Jiaberrr22 分钟前
微信小程序:跨页面数据修改全攻略
前端·javascript·微信小程序·小程序
黑客KKKing40 分钟前
网安-HTML
前端·后端·学习·安全·html
小马哥编程43 分钟前
Word中如何格式化与网页和 HTML 内容相关的元素
前端·html
weitao_111 小时前
使用opencv.js 的时候报错 Uncaught 1022911432
前端·javascript
℡52Hz★1 小时前
Three.js+Vue3+Vite应用lil-GUI调试开发3D效果(三)
开发语言·前端·javascript·3d
kelly07212 小时前
移动端H5缓存问题
前端·缓存
小白也想学C2 小时前
ubuntu22.04:解决google chrome 访问百度页面加载慢的问题
前端·chrome
青木川崎2 小时前
java集合面试题
java·windows·面试
关山月3 小时前
使用 Tailwind CSS 和 JavaScript 构建延迟加载图片
前端