Vue 3 透传 Attributes 第六章:透传 Attributes 的实际应用场景完全指南
扫描 关注或者微信搜一搜:编程智域 前端至全栈交流与成长
一、UI 组件库开发
<!-- BaseButton.vue -->
<template>
<button
class="btn"
:class="btnClasses"
v-bind="boundAttrs"
:disabled="isDisabled"
@click="handleClick"
>
<span v-if="loading" class="loading-spinner">
<svg class="spinner" viewBox="0 0 50 50">
<circle
cx="25"
cy="25"
r="20"
fill="none"
stroke-width="5"
/>
</svg>
</span>
<span v-if="$slots.icon" class="btn-icon">
<slot name="icon" />
</span>
<span class="btn-text">
<slot />
</span>
</button>
</template>
<script setup>
import { useAttrs, computed } from 'vue'
defineOptions({
inheritAttrs: false
})
const props = defineProps({
type: {
type: String,
default: 'default',
validator: (value) => ['default', 'primary', 'success', 'warning', 'danger'].includes(value)
},
size: {
type: String,
default: 'medium',
validator: (value) => ['small', 'medium', 'large'].includes(value)
},
loading: Boolean,
disabled: Boolean,
block: Boolean,
round: Boolean,
circle: Boolean
})
const emit = defineEmits(['click'])
const attrs = useAttrs()
const btnClasses = computed(() => ({
[`btn-${props.type}`]: true,
[`btn-${props.size}`]: true,
'btn-block': props.block,
'btn-round': props.round,
'btn-circle': props.circle,
'is-loading': props.loading,
'is-disabled': props.disabled
}))
const boundAttrs = computed(() => {
const { class: className, style, rest } = attrs
return {
class: className,
style,
rest
}
})
const isDisabled = computed(() => props.disabled || props.loading)
const handleClick = (event) => {
if (!isDisabled.value) {
emit('click', event)
}
}
</script>
<style scoped>
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8px 16px;
font-size: 14px;
line-height: 1.5;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
outline: none;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-text {
flex: 1;
}
.btn-icon {
margin-right: 8px;
display: flex;
align-items: center;
}
.btn-small {
padding: 4px 8px;
font-size: 12px;
}
.btn-large {
padding: 12px 24px;
font-size: 16px;
}
.btn-block {
display: flex;
width: 100%;
}
.btn-round {
border-radius: 20px;
}
.btn-circle {
width: 40px;
height: 40px;
padding: 0;
border-radius: 50%;
}
.btn-default {
background-color: #f0f0f0;
color: #333;
}
.btn-default:hover:not(:disabled) {
background-color: #e0e0e0;
}
.btn-primary {
background-color: #007bff;
color: #fff;
}
.btn-primary:hover:not(:disabled) {
background-color: #0056b3;
}
.btn-success {
background-color: #28a745;
color: #fff;
}
.btn-success:hover:not(:disabled) {
background-color: #218838;
}
.btn-warning {
background-color: #ffc107;
color: #333;
}
.btn-warning:hover:not(:disabled) {
background-color: #e0a800;
}
.btn-danger {
background-color: #dc3545;
color: #fff;
}
.btn-danger:hover:not(:disabled) {
background-color: #c82333;
}
.loading-spinner {
display: inline-flex;
margin-right: 8px;
}
.spinner {
width: 1em;
height: 1em;
animation: rotate 1s linear infinite;
}
.spinner circle {
stroke: currentColor;
stroke-dasharray: 90, 150;
stroke-dashoffset: 0;
stroke-linecap: round;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>使用:
<template>
<BaseButton type="primary" @click="handleSubmit">
提交
</BaseButton>
<BaseButton type="success" loading>
加载中
</BaseButton>
<BaseButton type="danger" disabled>
禁用
</BaseButton>
<BaseButton type="primary" block round>
圆角按钮
</BaseButton>
<BaseButton type="primary" circle>
<template #icon>
<svg viewBox="0 0 24 24" width="16" height="16">
<path fill="currentColor" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>
</template>
带图标
</BaseButton>
</template>1.2 输入框组件
<!-- BaseInput.vue -->
<template>
<div class="input-wrapper" :class="wrapperClasses">
<label v-if="label" :for="inputId" class="input-label">
<span v-if="required" class="required">*</span>
</label>
<div class="input-group">
<span v-if="$slots.prepend" class="input-prepend">
<slot name="prepend" />
</span>
<input
:id="inputId"
ref="inputRef"
class="input-field"
v-bind="boundAttrs"
:value="modelValue"
:disabled="disabled"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
/>
<span v-if="$slots.append" class="input-append">
<slot name="append" />
</span>
<button
v-if="clearable && modelValue"
class="input-clear"
@click="handleClear"
>
×
</button>
</div>
<span v-if="error" class="input-error"></span>
<span v-if="hint && !error" class="input-hint"></span>
</div>
</template>
<script setup>
import { useAttrs, computed, ref } from 'vue'
defineOptions({
inheritAttrs: false
})
const props = defineProps({
modelValue: [String, Number],
label: String,
required: Boolean,
disabled: Boolean,
clearable: Boolean,
error: String,
hint: String,
inputId: String,
size: {
type: String,
default: 'medium'
}
})
const emit = defineEmits(['update:modelValue', 'focus', 'blur', 'clear'])
const attrs = useAttrs()
const inputRef = ref(null)
const inputId = computed(() => props.inputId || `input-${Math.random().toString(36).slice(2)}`)
const wrapperClasses = computed(() => ({
'has-error': !!props.error,
[`input-${props.size}`]: true
}))
const boundAttrs = computed(() => {
const { class: className, style, rest } = attrs
return {
class: ['input-field', className].filter(Boolean).join(' '),
style,
rest
}
})
const handleInput = (event) => {
emit('update:modelValue', event.target.value)
}
const handleFocus = (event) => {
emit('focus', event)
}
const handleBlur = (event) => {
emit('blur', event)
}
const handleClear = () => {
emit('update:modelValue', '')
emit('clear')
inputRef.value?.focus()
}
defineExpose({
focus: () => inputRef.value?.focus(),
blur: () => inputRef.value?.blur(),
select: () => inputRef.value?.select()
})
</script>
<style scoped>
.input-wrapper {
margin-bottom: 16px;
}
.input-label {
display: block;
margin-bottom: 4px;
font-size: 14px;
font-weight: 500;
color: #333;
}
.required {
color: #dc3545;
margin-left: 4px;
}
.input-group {
display: flex;
align-items: center;
border: 1px solid #ddd;
border-radius: 4px;
transition: border-color 0.3s;
}
.input-group:focus-within {
border-color: #007bff;
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}
.input-field {
flex: 1;
border: none;
padding: 8px 12px;
font-size: 14px;
outline: none;
background: transparent;
}
.input-field:disabled {
background-color: #f5f5f5;
cursor: not-allowed;
}
.input-prepend,
.input-append {
display: flex;
align-items: center;
padding: 0 12px;
background: #f5f5f5;
border: none;
color: #666;
}
.input-clear {
border: none;
background: transparent;
padding: 0 8px;
font-size: 18px;
color: #999;
cursor: pointer;
transition: color 0.3s;
}
.input-clear:hover {
color: #666;
}
.input-error {
display: block;
font-size: 12px;
color: #dc3545;
margin-top: 4px;
}
.input-hint {
display: block;
font-size: 12px;
color: #6c757d;
margin-top: 4px;
}
.has-error .input-group {
border-color: #dc3545;
}
.input-small .input-field {
padding: 4px 8px;
font-size: 12px;
}
.input-large .input-field {
padding: 12px 16px;
font-size: 16px;
}
</style>使用:
<template>
<BaseInput
v-model="username"
label="用户名"
required
placeholder="请输入用户名"
clearable
:hint="usernameHint"
:error="usernameError"
/>
<BaseInput
v-model="email"
label="邮箱"
type="email"
placeholder="example@email.com"
>
<template #prepend>
<svg width="16" height="16" viewBox="0 0 24 24">
<path fill="currentColor" d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/>
</svg>
</template>
</BaseInput>
<BaseInput
v-model="search"
placeholder="搜索..."
clearable
>
<template #append>
<button @click="handleSearch">
<svg width="16" height="16" viewBox="0 0 24 24">
<path fill="currentColor" d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
</svg>
</button>
</template>
</BaseInput>
</template>
<script setup>
import { ref, computed } from 'vue'
const username = ref('')
const email = ref('')
const search = ref('')
const usernameHint = computed(() => {
return username.value.length >= 3 ? '✓ 符合要求' : '用户名至少 3 个字符'
})
const usernameError = computed(() => {
return username.value.length > 0 && username.value.length < 3 ? '用户名太短' : ''
})
const handleSearch = () => {
console.log('搜索:', search.value)
}
</script>二、高阶组件封装
2.1 带权限控制的包装组件
<!-- WithPermission.vue -->
<template>
<component
:is="as"
v-bind="$attrs"
v-if="hasPermission"
>
<slot />
</component>
<component
:is="fallback"
v-else
>
<slot name="fallback" />
</component>
</template>
<script setup>
defineOptions({
inheritAttrs: false
})
const props = defineProps({
as: {
type: String,
default: 'div'
},
permission: {
type: String,
required: true
},
fallback: {
type: String,
default: 'span'
}
})
// 模拟权限检查
const userPermissions = ['read', 'write']
const hasPermission = computed(() => {
return userPermissions.includes(props.permission)
})
</script>使用:
<template>
<WithPermission
permission="write"
as="button"
type="button"
class="edit-btn"
@click="handleEdit"
>
编辑
<template #fallback>
<span class="no-permission">无权限</span>
</template>
</WithPermission>
<WithPermission
permission="delete"
as="button"
type="button"
class="delete-btn"
@click="handleDelete"
>
删除
<template #fallback>
<span class="no-permission">无权限</span>
</template>
</WithPermission>
</template>2.2 带加载状态的包装组件
<!-- WithLoading.vue -->
<template>
<div class="loading-wrapper" v-bind="$attrs">
<slot v-if="!loading" />
<div v-else class="loading-overlay">
<div class="loading-spinner">
<svg class="spinner" viewBox="0 0 50 50">
<circle
cx="25"
cy="25"
r="20"
fill="none"
stroke-width="5"
/>
</svg>
</div>
<span v-if="loadingText" class="loading-text"></span>
</div>
</div>
</template>
<script setup>
defineOptions({
inheritAttrs: false
})
const props = defineProps({
loading: Boolean,
loadingText: String
})
</script>
<style scoped>
.loading-wrapper {
position: relative;
}
.loading-overlay {
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: inherit;
}
.loading-spinner {
display: flex;
}
.spinner {
width: 40px;
height: 40px;
animation: rotate 1s linear infinite;
}
.spinner circle {
stroke: #007bff;
stroke-dasharray: 90, 150;
stroke-dashoffset: 0;
stroke-linecap: round;
}
.loading-text {
margin-top: 12px;
font-size: 14px;
color: #666;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>使用:
<template>
<WithLoading
:loading="isLoading"
loading-text="加载中..."
class="content-box"
>
<div v-if="data">
<p></p>
<p></p>
</div>
</WithLoading>
<button
@click="loadData"
:disabled="isLoading"
>
</button>
</template>
<script setup>
import { ref } from 'vue'
const isLoading = ref(false)
const data = ref(null)
const loadData = async () => {
isLoading.value = true
try {
// 模拟异步请求
await new Promise(resolve => setTimeout(resolve, 2000))
data.value = {
title: '标题',
content: '内容'
}
} finally {
isLoading.value = false
}
}
</script>2.3 带错误处理的包装组件
<!-- WithErrorBoundary.vue -->
<template>
<component
:is="as"
v-bind="$attrs"
>
<slot v-if="!hasError" />
<div v-else class="error-boundary">
<slot name="error" :error="error" :reset="reset">
<p class="error-message">发生错误:{{ error?.message }}</p>
<button @click="reset">重试</button>
</slot>
</div>
</component>
</template>
<script setup>
import { ref } from 'vue'
defineOptions({
inheritAttrs: false
})
const props = defineProps({
as: {
type: String,
default: 'div'
}
})
const hasError = ref(false)
const error = ref(null)
const reset = () => {
hasError.value = false
error.value = null
}
// 错误处理(实际使用中需要结合错误捕获)
const handleError = (err) => {
hasError.value = true
error.value = err
}
defineExpose({
handleError,
reset
})
</script>
<style scoped>
.error-boundary {
padding: 20px;
text-align: center;
background: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 4px;
color: #721c24;
}
.error-message {
margin-bottom: 12px;
}
.error-boundary button {
padding: 8px 16px;
background: #721c24;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
}
.error-boundary button:hover {
background: #5a171d;
}
</style>三、表单组件高级应用
3.1 动态表单字段渲染器
<!-- FormRenderer.vue -->
<template>
<form class="form-renderer" v-bind="$attrs" @submit.prevent="handleSubmit">
<component
v-for="field in fields"
:key="field.prop"
:is="getComponentType(field.type)"
v-bind="getFieldProps(field)"
v-model="formData[field.prop]"
/>
<div class="form-actions">
<slot name="actions" :loading="isSubmitting">
<button
type="submit"
:disabled="isSubmitting"
class="submit-btn"
>
{{ isSubmitting ? '提交中...' : '提交' }}
</button>
</slot>
</div>
</form>
</template>
<script setup>
import { useAttrs, ref, computed } from 'vue'
defineOptions({
inheritAttrs: false
})
const props = defineProps({
fields: {
type: Array,
required: true
},
modelValue: {
type: Object,
default: () => ({})
}
})
const emit = defineEmits(['update:modelValue', 'submit'])
const attrs = useAttrs()
const isSubmitting = ref(false)
const formData = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value)
})
const getComponentType = (type) => {
const componentMap = {
text: 'input',
textarea: 'textarea',
select: 'select',
checkbox: 'input',
radio: 'input'
}
return componentMap[type] || 'input'
}
const getFieldProps = (field) => {
const baseProps = {
label: field.label,
required: field.required,
placeholder: field.placeholder,
error: field.error
}
if (field.type === 'select') {
baseProps.options = field.options
}
if (field.type === 'checkbox' || field.type === 'radio') {
baseProps.options = field.options
}
return baseProps
}
const handleSubmit = async () => {
isSubmitting.value = true
try {
await emit('submit', formData.value)
} finally {
isSubmitting.value = false
}
}
</script>
<style scoped>
.form-renderer {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.form-actions {
margin-top: 24px;
display: flex;
justify-content: flex-end;
gap: 12px;
}
.submit-btn {
padding: 10px 24px;
background: #007bff;
color: #fff;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background 0.3s;
}
.submit-btn:hover:not(:disabled) {
background: #0056b3;
}
.submit-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>使用:
<template>
<FormRenderer
v-model="formData"
:fields="formFields"
@submit="handleSubmit"
/>
</template>
<script setup>
import { ref } from 'vue'
const formData = ref({
username: '',
email: '',
gender: '',
bio: ''
})
const formFields = [
{
prop: 'username',
type: 'text',
label: '用户名',
required: true,
placeholder: '请输入用户名'
},
{
prop: 'email',
type: 'email',
label: '邮箱',
required: true,
placeholder: 'example@email.com'
},
{
prop: 'gender',
type: 'select',
label: '性别',
required: true,
options: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' }
]
},
{
prop: 'bio',
type: 'textarea',
label: '个人简介',
placeholder: '介绍一下自己',
rows: 4
}
]
const handleSubmit = (data) => {
console.log('提交数据:', data)
}
</script>3.2 表单验证包装器
<!-- FormValidator.vue -->
<template>
<form
class="validator-form"
v-bind="$attrs"
@submit.prevent="handleSubmit"
>
<slot :errors="errors" :validate="validate" />
</form>
</template>
<script setup>
import { useAttrs, ref, reactive } from 'vue'
defineOptions({
inheritAttrs: false
})
const props = defineProps({
modelValue: {
type: Object,
required: true
},
rules: {
type: Object,
required: true
}
})
const emit = defineEmits(['update:modelValue', 'submit'])
const attrs = useAttrs()
const errors = reactive({})
const validate = async () => {
const newErrors = {}
for (const [field, rules] of Object.entries(props.rules)) {
const value = props.modelValue[field]
for (const rule of rules) {
const error = await validateRule(field, value, rule)
if (error) {
newErrors[field] = error
break
}
}
}
Object.assign(errors, newErrors)
return Object.keys(newErrors).length === 0
}
const validateRule = async (field, value, rule) => {
if (rule.required && !value) {
return rule.message || `${field} 是必填项`
}
if (rule.pattern && !rule.pattern.test(value)) {
return rule.message || `${field} 格式不正确`
}
if (rule.min !== undefined && value.length < rule.min) {
return rule.message || `${field} 长度不能少于 ${rule.min}`
}
if (rule.max !== undefined && value.length > rule.max) {
return rule.message || `${field} 长度不能超过 ${rule.max}`
}
if (rule.validator) {
const result = await rule.validator(value)
if (result !== true) {
return result || rule.message
}
}
return null
}
const handleSubmit = async () => {
const isValid = await validate()
if (isValid) {
emit('submit', props.modelValue)
}
}
defineExpose({
validate,
errors
})
</script>
<style scoped>
.validator-form {
width: 100%;
}
</style>使用:
<template>
<FormValidator
v-model="formData"
:rules="formRules"
v-slot="{ errors, validate }"
>
<BaseInput
v-model="formData.username"
label="用户名"
:error="errors.username"
placeholder="请输入用户名"
/>
<BaseInput
v-model="formData.email"
label="邮箱"
:error="errors.email"
type="email"
placeholder="example@email.com"
/>
<BaseInput
v-model="formData.password"
label="密码"
:error="errors.password"
type="password"
placeholder="请输入密码"
/>
<button type="submit" @click="validate">
提交
</button>
</FormValidator>
</template>
<script setup>
import { ref } from 'vue'
const formData = ref({
username: '',
email: '',
password: ''
})
const formRules = {
username: [
{ required: true, message: '用户名不能为空' },
{ min: 3, message: '用户名至少 3 个字符' },
{ max: 20, message: '用户名最多 20 个字符' }
],
email: [
{ required: true, message: '邮箱不能为空' },
{
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: '邮箱格式不正确'
}
],
password: [
{ required: true, message: '密码不能为空' },
{ min: 6, message: '密码至少 6 个字符' },
{
validator: (value) => {
if (!/[A-Z]/.test(value)) {
return '密码必须包含大写字母'
}
if (!/[0-9]/.test(value)) {
return '密码必须包含数字'
}
return true
}
}
]
}
</script>四、第三方组件包装
4.1 包装图表库组件
<!-- ChartWrapper.vue -->
<template>
<div
ref="chartRef"
class="chart-wrapper"
v-bind="$attrs"
/>
</template>
<script setup>
import { useAttrs, ref, onMounted, onUnmounted, watch } from 'vue'
defineOptions({
inheritAttrs: false
})
const props = defineProps({
options: {
type: Object,
required: true
},
type: {
type: String,
default: 'line'
}
})
const attrs = useAttrs()
const chartRef = ref(null)
let chartInstance = null
onMounted(() => {
initChart()
})
onUnmounted(() => {
chartInstance?.destroy()
})
watch(() => props.options, () => {
updateChart()
}, { deep: true })
const initChart = () => {
// 假设使用 Chart.js
if (chartRef.value) {
const ctx = chartRef.value.getContext('2d')
chartInstance = new Chart(ctx, {
type: props.type,
data: props.options.data,
options: {
...props.options,
responsive: true,
maintainAspectRatio: false
}
})
}
}
const updateChart = () => {
if (chartInstance) {
chartInstance.data = props.options.data
chartInstance.options = {
...props.options,
responsive: true,
maintainAspectRatio: false
}
chartInstance.update()
}
}
defineExpose({
chart: chartInstance,
update: updateChart
})
</script>
<style scoped>
.chart-wrapper {
position: relative;
width: 100%;
height: 400px;
}
</style>4.2 包装富文本编辑器
<!-- RichTextEditor.vue -->
<template>
<div class="editor-wrapper" v-bind="$attrs">
<div
ref="editorRef"
class="editor-container"
/>
</div>
</template>
<script setup>
import { useAttrs, ref, onMounted, watch } from 'vue'
defineOptions({
inheritAttrs: false
})
const props = defineProps({
modelValue: String,
placeholder: String,
height: {
type: String,
default: '300px'
}
})
const emit = defineEmits(['update:modelValue', 'change'])
const attrs = useAttrs()
const editorRef = ref(null)
let editorInstance = null
onMounted(() => {
initEditor()
})
watch(() => props.modelValue, (newValue) => {
if (editorInstance && newValue !== editorInstance.getContent()) {
editorInstance.setContent(newValue || '')
}
})
const initEditor = () => {
// 假设使用 TinyMCE
if (editorRef.value) {
tinymce.init({
target: editorRef.value,
height: props.height,
placeholder: props.placeholder,
content: props.modelValue,
setup: (editor) => {
editorInstance = editor
editor.on('change', () => {
const content = editor.getContent()
emit('update:modelValue', content)
emit('change', content)
})
},
...attrs
})
}
}
onUnmounted(() => {
editorInstance?.destroy()
})
defineExpose({
editor: editorInstance,
getContent: () => editorInstance?.getContent(),
setContent: (content) => editorInstance?.setContent(content)
})
</script>
<style scoped>
.editor-wrapper {
width: 100%;
}
.editor-container {
border: 1px solid #ddd;
border-radius: 4px;
overflow: hidden;
}
</style>五、性能优化最佳实践
5.1 避免不必要的透传
<script setup>
import { useAttrs, computed } from 'vue'
defineOptions({
inheritAttrs: false
})
const attrs = useAttrs()
// 只透传需要的属性
const boundAttrs = computed(() => {
const allowedKeys = ['id', 'placeholder', 'disabled', 'readonly']
const result = {}
allowedKeys.forEach(key => {
if (key in attrs) {
result[key] = attrs[key]
}
})
return result
})
</script>
<template>
<input v-bind="boundAttrs" />
</template>5.2 使用计算属性缓存
<script setup>
import { useAttrs, computed } from 'vue'
defineOptions({
inheritAttrs: false
})
const attrs = useAttrs()
// 使用计算属性缓存处理后的 attrs
const processedAttrs = computed(() => {
const { class: className, style, ...rest } = attrs
return {
class: ['base-class', className].filter(Boolean).join(' '),
style: {
...baseStyle.value,
...style
},
...rest
}
})
</script>课后 Quiz
问题 1:在 UI 组件库开发中,透传 Attributes 的主要优势是什么?
答案:
提高组件的灵活性和可定制性
减少 props 的数量,简化组件 API
支持原生 HTML 属性的直接传递
便于组件的组合和复用
问题 2:如何包装第三方组件以支持 Attributes 透传?
答案:
设置
inheritAttrs: false使用
v-bind="$attrs"将 attributes 绑定到包装元素或内部组件在初始化第三方组件时,将相关 attributes 传递给配置
使用
defineExpose暴露必要的方法和属性
问题 3:在高阶组件中如何使用透传 Attributes?
答案:
使用
defineOptions({ inheritAttrs: false })禁用自动透传在包装的元素上使用
v-bind="$attrs"根据业务逻辑条件性地渲染包装内容
保持透传 attributes 的响应式更新
参考链接
余下文章内容请点击跳转至 个人博客页面 或者 扫描 关注或者微信搜一搜:编程智域 前端至全栈交流与成长,阅读完整的文章:
评论
发表评论