Vue3组件二次封装的小技巧

258 阅读1分钟

1. 使用 defineModel 来实现双向数据绑定

// Child.vue
<template>
  <div style="border: 2px solid black">
    <h2>Child.vue</h2>
    <div>{{ modelValue }}</div>
    <button @click="handleChange">修改</button>
  </div>
</template>

<script setup lang="ts">
import type { IUser } from "./Parent.vue";

const [modelValue, modelModifiers] = defineModel<IUser>({
  type: Object,
  default: {
    name: "--",
    age: "--",
  },
});

const handleChange = () => {
  modelValue.value.name = "李四";

  // ==> modelModifiers: { lazy: true }
  console.log("==> modelModifiers:", modelModifiers);
};
</script>

<style lang="scss" scoped></style>
// Parent.vue
<template>
  <div style="border: 2px solid black; box-sizing: border-box; padding: 30px">
    <h2>Parent.vue</h2>
    <div>{{ user }}</div>
    <Child v-model.lazy="user" />
  </div>
</template>

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

export interface IUser {
  name: string;
  age: number;
}

const user = ref<IUser>({
  name: "张三",
  age: 18,
});
</script>

<style lang="scss" scoped></style>
// index.vue
<template>
  <Parent />
</template>

<script setup lang="ts">
import Parent from "./Parent.vue";
</script>

<style lang="scss" scoped></style>

2. 使用 defineExpose + Proxy 获取子组件的 ref

// MyInput.vue
<template>
  <div class="myInput">
    <component :is="h(ElInput, $attrs, $slots)" ref="inputRef" />
  </div>
</template>

<script setup lang="ts">
import { h, ref } from "vue";
import { ElInput } from "element-plus";

const inputRef = ref();

defineExpose(
  new Proxy(
    {},
    {
      get(_target, prop) {
        return inputRef.value?.[prop];
      },
      has(_target, prop) {
        return prop in inputRef.value;
      },
    }
  )
);
</script>

<style lang="scss" scoped></style>

// index.vue
<template>
  <div class="">
    <p>
      <MyInputPlus ref="inputPlusRef" />
    </p>
    <button @click="handleSetInputFocus">set input focus</button>
    <button @click="handleSetInputBlur">set input blur</button>
  </div>
</template>

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

const inputPlusRef = ref();

const handleSetInputFocus = () => {
  inputPlusRef.value.focus?.();
};
const handleSetInputBlur = () => {
  inputPlusRef.value.blur?.();
};
</script>

<style lang="scss" scoped></style>

3. 使用 h 函数透传 $slots

// MyInput.vue
<template>
  <div class="myInput">
    <div class="prefix">
      <slot name="prefix"> left </slot>
    </div>
    <input type="text" placeholder="TestTransferSlots" />
    <div class="suffix">
      <slot name="suffix"> right </slot>
    </div>
  </div>
</template>

<script setup lang="ts"></script>

<style lang="scss" scoped>
.myInput {
  display: flex;
  gap: 5px;
}
</style>

// MyInputPlus.vue
<template>
  <div class="">
    <component :is="h(MyInput, $attrs, $slots)" />
  </div>
</template>

<script setup lang="ts">
import { h, ref } from "vue";
import MyInput from "./MyInput.vue";
</script>

<style lang="scss" scoped></style>

// index.vue
<template>
  <div>
    <MyInputPlus>
      <template #prefix> + </template>
      <template #suffix> - </template>
    </MyInputPlus>
  </div>
</template>

<script setup lang="ts">
import MyInputPlus from "./MyInputPlus.vue";
</script>

<style lang="scss" scoped></style>

4. 使用 @vue:xxx 监听子组件生命周期

// Child.vue
<template>
  <div class="">Child</div>
</template>

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

onMounted(async () => {
  console.log("==> Child onMounted");
});
</script>

<style lang="scss" scoped></style>
// Parent.vue
<template>
  <div class="">
    <Child @vue:mounted="handleChildMounted" />
  </div>
</template>

<script setup lang="ts">
import Child from "./Child.vue";

const handleChildMounted = () => {
  console.log("==> [Parent]:Child mounted");
};
</script>

<style lang="scss" scoped></style>
// index.vue
<template>
  <div class="">
    <Parent />
  </div>
</template>

<script setup lang="ts">
import Parent from "./Parent.vue";
</script>

<style lang="scss" scoped></style>