Vue 3 透传 Attributes 第六章:透传 Attributes 的实际应用场景完全指南

扫描 二维码 关注或者微信搜一搜:编程智域 前端至全栈交流与成长

发现 1000+ 提升效率与开发的 AI 工具和实用程序https://tools.cmdragon.cn/

一、UI 组件库开发

1.1 基础按钮组件

<!-- 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">
     {{ 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">{{ error }}</span>
   <span v-if="hint && !error" class="input-hint">{{ 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">{{ loadingText }}</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>{{ data.title }}</p>
     <p>{{ data.content }}</p>
   </div>
 </WithLoading>
 
 <button
   @click="loadData"
   :disabled="isLoading"
 >
   {{ 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 透传?

答案

  1. 设置 inheritAttrs: false

  2. 使用 v-bind="$attrs" 将 attributes 绑定到包装元素或内部组件

  3. 在初始化第三方组件时,将相关 attributes 传递给配置

  4. 使用 defineExpose 暴露必要的方法和属性


问题 3:在高阶组件中如何使用透传 Attributes?

答案

  1. 使用 defineOptions({ inheritAttrs: false }) 禁用自动透传

  2. 在包装的元素上使用 v-bind="$attrs"

  3. 根据业务逻辑条件性地渲染包装内容

  4. 保持透传 attributes 的响应式更新

参考链接

余下文章内容请点击跳转至 个人博客页面 或者 扫描 二维码 关注或者微信搜一搜:编程智域 前端至全栈交流与成长,阅读完整的文章:Vue 3 透传 Attributes 第六章:透传 Attributes 的实际应用场景完全指南

评论

此博客中的热门博文

深入探讨聚合函数(COUNT, SUM, AVG, MAX, MIN):分析和总结数据的新视野

数据库与编程语言的连接

数据库的创建与删除:理论与实践