Vue3 Composition API中的
useSlots和useAttrs是处理组件插槽和属性的关键工具。
useSlots用于访问父组件传递的插槽内容,支持检查插槽存在性、动态处理插槽内容及类型安全;
useAttrs用于获取非props属性,可实现属性继承控制、过滤合并及手动绑定。
两者结合可创建高阶组件、表单包装器等灵活组件,但需注意性能优化和类型安全。
最佳实践包括:
- 明确属性继承策略
- 分离UI/功能属性
- 智能处理插槽内容
- 配合TypeScript实现类型安全
useSlots 和 useAttrs 深度解析
这两个是 Vue 3 Composition API 中的实用工具函数,专门用于在 <script setup> 中访问插槽 和非 props 属性。
1. useSlots - 访问插槽内容
基本概念
插槽(Slots)允许父组件向子组件传递模板内容。
基本用法
vue
javascript
<!-- ChildComponent.vue -->
<script setup>
import { useSlots } from 'vue'
// 获取插槽对象
const slots = useSlots()
// 检查插槽是否存在
const hasDefaultSlot = !!slots.default
const hasHeaderSlot = !!slots.header
const hasFooterSlot = !!slots.footer
// 访问插槽内容
console.log(slots.default) // 默认插槽的 VNode 数组
console.log(slots.header) // 具名插槽的 VNode 数组
</script>
<template>
<div class="container">
<!-- 条件渲染具名插槽 -->
<header v-if="hasHeaderSlot">
<slot name="header"></slot>
</header>
<!-- 默认插槽 -->
<main>
<slot></slot>
</main>
<!-- 带作用域的插槽 -->
<footer v-if="hasFooterSlot">
<slot name="footer" :data="footerData"></slot>
</footer>
</div>
</template>
父组件使用示例
vue
javascript
<!-- ParentComponent.vue -->
<template>
<ChildComponent>
<!-- 默认插槽内容 -->
<p>这是默认插槽内容</p>
<!-- 具名插槽 -->
<template #header>
<h1>页面标题</h1>
</template>
<!-- 作用域插槽 -->
<template #footer="{ data }">
<p>页脚:{{ data.message }}</p>
</template>
</ChildComponent>
</template>
高级用法:动态插槽
vue
javascript
<!-- DynamicSlotComponent.vue -->
<script setup>
import { useSlots, computed } from 'vue'
const slots = useSlots()
// 动态处理插槽内容
const processedSlots = computed(() => {
const result = {}
// 遍历所有插槽
for (const [name, slotFn] of Object.entries(slots)) {
if (slotFn) {
// 处理插槽内容(例如添加包装)
result[name] = () => {
const vnodes = slotFn()
return vnodes.map(vnode => {
// 为每个节点添加特定类名
return h('div', { class: 'slot-wrapper' }, [vnode])
})
}
}
}
return result
})
</script>
<template>
<!-- 动态渲染处理后的插槽 -->
<slot v-if="processedSlots.default" :name="processedSlots.default"></slot>
<!-- 或者使用渲染函数 -->
<component
v-for="(slotFn, name) in processedSlots"
:key="name"
:is="slotFn"
/>
</template>
插槽类型检查(TypeScript)
vue
javascript
<script setup lang="ts">
import { useSlots, Slots } from 'vue'
// 定义插槽类型
interface ComponentSlots {
default?: () => any
header?: (props: { title: string }) => any
footer?: (props: { year: number }) => any
}
// 类型断言
const slots = useSlots() as unknown as ComponentSlots
// 现在有类型提示
if (slots.header) {
// slots.header() 需要传递正确的 props
const headerContent = slots.header({ title: 'Hello' })
}
</script>
2. useAttrs - 访问非 props 属性
基本概念
attrs 包含所有传递给组件的非 props 属性(class、style、自定义属性、事件监听器等)。
基本用法
vue
javascript
<!-- ButtonComponent.vue -->
<script setup>
import { useAttrs } from 'vue'
// 获取属性对象
const attrs = useAttrs()
console.log(attrs)
// 可能包含:
// {
// class: 'btn-primary',
// style: { color: 'red' },
// id: 'submit-btn',
// 'data-test': 'button',
// onClick: fn
// }
</script>
<template>
<!-- 手动绑定所有属性 -->
<button v-bind="attrs">
<slot></slot>
</button>
</template>
属性继承控制
默认情况下,Vue 会自动继承非 props 属性到根元素。可以通过选项关闭:
vue
javascript
<script>
// 关闭自动属性继承
export default {
inheritAttrs: false
}
</script>
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
// 现在需要手动处理属性绑定
</script>
<template>
<div class="wrapper">
<!-- 只绑定特定属性到内部元素 -->
<button :class="attrs.class" :style="attrs.style">
<slot></slot>
</button>
<!-- 其他属性绑定到其他地方 -->
<span v-bind="filteredAttrs"></span>
</div>
</template>
属性过滤和处理
vue
javascript
<script setup>
import { useAttrs, computed } from 'vue'
const attrs = useAttrs()
// 1. 分离 class 和 style
const { class: className, style, ...otherAttrs } = attrs
// 2. 计算属性过滤
const buttonAttrs = computed(() => {
const result = { ...otherAttrs }
// 移除特定属性
delete result['data-internal']
// 添加额外属性
result['data-component'] = 'custom-button'
return result
})
// 3. 合并 class
const mergedClass = computed(() => {
const baseClass = 'btn'
return [baseClass, className].filter(Boolean).join(' ')
})
// 4. 监听属性变化
import { watchEffect } from 'vue'
watchEffect(() => {
console.log('属性变化:', attrs)
})
</script>
<template>
<button
:class="mergedClass"
:style="style"
v-bind="buttonAttrs"
>
<slot></slot>
</button>
</template>
useSlots 和 useAttrs 的实际应用场景
场景1:高阶组件/包装组件
vue
javascript
<!-- WithLoading.vue - 加载状态包装器 -->
<script setup>
import { useSlots, useAttrs } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
const props = defineProps({
loading: Boolean,
loadingText: {
type: String,
default: '加载中...'
}
})
</script>
<template>
<div class="with-loading" v-bind="attrs">
<div v-if="props.loading" class="loading-overlay">
{{ props.loadingText }}
</div>
<!-- 透传所有插槽 -->
<slot v-if="slots.default" name="default"></slot>
<slot v-if="slots.header" name="header"></slot>
<slot v-if="slots.footer" name="footer"></slot>
</div>
</template>
场景2:表单字段包装器
vue
javascript
<!-- FormField.vue -->
<script setup>
import { useAttrs, computed } from 'vue'
const attrs = useAttrs()
const props = defineProps({
label: String,
error: String
})
// 提取输入框相关属性
const inputAttrs = computed(() => {
const { class: _, style: __, label, error, ...inputProps } = attrs
return inputProps
})
// 提取容器属性
const wrapperAttrs = computed(() => {
const { class: wrapperClass, style: wrapperStyle } = attrs
return { class: wrapperClass, style: wrapperStyle }
})
</script>
<template>
<div class="form-field" v-bind="wrapperAttrs">
<label v-if="props.label">{{ props.label }}</label>
<!-- 绑定所有输入相关属性到 input -->
<input v-bind="inputAttrs" />
<div v-if="props.error" class="error-message">
{{ props.error }}
</div>
<!-- 其他插槽内容 -->
<slot></slot>
</div>
</template>
场景3:组件透传
vue
javascript
<!-- TransparentWrapper.vue -->
<script setup>
import { useSlots, useAttrs } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
// 不定义任何 props,完全透传
</script>
<template>
<!-- 透传到第三方组件或原生元素 -->
<el-button v-bind="attrs">
<!-- 透传所有插槽 -->
<template v-for="(_, name) in slots" #[name]="slotProps">
<slot :name="name" v-bind="slotProps"></slot>
</template>
</el-button>
</template>
TypeScript 高级用法
完整的类型安全示例
vue
TypeScript
<script setup lang="ts">
import { useSlots, useAttrs, Slots, VNode } from 'vue'
// 1. 定义插槽类型
interface MySlots {
default?: () => VNode[]
title?: (props: { size: 'sm' | 'lg' }) => VNode[]
actions?: () => VNode[]
}
// 2. 定义属性类型(排除已定义的 props)
interface MyAttrs {
class?: string
style?: Record<string, string | number>
onClick?: (event: MouseEvent) => void
[key: `data-${string}`]: string
}
// 3. 类型断言
const slots = useSlots() as unknown as MySlots
const attrs = useAttrs() as unknown as MyAttrs
// 4. 类型安全的处理函数
const renderTitleSlot = () => {
if (slots.title) {
return slots.title({ size: 'lg' })
}
return null
}
// 5. 属性类型守卫
const handleClick = (event: MouseEvent) => {
if (attrs.onClick) {
attrs.onClick(event)
}
}
</script>
泛型组件包装器
vue
javascript
<!-- GenericWrapper.vue -->
<script setup lang="ts" generic="T extends Record<string, any>">
import { useSlots, useAttrs, Slots } from 'vue'
const props = defineProps<{
data: T
renderItem?: (item: T) => any
}>()
const slots = useSlots()
const attrs = useAttrs()
// 泛型类型在插槽中的使用
interface ScopedSlots {
default?: (props: { item: T; index: number }) => any
header?: () => any
}
const typedSlots = slots as unknown as ScopedSlots
</script>
<template>
<div v-bind="attrs">
<slot name="header"></slot>
<!-- 作用域插槽传递泛型数据 -->
<slot
v-if="typedSlots.default"
:item="props.data"
:index="0"
></slot>
<!-- 或者使用渲染函数 -->
<template v-else-if="props.renderItem">
{{ props.renderItem(props.data) }}
</template>
</div>
</template>
常见问题与解决方案
Q1: useSlots 和 $slots 的区别?
javascript
// Options API 中使用 $slots
export default {
mounted() {
console.log(this.$slots) // Options API 方式
}
}
// Composition API 中使用 useSlots
import { useSlots } from 'vue'
const slots = useSlots() // Composition API 方式
// 两者访问的是相同的内容,只是 API 不同
Q2: 如何判断插槽是否为空?
javascript
const slots = useSlots()
// ❌ 错误:直接判断布尔值
if (slots.default) { /* 可能不准确 */ }
// ✅ 正确:判断函数调用结果
if (slots.default && slots.default().length > 0) {
// 有实际内容
}
// ✅ 更好的方法:封装工具函数
const hasSlotContent = (name = 'default') => {
const slot = slots[name]
return slot && slot().length > 0
}
Q3: 属性继承冲突怎么办?
vue
javascript
<script setup>
import { useAttrs, computed } from 'vue'
const attrs = useAttrs()
// 处理 class 合并
const mergedClass = computed(() => {
const baseClass = 'my-component'
const attrClass = attrs.class || ''
// 合并多个 class
return `${baseClass} ${attrClass}`.trim()
})
// 处理 style 合并
const mergedStyle = computed(() => {
const baseStyle = { color: 'blue' }
const attrStyle = attrs.style || {}
return { ...baseStyle, ...attrStyle }
})
</script>
Q4: 动态插槽名如何处理?
vue
javascript
<script setup>
import { useSlots } from 'vue'
const slots = useSlots()
const slotName = ref('content')
// 动态访问插槽
const dynamicSlot = computed(() => {
return slots[slotName.value]
})
// 或者使用渲染函数
const renderDynamicSlot = (name) => {
const slot = slots[name]
if (slot) {
return h('div', slot())
}
return null
}
</script>
最佳实践总结
useSlots 最佳实践:
-
检查插槽存在性 :使用
slots.name?.()而不是slots.name -
避免过度使用:只在需要动态处理插槽时使用
-
类型安全:为 TypeScript 项目定义插槽类型
-
性能考虑:避免在渲染函数中频繁调用 slot 函数
useAttrs 最佳实践:
-
明确继承策略 :使用
inheritAttrs: false明确控制 -
属性分离:分离 UI 属性和功能属性
-
避免属性冲突:处理 class、style 合并
-
安全性:过滤敏感或不必要的属性
两者结合的最佳模式:
vue
javascript
<!-- SmartWrapper.vue -->
<script setup>
import { useSlots, useAttrs, computed } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
// 智能属性处理
const processedAttrs = computed(() => {
const {
class: userClass,
style: userStyle,
...restAttrs
} = attrs
return {
class: `wrapper ${userClass || ''}`.trim(),
style: {
position: 'relative',
...(userStyle || {})
},
...restAttrs
}
})
// 插槽内容增强
const enhancedSlots = computed(() => {
const result = {}
Object.keys(slots).forEach(name => {
if (slots[name]) {
// 为每个插槽内容添加包装
result[name] = () => {
const content = slots[name]()
return content.map(vnode => ({
...vnode,
props: {
...vnode.props,
'data-slot': name
}
}))
}
}
})
return result
})
</script>
<template>
<div v-bind="processedAttrs">
<template v-for="(slotFn, name) in enhancedSlots" :key="name">
<slot :name="name" v-if="slotFn"></slot>
</template>
</div>
</template>
useSlots 和 useAttrs 提供了在 Composition API 中访问组件"外部输入"的能力,它们是创建灵活、可复用组件的重要工具。正确使用它们可以大大提高组件的适应性和开发效率。