04|属性透传
第二章:组件模型与通信纵览 / 04。调用方传入但未被声明为 props 或 emits 的内容,会进入 attrs;封装组件必须决定这些内容最终落到哪个 DOM 节点。
前置问题:谁才是交互目标
如果 TextField 的根元素是 <label>,调用方传入的 disabled、name、aria-describedby 显然应落到内部 <input>,而不是标签外壳。默认继承无法知道这个意图,所以包装组件要接管落点。
最小可运行封装
<script setup lang="ts">
defineOptions({ inheritAttrs: false })
defineProps<{
label: string
help?: string
helpId?: string
}>()
</script>
<template>
<label class="field">
<span>{{ label }}</span>
<input v-bind="$attrs" />
</label>
<p v-if="help" :id="helpId">{{ help }}</p>
</template><script setup lang="ts">
import TextField from './TextField.vue'
</script>
<template>
<TextField
label="邮箱"
help="用于接收通知"
help-id="email-help"
id="email"
name="email"
type="email"
aria-describedby="email-help"
data-testid="email-input"
/>
</template>label、help 与 helpId 被 defineProps 消费,不会留在 $attrs。id、name、type、aria-describedby 和 data-testid 没有被声明,因此进入 $attrs,再由 v-bind="$attrs" 一次绑定到 <input>。
默认继承规则
| 组件形状 | 默认行为 | 需要注意 |
|---|---|---|
| 单个元素根节点 | attrs 自动落到根元素 | class 与 style 会和根节点已有值合并 |
| 根节点是另一个组件 | attrs 可继续向该组件透传 | 最终落点取决于更深一层的合同 |
| 多个根节点 | Vue 不会猜落点,并在未绑定时警告 | 至少一个根节点显式 v-bind="$attrs" |
inheritAttrs: false | 关闭自动落点 | $attrs 仍然可读,可手动绑定 |
正文例子同时使用多根节点和 inheritAttrs: false,即使将来重排根节点,输入属性仍稳定地落在 <input>。
class 和 style 也属于 attrs
若调用方写 <TextField class="wide" />,且组件保持默认单根继承,wide 会加在根 <label> 上,不是内部 <input>。包装原生控件时,这种“看起来有样式却选错节点”的错误很常见。若希望同时给外壳和输入传样式,应设计两个明确 prop,而不是让一个 class 同时承担两种含义。
在脚本中读取 attrs
<script setup lang="ts">
import { useAttrs } from 'vue'
const attrs = useAttrs()
console.log(attrs['aria-label'])
</script>连字符属性要用方括号读取。useAttrs() 返回的对象会在渲染时反映最新 attrs,但它不是普通响应式对象,不能指望 watch(attrs, ...) 捕获变化。若某个值要参与计算、监听或业务分支,就把它声明成 prop;attrs 只承担透传。
失败路径:忘记多根节点的落点
先只删掉 v-bind="$attrs":因为仍有 inheritAttrs: false,Vue 尊重你接管属性的决定,不再尝试自动透传,也不会因此告警。输入会静默失去 name、ARIA 与测试选择器。再移除 defineOptions({ inheritAttrs: false }),恢复默认继承:此时多根组件没有指定出口,才会出现 extraneous non-props attributes 警告。无警告不能证明透传正确,最终要检查真实input。
另一个失败是把 $attrs 同时绑定到多个根节点:这会复制 id,产生无效 HTML,也可能让同一个监听器触发多次。一个 attrs 集合通常只应有一个明确目标。
何时应该声明 prop
- 值会影响组件自己的渲染逻辑:声明 prop。
- 值只是原样交给某个 DOM 元素:保留为 attr。
- 调用方需要选择“外壳还是内部控件”:设计两个有名字的入口。
- 需要监听值的变化:声明 prop,不监听
useAttrs()。
练习:精确透传搜索输入属性
实现 SearchField.vue:
- props 只有
label: string与help: string。 - 模板保留两个根节点:一个
<label>和一个帮助<p>。 - 调用方传入的
id、name、placeholder、aria-describedby、data-testid与class全部只落到内部<input>。 label、help不得出现在<input>的 DOM 属性中。- 只删除显式透传时确认属性静默丢失;再恢复默认继承以复现多根警告。最后恢复关闭继承与显式透传,确认只有一个带该
id的元素。
参考答案:查看第 2 章第 04 题参考答案。
通关自检:给定任意包装组件,你能列出哪些键被 props/emits 消费、哪些留在 attrs,以及 attrs 最终绑定到哪个真实元素。