组件封装 1 组件的二次封装

Huy大约 3 分钟javascriptjavascript

本文旨在总结在 vue 中的封装基础逻辑思路。

组件的二次封装

在 Vue 中,对组件进行二次封装主要涉及三个方面:属性(Attributes)、插槽(Slots)以及引用(Ref)的封装。

1. 属性的封装

利用 Vue 的 $attrs,可以轻松获取父组件传递的所有属性,通过使用 v-bind,可以将所有属性绑定到子组件上,无需逐个添加。

<template>
  <BaseComponent v-bind="$attrs" />
</template>

<script>
export default {}
</script>

2. 插槽的封装

类似于属性的处理方式,利用 $slots 属性获取父组件传递的插槽。通过循环遍历插槽,可以对它们进行二次封装。

<template>
  <BaseComponent v-bind="$attrs">
    <template v-for="(value, name) in $slots" #[name]="slotData">
      <slot :name="name" v-bind="slotData || {}"></slot>
    </template>
  </BaseComponent>
</template>

<script>
export default {}
</script>

3. 引用的封装

引用的封装相对较为复杂。使用 ref 的目的是为了获取子组件内部的方法和数据,虽然这并不是推荐的做法,但也可作为备选方案。

在这里,我们将封装组件中的所有 ref 属性值提取到外部,从而可以访问原组件中的方法和数据。

<template>
  <BaseComponent ref="refCom" v-bind="$attrs">
    <template v-for="(value, name) in $slots" #[name]="slotData">
      <slot :name="name" v-bind="slotData || {}"></slot>
    </template>
  </BaseComponent>
</template>

<script>
export default {
  mounted() {
    const entries = Object.entries(this.$refs.refCom)
    for (const [key, value] of entries) {
      this[key] = value
    }
  },
}
</script>

通过这三个方面的封装,可以更灵活地使用和定制 Vue 组件,提高代码的可维护性和复用性。

封装组件的传参

在封装组件时,常会用到组件传参,但是参数可能会在子组件中改变。通常的办法是采用 v-model,但是在二次封装组件时,这样会打破单向数据流的传递。此时我们可以采用 computed 计算属性进行传参。

先看原本的代码:

<!-- 父组件 -->
<template>
  <div>
    <searchBar v-model="searchData" />
  </div>
</template>

<script setup>
import { ref } from 'vue'
import searchBar from './searchBar.vue'
const searchData = ref({
  keyword: '',
  options: [
    { label: 'label1', value: 'label1' },
    { label: 'label2', value: 'label2' },
  ],
  selectedValue: 'select',
})
</script>
<!-- 子组件 -->
<template>
  <el-input v-model="modelValue.keyword">
    <template #prepend>
      <el-select v-model="modelValue.selectedValue">
        <el-option
          v-for="item in modelValue.options"
          :key="item.value"
          :label="item.label"
          :value="item.value"
        />
      </el-select>
    </template>
  </el-input>
</template>

<script setup>
const props = defineProps({
  modelValue: {
    type: Object,
    required: true,
  },
})
</script>

初步处理:先将 v-model 拆分为 modelValue 数据绑定和 @update:modelValue 数据更新事件。

<template>
  <el-input
    :modelValue="modelValue.keyword"
    @update:modelValue="handleKeywordChange"
  >
    <template #prepend>
      <el-select v-model="modelValue.selectedValue">
        <el-option
          v-for="item in modelValue.options"
          :key="item.value"
          :label="item.label"
          :value="item.value"
        />
      </el-select>
    </template>
  </el-input>
</template>

<script setup>
import { computed } from 'vue'
const props = defineProps({
  modelValue: {
    type: Object,
    required: true,
  },
})

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

const handleKeywordChange = (value) => {
  emit('update:modelValue', {
    ...props.modelValue,
    keyword: value,
  })
}
</script>

改进处理: 利用 computed 计算数据对父组件传递过来的值进行包装,利用 get 和 set 进行传递。此时依旧改用 v-model 进行绑定 computed 的计算值。

<template>
  <el-input v-model="keyword">
    <template #prepend>
      <el-select v-model="modelValue.selectedValue">
        <el-option
          v-for="item in modelValue.options"
          :key="item.value"
          :label="item.label"
          :value="item.value"
        />
      </el-select>
    </template>
  </el-input>
</template>

<script setup>
import { computed } from 'vue'
const props = defineProps({
  modelValue: {
    type: Object,
    required: true,
  },
})

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

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

但是这样有一个缺点,即所有的属性都要用 computed 进行一次转换,过于繁琐。因此,可以采用 ES6 中 Proxy 对象进行代理。

<template>
  <el-input v-model="model.keyword">
    <template #prepend>
      <el-select v-model="model.selectedValue">
        <el-option
          v-for="item in model.options"
          :key="item.value"
          :label="item.label"
          :value="item.value"
        />
      </el-select>
    </template>
  </el-input>
</template>

<script setup>
import { computed } from 'vue'
const props = defineProps({
  modelValue: {
    type: Object,
    required: true,
  },
})

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

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

进一步封装:

import { computed } from 'vue'
export function useModel(props, propName, emit) {
  return computed({
    get() {
      return new Proxy(props[propName], {
        set(obj, name, val) {
          emit(`update:${propName}`, {
            ...obj,
            [name]: val,
          })
          return true
        },
      })
    },
    set(value) {
      emit(`update:${propName}`, value)
    },
  })
}
Loading...