Vue 3 组合式 API 中的 Slots 与 Attrs 实践
·
Vue 3 组合式 API 中的 Slots 与 Attrs 实践
文章目录
1. 概述
在 Vue 3 的组合式 API 中,可以通过 useSlots() 和 useAttrs() 来访问组件的插槽和属性。
2. 使用 useSlots()
2.1 基本用法
<!-- SlotComponent.vue -->
<template>
<div class="slot-container">
<div class="header">
<slot name="header">
<h2>默认标题</h2>
</slot>
</div>
<div class="content">
<slot>
<p>默认内容</p>
</slot>
</div>
<div class="footer">
<slot name="footer">
<p>默认页脚</p>
</slot>
</div>
</div>
</template>
<script setup lang="ts">
import { useSlots, computed } from 'vue'
// 获取插槽对象
const slots = useSlots()
// 检查插槽是否存在
const hasHeaderSlot = computed(() => !!slots.header)
const hasDefaultSlot = computed(() => !!slots.default)
const hasFooterSlot = computed(() => !!slots.footer)
// 动态渲染逻辑
console.log('Header slot exists:', hasHeaderSlot.value)
console.log('Default slot exists:', hasDefaultSlot.value)
console.log('Footer slot exists:', hasFooterSlot.value)
</script>
<style scoped>
.slot-container {
border: 1px solid #ccc;
padding: 20px;
margin: 10px 0;
}
.header {
background: #f0f0f0;
padding: 10px;
}
.footer {
background: #f9f9f9;
padding: 10px;
margin-top: 10px;
}
</style>
<!-- ParentComponent.vue -->
<template>
<div>
<SlotComponent>
<template #header>
<h2>自定义标题</h2>
</template>
<p>这是自定义内容</p>
<template #footer>
<button @click="handleClick">自定义按钮</button>
</template>
</SlotComponent>
</div>
</template>
<script setup lang="ts">
import SlotComponent from './SlotComponent.vue'
const handleClick = () => {
console.log('按钮被点击')
}
</script>
2.2 动态插槽处理
<!-- DynamicSlotComponent.vue -->
<template>
<div class="dynamic-slot">
<div v-for="slotName in availableSlots" :key="slotName">
<div v-if="slots[slotName]" class="slot-item">
<h4>插槽: {{ slotName }}</h4>
<slot :name="slotName"></slot>
</div>
</div>
<!-- 处理动态插槽 -->
<div v-if="hasDynamicSlots" class="dynamic-slots">
<h4>动态插槽:</h4>
<div
v-for="slotName in dynamicSlotNames"
:key="slotName"
class="dynamic-slot-item"
>
<slot :name="slotName"></slot>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useSlots, computed, ref } from 'vue'
const slots = useSlots()
// 预定义的插槽名称
const predefinedSlots = ['header', 'content', 'footer', 'sidebar']
// 可用的插槽
const availableSlots = computed(() => {
return predefinedSlots.filter(slotName => !!slots[slotName])
})
// 动态插槽(非预定义的)
const dynamicSlotNames = computed(() => {
return Object.keys(slots).filter(
slotName => !predefinedSlots.includes(slotName) && slotName !== 'default'
)
})
const hasDynamicSlots = computed(() => dynamicSlotNames.value.length > 0)
// 调试信息
console.log('所有插槽:', Object.keys(slots))
console.log('可用插槽:', availableSlots.value)
console.log('动态插槽:', dynamicSlotNames.value)
</script>
<style scoped>
.dynamic-slot {
border: 2px dashed #007bff;
padding: 15px;
margin: 10px 0;
}
.slot-item {
border: 1px solid #28a745;
padding: 10px;
margin: 5px 0;
background: #f8fff9;
}
.dynamic-slots {
border: 1px solid #ffc107;
padding: 10px;
margin-top: 10px;
background: #fffef0;
}
.dynamic-slot-item {
border: 1px dashed #ffc107;
padding: 8px;
margin: 5px 0;
}
</style>
<!-- ParentDynamicComponent.vue -->
<template>
<div>
<DynamicSlotComponent>
<template #header>
<h3>头部内容</h3>
</template>
<template #content>
<p>主要内容区域</p>
</template>
<template #customSection>
<div style="color: red;">自定义区域内容</div>
</template>
<template #anotherCustom>
<button>另一个自定义按钮</button>
</template>
</DynamicSlotComponent>
</div>
</template>
<script setup lang="ts">
import DynamicSlotComponent from './DynamicSlotComponent.vue'
</script>
3. 使用 useAttrs()
3.1 基本属性处理
<!-- AttrsComponent.vue -->
<template>
<div class="attrs-container" v-bind="filteredAttrs">
<h3>属性组件</h3>
<div class="attrs-display">
<h4>接收到的属性:</h4>
<pre>{{ JSON.stringify(attrs, null, 2) }}</pre>
</div>
<div class="filtered-attrs">
<h4>过滤后的属性:</h4>
<pre>{{ JSON.stringify(filteredAttrs, null, 2) }}</pre>
</div>
<!-- 手动绑定特定属性 -->
<input
:value="inputValue"
v-bind="inputAttrs"
@input="handleInput"
/>
</div>
</template>
<script setup lang="ts">
import { useAttrs, computed, ref } from 'vue'
// 获取所有属性(包括 class 和 style)
const attrs = useAttrs()
const inputValue = ref('')
// 过滤掉不需要的属性
const filteredAttrs = computed(() => {
const { class: className, style, ...rest } = attrs
return rest
})
// 专门处理输入框属性
const inputAttrs = computed(() => {
return {
placeholder: attrs.placeholder || '默认占位符',
type: attrs.type || 'text',
disabled: attrs.disabled || false
}
})
const handleInput = (event: Event) => {
inputValue.value = (event.target as HTMLInputElement).value
console.log('输入值:', inputValue.value)
}
// 监听属性变化
console.log('组件属性:', attrs)
</script>
<style scoped>
.attrs-container {
border: 2px solid #6f42c1;
padding: 20px;
margin: 10px 0;
background: #f8f9fa;
}
.attrs-display, .filtered-attrs {
margin: 15px 0;
padding: 10px;
background: white;
border: 1px solid #dee2e6;
}
pre {
background: #f8f9fa;
padding: 10px;
border-radius: 4px;
font-size: 12px;
}
input {
width: 100%;
padding: 8px;
margin-top: 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
</style>
<!-- ParentAttrsComponent.vue -->
<template>
<div>
<AttrsComponent
id="custom-id"
class="custom-class"
style="color: blue;"
data-test="test-data"
placeholder="请输入内容"
type="password"
:disabled="isDisabled"
custom-attr="自定义属性"
@custom-event="handleCustomEvent"
/>
<button @click="isDisabled = !isDisabled">
{{ isDisabled ? '启用' : '禁用' }}输入框
</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import AttrsComponent from './AttrsComponent.vue'
const isDisabled = ref(false)
const handleCustomEvent = (event: any) => {
console.log('自定义事件:', event)
}
</script>
3.2 属性继承控制
<!-- InheritAttrsComponent.vue -->
<template>
<div class="inherit-container">
<h3>属性继承控制</h3>
<!-- 手动控制属性继承 -->
<div class="with-attrs" v-bind="filteredAttrs">
这个元素继承了过滤后的属性
</div>
<!-- 不继承属性的元素 -->
<div class="without-attrs">
这个元素没有继承属性
</div>
<!-- 传递属性到子组件 -->
<ChildComponent v-bind="childAttrs" />
</div>
</template>
<script setup lang="ts">
import { useAttrs, computed } from 'vue'
import ChildComponent from './ChildComponent.vue'
// 禁用默认的属性继承
defineOptions({
inheritAttrs: false
})
const attrs = useAttrs()
// 过滤出需要的属性
const filteredAttrs = computed(() => {
const { class: _, style: __, ...rest } = attrs
return {
...rest,
title: '自定义标题'
}
})
// 传递给子组件的属性
const childAttrs = computed(() => {
return {
'data-parent': '来自父组件',
...filteredAttrs.value
}
})
console.log('所有属性:', attrs)
</script>
<style scoped>
.inherit-container {
border: 2px solid #e83e8c;
padding: 20px;
margin: 10px 0;
}
.with-attrs {
background: #d1ecf1;
padding: 10px;
margin: 10px 0;
border: 1px solid #bee5eb;
}
.without-attrs {
background: #f8d7da;
padding: 10px;
margin: 10px 0;
border: 1px solid #f5c6cb;
}
</style>
<!-- ChildComponent.vue -->
<template>
<div class="child-component" v-bind="attrs">
<h4>子组件</h4>
<pre>子组件属性: {{ JSON.stringify(attrs, null, 2) }}</pre>
</div>
</template>
<script setup lang="ts">
import { useAttrs } from 'vue'
const attrs = useAttrs()
</script>
<style scoped>
.child-component {
background: #fff3cd;
padding: 10px;
margin: 10px 0;
border: 1px solid #ffeaa7;
}
</style>
4. Slots 和 Attrs 结合使用
4.1 高级组件模式
<!-- AdvancedComponent.vue -->
<template>
<div class="advanced-component" v-bind="containerAttrs">
<!-- 动态渲染插槽 -->
<div class="slot-renderer">
<div
v-for="slotName in renderableSlots"
:key="slotName"
:class="`slot-${slotName}`"
>
<slot :name="slotName"></slot>
</div>
</div>
<!-- 默认插槽 -->
<div v-if="hasDefaultSlot" class="default-slot">
<slot></slot>
</div>
<!-- 基于属性的条件渲染 -->
<div v-if="showConditional" class="conditional-content">
<h4>条件内容</h4>
<p>这个内容基于属性显示: {{ attrs.condition }}</p>
</div>
</div>
</template>
<script setup lang="ts">
import { useSlots, useAttrs, computed } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
// 可渲染的插槽
const renderableSlots = computed(() => {
return Object.keys(slots).filter(slotName => slotName !== 'default')
})
// 检查默认插槽
const hasDefaultSlot = computed(() => !!slots.default)
// 基于属性的条件
const showConditional = computed(() => {
return attrs.condition !== undefined && attrs.condition !== 'false'
})
// 容器属性
const containerAttrs = computed(() => {
const { class: className, style, ...rest } = attrs
return {
...rest,
'data-advanced': 'true',
'data-slots-count': renderableSlots.value.length
}
})
// 调试信息
console.log('高级组件 - 插槽:', Object.keys(slots))
console.log('高级组件 - 属性:', attrs)
</script>
<style scoped>
.advanced-component {
border: 3px solid #20c997;
padding: 20px;
margin: 15px 0;
background: #f0fdf4;
}
.slot-renderer {
display: grid;
gap: 10px;
margin-bottom: 15px;
}
.default-slot {
background: #e7f3ff;
padding: 15px;
border: 1px solid #b3d9ff;
}
.conditional-content {
background: #fff3cd;
padding: 10px;
margin-top: 10px;
border: 1px solid #ffeaa7;
}
</style>
<!-- ParentAdvancedComponent.vue -->
<template>
<div>
<AdvancedComponent
id="advanced-demo"
class="custom-advanced"
style="font-size: 16px;"
condition="show"
data-version="1.0"
:custom-prop="dynamicValue"
>
<template #header>
<h2>高级头部</h2>
</template>
<template #content>
<p>这是通过插槽传递的内容</p>
</template>
<template #actions>
<button @click="handleAction">操作按钮</button>
</template>
<p>这是默认插槽的内容</p>
</AdvancedComponent>
<button @click="dynamicValue = !dynamicValue">
切换属性: {{ dynamicValue }}
</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import AdvancedComponent from './AdvancedComponent.vue'
const dynamicValue = ref(true)
const handleAction = () => {
console.log('操作按钮被点击')
}
</script>
5. 实际应用场景
5.1 表单字段组件
<!-- FormField.vue -->
<template>
<div class="form-field" v-bind="containerAttrs">
<label v-if="label || slots.label" class="form-label">
<slot name="label">{{ label }}</slot>
</label>
<div class="input-container">
<slot name="prepend"></slot>
<input
v-bind="inputAttrs"
:value="modelValue"
@input="handleInput"
@blur="handleBlur"
class="form-input"
/>
<slot name="append"></slot>
</div>
<div v-if="error || slots.error" class="error-message">
<slot name="error">{{ error }}</slot>
</div>
<div v-if="slots.help" class="help-text">
<slot name="help"></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { useSlots, useAttrs, computed } from 'vue'
interface Props {
modelValue?: string
label?: string
error?: string
}
const props = defineProps<Props>()
const emit = defineEmits<{
'update:modelValue': [value: string]
'blur': []
}>()
const slots = useSlots()
const attrs = useAttrs()
// 输入框属性
const inputAttrs = computed(() => {
const { class: _, style: __, label, error, ...inputProps } = attrs
return {
type: 'text',
...inputProps
}
})
// 容器属性
const containerAttrs = computed(() => {
return {
class: [
'form-field-wrapper',
attrs.class,
props.error ? 'has-error' : ''
].filter(Boolean).join(' '),
style: attrs.style
}
})
const handleInput = (event: Event) => {
const value = (event.target as HTMLInputElement).value
emit('update:modelValue', value)
}
const handleBlur = () => {
emit('blur')
}
</script>
<style scoped>
.form-field {
margin-bottom: 1rem;
}
.form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
}
.input-container {
display: flex;
align-items: center;
gap: 0.5rem;
}
.form-input {
flex: 1;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
}
.form-input:focus {
outline: none;
border-color: #007bff;
}
.error-message {
color: #dc3545;
font-size: 0.875rem;
margin-top: 0.25rem;
}
.help-text {
color: #6c757d;
font-size: 0.875rem;
margin-top: 0.25rem;
}
.has-error .form-input {
border-color: #dc3545;
}
</style>
<!-- FormDemo.vue -->
<template>
<div class="form-demo">
<FormField
v-model="username"
label="用户名"
placeholder="请输入用户名"
required
class="custom-field"
>
<template #prepend>
<span>👤</span>
</template>
</FormField>
<FormField
v-model="email"
label="邮箱"
type="email"
:error="emailError"
>
<template #label>
<span style="color: red;">*</span>
邮箱地址
</template>
<template #help>
<p>请输入有效的邮箱地址</p>
</template>
</FormField>
<FormField
v-model="password"
label="密码"
type="password"
>
<template #append>
<button type="button" @click="togglePassword">
{{ showPassword ? '隐藏' : '显示' }}
</button>
</template>
</FormField>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import FormField from './FormField.vue'
const username = ref('')
const email = ref('')
const password = ref('')
const showPassword = ref(false)
const emailError = computed(() => {
if (!email.value) return '邮箱不能为空'
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value)) {
return '邮箱格式不正确'
}
return ''
})
const togglePassword = () => {
showPassword.value = !showPassword.value
}
</script>
6. 最佳实践和注意事项
6.1 性能考虑
<script setup lang="ts">
import { useSlots, useAttrs, computed, watchEffect } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
// 好的做法:使用 computed
const hasHeaderSlot = computed(() => !!slots.header)
// 不好的做法:在模板中直接调用
// {{ !!slots.header }}
// 监听属性变化(谨慎使用)
watchEffect(() => {
// 只在必要时监听
if (attrs.disabled) {
console.log('组件被禁用')
}
})
// 属性过滤的最佳实践
const inputAttrs = computed(() => {
// 明确列出需要的属性
const {
type = 'text',
placeholder = '',
disabled = false,
...rest
} = attrs
// 过滤掉不需要传递的属性
const { class: _, style: __, ...filteredRest } = rest
return {
type,
placeholder,
disabled,
...filteredRest
}
})
</script>
6.2 类型安全
<script setup lang="ts">
import { useSlots, useAttrs } from 'vue'
// 为插槽提供类型
interface ComponentSlots {
header?: (props: { title: string }) => any
default?: (props: { data: any }) => any
footer?: () => any
}
// 为属性提供类型
interface ComponentAttrs {
id?: string
class?: string
style?: string
'data-test'?: string
[key: string]: any
}
// 在实际使用中,Vue 会自动推断类型
const slots = useSlots()
const attrs = useAttrs()
// 类型安全的插槽检查
const hasTypedSlot = (slotName: keyof ComponentSlots): boolean => {
return !!slots[slotName]
}
// 类型安全的属性访问
const getSafeAttr = (key: keyof ComponentAttrs) => {
return attrs[key]
}
</script>
7. 总结
useSlots() 主要用途:
- 检查插槽存在性:动态决定是否渲染某些内容
- 条件渲染:基于插槽存在性进行条件渲染
- 动态插槽处理:处理未知名称的插槽
- 插槽组合:创建复杂的插槽组合逻辑
useAttrs() 主要用途:
- 属性继承控制:精确控制属性的继承行为
- 属性过滤:过滤和转换传入的属性
- 属性传递:将属性传递给子元素或组件
- 动态属性绑定:基于属性值进行条件渲染
最佳实践:
- 明确继承:使用
inheritAttrs: false明确控制属性继承 - 性能优化:使用 computed 包装 slots 和 attrs 的访问
- 类型安全:为复杂的插槽和属性场景提供 TypeScript 类型
- 适度使用:不要过度使用,只在真正需要时访问 slots 和 attrs
通过合理使用 useSlots() 和 useAttrs(),可以创建更加灵活和可复用的 Vue 组件。
更多推荐
所有评论(0)