Bootstrap

Vue.js 学习总结(17)—— Vue3 的 5 个组合式 API 方法详解

前言

在 Vue3 中,defineProps、defineEmits、defineExpose、defineOptions、defineSlots 是一组新的功能函数,用于定义组件的属性、事件、暴露、选项和插槽。

函数名称用途基本用法备注
defineProps定义组件的属性(props)const props = defineProps({...});提供了基本的属性和事件处理通信功能
defineEmits定义组件可以发出的事件const emit = defineEmits([...]);同上
defineExpose定义组件暴露给父组件的方法或属性defineExpose({...});提供了更高级的组件封装能力,适合需要高度自定义和复用的组件。
defineOptions在组合式 API 中设置组件的选项defineOptions({...});提供了在组合式 API 中设置组件选项的能力,适用于复杂组件或者需要大量配置的场景。
defineSlots定义和访问组件的插槽内容const slots = defineSlots();简化了插槽内容的处理,适合需要动态内容和复杂布局的组件。

使用示例与分析

1. defineProps:定义组件属性

在 Vue 3 中,通过 defineProps 函数可以声明和验证组件的属性,使得父子组件之间数据的传递更加安全和可控。适用于需要明确定义和验证输入属性的组件,特别是在使用 TypeScript 等类型系统时,能够提供良好的类型推断和验证。简化了属性定义和验证流程,但是对于简单的属性传递,可能显得有些繁琐,特别是在不需要严格验证的情况下。例如,假设我们有一个简单的计数器组件,它接收一个初始计数值作为属性:

<!-- Counter.vue -->
<template>
  <div>
    <p>{{ count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';

// 定义属性
const props = defineProps({
  initialCount: {
    type: Number,
    required: true
  }
});

// 使用属性
const count = ref(props.initialCount);

function increment() {
  count.value++;
}
</script>

父组件

<!-- Parent.vue -->
<template>
  <div>
    <Counter :initialCount="count" />
  </div>
</template>

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

const count = ref<number>(100);
</script>

2. defineEmits:定义组件事件

defineEmits 允许组件明确声明它可以触发的事件,提供了一种类型安全的事件处理机制,替代了传统的 $emit 方法。适用于需要明确定义和管理组件事件的场景,尤其是在复杂的组件通信和状态管理中尤为有用。例如,我们可以扩展我们的计数器组件,使其在计数值变化时发出事件,被父组件接收到。

<!-- Counter.vue -->
<template>
  <div>
    <p>{{ count }}</p>
    <button @click="handleIncrement">Increment</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';

// 定义属性
const props = defineProps({
  initialCount: {
    type: Number,
    required: true
  }
});

// 定义事件
const emit = defineEmits(['update:count']);

const count = ref(props.initialCount);

function handleIncrement() {
  count.value++;
  emit('update:count', count.value);
}
</script>

父组件

<!-- Parent.vue -->
<template>
  <div>
    <Counter :initialCount="count" @update:count="handleCountUpdate" />
  </div>
</template>

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

const count = ref<number>(100)

function handleCountUpdate(value: number) {
  console.log(value)
}
</script>

defineProps(定义组件属性)与defineEmits(定义组件事件)这两个组合式 API,是我们在 Vue3 组件通信常用的方法之一。

3. defineExpose:暴露给父组件的方法或属性

defineExpose 函数允许组件开发者明确定义哪些内部方法或属性可以被父组件访问和调用,增强了组件的封装性和复用性。适用于需要向外部暴露特定方法或属性的组件,使得父子组件之间的通信更加直接和有效。提高了组件的封装性和可复用性,但是可能会暴露过多的内部实现细节,所以使用时需要注意暴露方法。我们继续修改我们的计数器组件,使其暴露一个重置计数值的方法:

<!-- Counter.vue -->
<template>
  <div>
    <p>{{ count }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

<script setup>
import { ref } from 'vue';

// 定义属性
const props = defineProps({
  initialCount: {
    type: Number,
    required: true
  }
});

// 定义事件
const emit = defineEmits(['update:count']);

const count = ref(props.initialCount);

function increment() {
  count.value++;
  emit('update:count', count.value);
}

function resetCount() {
  count.value = props.initialCount;
}

// 暴露方法
defineExpose({
  reset: resetCount
});
</script>

现在,父组件可以通过引用调用子组件的 reset 方法:

<!-- Parent -->
<template>
  <Counter ref="counter" :initialCount="10" @update:count="handleCountUpdate" />
  <button @click="resetCounter">Reset Counter</button>
</template>

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

const counter = ref(null);

function handleCountUpdate(value) {
  console.log(value);
}

function resetCounter() {
  counter.value.reset();
}
</script>

「请注意:defineExpose[...] 定义的方法或者属性,由父组件来使用。但是,必须配合在父组件设置 ref 来使用。」

4. defineOptions:设置组件选项

defineOptions 函数允许在组合式 API 中设置组件的选项,如 namecomponents 和 directives,使得在组合式 API 中使用类似于选项式 API 的配置。适用于需要在组合式 API 中使用选项式 API 的配置方式的场景,尤其是对于复杂组件的配置和管理非常有帮助。提供了类似于选项式 API 的配置方式,增强了组件的配置灵活性和可扩展性。但是对于简单的组件可能显得过于复杂,不适合所有场景。

defineOptions({
  name: 'ComponentName',
  components: { ... },
  directives: { ... }
});

下面,我们来定义一个包含子组件和自定义指令的组件:

<!-- ParentComponent.vue -->
<template>
  <div v-focus>
    <p>{{ message }}</p>
    <ChildComponent />
  </div>
</template>

<script setup>
import ChildComponent from './ChildComponent.vue';

const message = ref('Hello from ParentComponent');

// 定义选项
defineOptions({
  name: 'ParentComponent',
  components: {
    ChildComponent
  },
  directives: {
    focus: {
      mounted(el) {
        el.focus();
      }
    }
  }
});
</script>

5. defineSlots:处理组件插槽

defineSlots 函数用于定义和访问组件的插槽内容,简化了在组合式 API 中处理动态内容和复杂布局的逻辑。适用于需要动态内容和复杂布局的组件,能够提升组件的灵活性和可重用性。简化了插槽内容的处理逻辑,但目前在实际应用中用到的场景相对较少,对于简单的插槽处理可能显得过于复杂。这里有一个例子,展示了如何在子组件中根据不同的插槽名和默认插槽,访问和处理多个插槽内容:

<!-- SlotComponent.vue -->
<template>
  <div>
    <div class="header">
      <slot name="header"></slot>
    </div>
    <div class="content">
      <slot></slot>
    </div>
    <div class="footer">
      <slot name="footer"></slot>
    </div>
  </div>
</template>

<script setup>
import { defineSlots } from 'vue';

const slots = defineSlots();
console.log('Header Slot content:', slots.header ? slots.header() : 'No header content');
console.log('Default Slot content:', slots.default ? slots.default() : 'No default content');
console.log('Footer Slot content:', slots.footer ? slots.footer() : 'No footer content');
</script>

在父组件中使用插槽:

<!-- ParentComponent.vue -->
<template>
  <div>
    <SlotComponent>
      <template #header>
        <h2>Header Slot</h2>
      </template>
      <template #footer>
        <p>Footer Slot</p>
      </template>
    </SlotComponent>
  </div>
</template>

<script setup>
import SlotComponent from './SlotComponent.vue';
</script>

适用场景总结

defineProps 和 defineEmits:这两者多配合使用,适用于简单组件和快速开发,因为它们提供了基本的属性和事件处理功能。

defineExpose:适用于高度封装和复用性要求,它提供了更高级的组件封装能力,可以将子组件的能力暴露出去给父组件。

defineOptions:适用于复杂组件和高级配置,它提供了在组合式 API 中设置组件选项的能力,适用于复杂组件或者需要大量配置的场景。

defineSlots 适用于插槽处理和动态内容,它简化了对于插槽内容的处理。

扩展

1. vue3 与 ESLint

在使用 Vue 3 和 ESLint 时,可能需要配置 ESLint 以识别 Vue 3 的组合式 API 函数。这可以通过在 ESLint 配置文件中添加全局变量声明来实现。以下是一个示例配置:

// .eslintrc.js
module.exports = {
  root: true,
  env: {
    node: true,
    browser: true,
    es2021: true
  },
  parser: 'vue-eslint-parser',
  extends: [
    'eslint:recommended',
    'plugin:vue/vue3-strongly-recommended'
    // 'standard'
  ],
  parserOptions: {
    ecmaVersion: 12,
    parser: '@typescript-eslint/parser',
    sourceType: 'module'
  },
  plugins: [
    'vue',
    '@typescript-eslint'
  ],
  rules: {
    quotes: [1, 'single'],
    'vue/script-setup-uses-vars': 0,
    'vue/no-multiple-template-root': 'off',
    'no-unused-vars': 0,
    camelcase: 0,
    'no-undef': 0,
    'vue/max-attributes-per-line': ['error', {
      singleline: {
        max: 3
      },
      multiline: {
        max: 3
      }
    }]
  },
  globals: {
    withDefaults: true,
    defineExpose: true,
    defineEmits: true,
    defineProps: true
  }
}

这种配置告诉 ESLint 这些函数是全局的,并且是只读的(即不能重新赋值)。这将防止 ESLint 报告未定义的全局变量错误。

2. withDefaults辅助函数(很重要)

建议在代码中,一定要配置 withDefaultswithDefaults 是一个辅助函数,可以帮助在定义组件属性时设置默认值,使得组件属性在没有传递值时有一个预设的默认值。这样可以简化代码,避免在每个属性上都手动设置默认值。而且由于 withDefaults 可以帮助定义属性的类型和默认值,使得 TypeScript 能够正确地推断和检查组件的属性使用。

const props = withDefaults(defineProps({
  message: String,
  count: { type: Number, default: 0 },
}));

或者

const props = withDefaults(defineProps<{
  message: string,
  count: number,
}>(), {
  message: 'hello',
  count: 0
})

结论

Vue 3 的组合式 API 为开发者提供了更多的灵活性和模块化能力。通过使用 definePropsdefineEmitsdefineExposedefineOptions 和 defineSlots,我们可以更加简洁和直观地定义和使用组件的属性、事件、暴露方法、选项和插槽。

;