Vue3模板进阶:动态样式与事件处理实战
1. Vue3 模板进阶核心概念解析
在掌握了 Vue3 的基础语法后,很多开发者会遇到一个共同的困惑:为什么我的代码能运行,但总感觉不够"专业"?这个问题的答案往往藏在模板层的进阶用法中。模板不仅仅是数据的展示层,更是交互逻辑与数据绑定的关键枢纽。
动态样式绑定是第一个需要攻克的难点。在真实项目中,我们很少会遇到静态不变的样式。一个典型的电商网站就包含:购物车按钮的选中状态、商品列表的悬停效果、表单验证的错误提示等动态样式需求。这些场景都要求我们深入理解 Vue 的样式绑定机制。
2. 动态 class 绑定实战指南
2.1 三种绑定方式对比
对象语法是最常用的动态 class 绑定方式,特别适合状态切换场景。它的优势在于:
- 可读性强,一眼就能看出哪些 class 会被添加
- 维护方便,只需修改响应式变量的值
- 与 Vue 的响应式系统完美集成
<template> <div :class="{ 'active': isActive, 'error': hasError, 'disabled': isDisabled }"> 订单状态 </div> </template> <script setup> const isActive = ref(true) const hasError = ref(false) const isDisabled = ref(true) </script>数组语法适合需要组合多个 class 的场景,特别是在使用第三方库时:
<template> <div :class="[ baseClass, theme === 'dark' ? 'dark-theme' : 'light-theme', { 'rounded': isRounded } ]"> 主题卡片 </div> </template>2.2 性能优化建议
虽然动态 class 绑定非常方便,但需要注意:
- 避免在模板中编写复杂的逻辑判断
- 对于频繁变化的 class,考虑使用计算属性
- 大型项目中使用 CSS Modules 或 Scoped Styles 避免命名冲突
提示:在 Vue 3.2+ 版本中,可以在 style 标签中使用 v-bind 直接绑定 CSS 变量,这为动态样式提供了另一种实现方式。
3. 动态 style 绑定深度解析
3.1 样式绑定的高级用法
行内样式绑定在处理需要精确计算的样式时表现出色。一个典型的例子是进度条组件:
<template> <div class="progress-bar"> <div class="progress-fill" :style="{ width: `${progress}%`, backgroundColor: progressColor, transition: `width ${animationDuration}ms ease` }" ></div> </div> </template> <script setup> const progress = ref(0) const progressColor = computed(() => progress.value > 80 ? '#ff4757' : '#2ed573' ) const animationDuration = 300 </script>3.2 样式绑定的最佳实践
在实际项目中,建议遵循以下原则:
- 将样式逻辑尽可能移出模板,使用计算属性
- 对于复杂的动画效果,优先考虑 CSS 动画而非 JS 计算
- 使用 CSS 变量与 Vue 数据绑定结合的方式提高灵活性
// 更好的做法 const progressStyle = computed(() => ({ width: `${progress.value}%`, backgroundColor: progress.value > 80 ? '#ff4757' : '#2ed573' }))4. 事件处理进阶技巧
4.1 事件对象的高级应用
事件对象不仅包含基本的事件信息,还能帮助我们实现更精细的控制:
<template> <div class="draggable" @mousedown="startDrag" @mousemove="handleDrag" @mouseup="endDrag" > 可拖拽元素 </div> </template> <script setup> const startPosition = { x: 0, y: 0 } const startDrag = (e) => { startPosition.x = e.clientX startPosition.y = e.clientY // 添加全局事件监听 window.addEventListener('mousemove', handleDrag) window.addEventListener('mouseup', endDrag) } const handleDrag = (e) => { // 计算移动距离 const dx = e.clientX - startPosition.x const dy = e.clientY - startPosition.y // 应用变换 e.target.style.transform = `translate(${dx}px, ${dy}px)` } const endDrag = () => { // 移除全局事件监听 window.removeEventListener('mousemove', handleDrag) window.removeEventListener('mouseup', endDrag) } </script>4.2 自定义事件修饰符
除了内置修饰符,Vue 还允许创建自定义修饰符。例如实现一个双击修饰符:
// main.js app.config.globalProperties.$doubleClick = { beforeMount(el, binding) { let timeout el.addEventListener('click', () => { if (!timeout) { timeout = setTimeout(() => { timeout = null }, 300) } else { clearTimeout(timeout) timeout = null binding.value() } }) } }使用方式:
<button v-double-click="handleDoubleClick">双击我</button>5. 表单处理全攻略
5.1 复杂表单验证模式
对于需要复杂验证的表单,推荐使用组合式函数封装验证逻辑:
// useFormValidation.js export function useFormValidation() { const errors = ref({}) const validateEmail = (email) => { const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ return re.test(email) } const validateField = (field, value) => { switch (field) { case 'email': errors.value.email = validateEmail(value) ? '' : '无效的邮箱地址' break case 'password': errors.value.password = value.length >= 6 ? '' : '密码至少6位' break // 更多字段验证... } } return { errors, validateField } }在组件中使用:
<template> <form @submit.prevent="handleSubmit"> <input v-model="form.email" @blur="validateField('email', form.email)" > <span class="error">{{ errors.email }}</span> <input type="password" v-model="form.password" @blur="validateField('password', form.password)" > <span class="error">{{ errors.password }}</span> <button type="submit">提交</button> </form> </template> <script setup> import { useFormValidation } from './useFormValidation' const form = reactive({ email: '', password: '' }) const { errors, validateField } = useFormValidation() const handleSubmit = () => { // 提交前的最终验证 validateField('email', form.email) validateField('password', form.password) if (!Object.values(errors.value).some(e => e)) { // 提交表单... } } </script>5.2 文件上传处理
处理文件上传需要特别注意:
<template> <div> <input type="file" ref="fileInput" @change="handleFileChange" accept="image/*" > <button @click="uploadFile">上传</button> <div v-if="previewUrl"> <img :src="previewUrl" alt="预览" style="max-width: 200px;"> </div> </div> </template> <script setup> import { ref } from 'vue' const fileInput = ref(null) const selectedFile = ref(null) const previewUrl = ref('') const handleFileChange = (e) => { const file = e.target.files[0] if (!file) return selectedFile.value = file // 创建预览图 const reader = new FileReader() reader.onload = (e) => { previewUrl.value = e.target.result } reader.readAsDataURL(file) } const uploadFile = async () => { if (!selectedFile.value) return const formData = new FormData() formData.append('file', selectedFile.value) try { const response = await fetch('/api/upload', { method: 'POST', body: formData }) // 处理响应... } catch (error) { console.error('上传失败:', error) } } </script>6. 性能优化与最佳实践
6.1 减少模板复杂度
随着项目规模增长,模板容易变得臃肿。解决方法包括:
- 将复杂逻辑提取到计算属性中
- 使用组件拆分复杂模板
- 利用渲染函数处理极端复杂的渲染逻辑
// 使用渲染函数优化复杂条件渲染 import { h } from 'vue' const ComplexRenderer = { props: ['items'], setup(props) { return () => h('div', props.items.map(item => h('div', { class: item.status }, item.text) ) ) } }6.2 事件处理优化
高频触发的事件(如scroll、resize)需要特别处理:
import { onMounted, onUnmounted } from 'vue' export function useWindowEvent(event, callback) { onMounted(() => window.addEventListener(event, callback)) onUnmounted(() => window.removeEventListener(event, callback)) } // 在组件中使用 useWindowEvent('resize', () => { // 处理resize事件 })对于高频事件,考虑使用防抖:
import { debounce } from 'lodash-es' const handleScroll = debounce(() => { // 处理滚动逻辑 }, 100) useWindowEvent('scroll', handleScroll)7. 常见问题深度解析
7.1 样式绑定不生效的排查步骤
当动态样式不生效时,可以按照以下步骤排查:
- 检查绑定的数据是否为响应式(使用ref/reactive)
- 确认浏览器开发者工具中class/style是否正确应用
- 检查CSS作用域(scoped样式可能导致的问题)
- 验证CSS选择器优先级是否被覆盖
- 检查是否有拼写错误或语法错误
7.2 表单双向绑定的陷阱
v-model虽然方便,但在某些情况下需要注意:
- 自定义组件中使用时需要正确定义modelValue和update事件
- 对于原生表单元素,不同浏览器可能有不同的默认行为
- 处理大量表单数据时,考虑使用表单状态管理库(如vee-validate)
<!-- 自定义输入组件 --> <template> <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" > </template> <script setup> defineProps(['modelValue']) defineEmits(['update:modelValue']) </script>8. 综合实战:构建一个交互式任务管理器
让我们将所学知识应用到一个实际项目中:
<template> <div class="task-manager"> <form @submit.prevent="addTask" class="task-form"> <input v-model="newTask" placeholder="添加新任务" :class="{ 'error': newTaskError }" > <button type="submit">添加</button> <span v-if="newTaskError" class="error-msg">任务不能为空</span> </form> <div class="filters"> <button v-for="filter in filters" :key="filter" @click="currentFilter = filter" :class="{ active: currentFilter === filter }" > {{ filter }} </button> </div> <ul class="task-list"> <li v-for="task in filteredTasks" :key="task.id" :class="{ 'completed': task.completed, 'high-priority': task.priority === 'high' }" > <input type="checkbox" v-model="task.completed" @change="updateTask(task)" > <span>{{ task.text }}</span> <button @click="removeTask(task.id)" class="delete-btn" :style="{ backgroundColor: deleteBtnColor }" > 删除 </button> </li> </ul> <div class="stats"> <p>总计: {{ totalTasks }} 任务</p> <p>已完成: {{ completedTasks }}</p> </div> </div> </template> <script setup> import { ref, computed, onMounted } from 'vue' const tasks = ref([]) const newTask = ref('') const newTaskError = ref(false) const currentFilter = ref('全部') const deleteBtnColor = ref('#ff6b6b') const filters = ['全部', '进行中', '已完成'] // 从本地存储加载任务 onMounted(() => { const savedTasks = localStorage.getItem('tasks') if (savedTasks) tasks.value = JSON.parse(savedTasks) }) // 计算属性 const filteredTasks = computed(() => { switch (currentFilter.value) { case '进行中': return tasks.value.filter(t => !t.completed) case '已完成': return tasks.value.filter(t => t.completed) default: return tasks.value } }) const totalTasks = computed(() => tasks.value.length) const completedTasks = computed(() => tasks.value.filter(t => t.completed).length ) // 方法 const addTask = () => { if (!newTask.value.trim()) { newTaskError.value = true return } tasks.value.push({ id: Date.now(), text: newTask.value, completed: false, priority: 'normal' }) newTask.value = '' newTaskError.value = false saveTasks() } const removeTask = (id) => { tasks.value = tasks.value.filter(t => t.id !== id) saveTasks() } const updateTask = (task) => { saveTasks() } const saveTasks = () => { localStorage.setItem('tasks', JSON.stringify(tasks.value)) } </script> <style scoped> .task-manager { max-width: 600px; margin: 0 auto; padding: 20px; } .task-form { display: flex; margin-bottom: 20px; } .error { border-color: red; } .error-msg { color: red; margin-left: 10px; } .filters { margin-bottom: 20px; } .filters button { margin-right: 10px; padding: 5px 10px; cursor: pointer; } .filters button.active { background-color: #4CAF50; color: white; } .task-list { list-style: none; padding: 0; } .task-list li { display: flex; align-items: center; padding: 10px; margin-bottom: 10px; background-color: #f9f9f9; border-radius: 4px; } .task-list li.completed { opacity: 0.7; text-decoration: line-through; } .task-list li.high-priority { border-left: 3px solid #ff4757; } .task-list input[type="checkbox"] { margin-right: 10px; } .delete-btn { margin-left: auto; padding: 3px 8px; background-color: #ff6b6b; color: white; border: none; border-radius: 3px; cursor: pointer; } .stats { margin-top: 20px; display: flex; justify-content: space-between; } </style>这个示例展示了:
- 动态 class 和 style 的综合应用
- 表单处理和验证
- 列表渲染和过滤
- 本地存储集成
- 完整的交互逻辑
- 响应式样式设计
9. 高级技巧与模式
9.1 动态组件与过渡动画
结合动态组件和过渡动画可以创建更流畅的用户体验:
<template> <div> <button v-for="tab in tabs" :key="tab" @click="currentTab = tab" :class="{ active: currentTab === tab }" > {{ tab }} </button> <transition name="fade" mode="out-in"> <component :is="currentTabComponent" class="tab-content" /> </transition> </div> </template> <script setup> import { ref, computed } from 'vue' import Home from './Home.vue' import Posts from './Posts.vue' import Archive from './Archive.vue' const tabs = ['Home', 'Posts', 'Archive'] const currentTab = ref('Home') const currentTabComponent = computed(() => { switch (currentTab.value) { case 'Home': return Home case 'Posts': return Posts case 'Archive': return Archive } }) </script> <style> .fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; } .fade-enter-from, .fade-leave-to { opacity: 0; } .tab-content { margin-top: 20px; } button.active { background-color: #4CAF50; color: white; } </style>9.2 渲染函数与JSX
对于极端复杂的渲染逻辑,可以使用渲染函数或JSX:
import { h, ref } from 'vue' const DynamicHeading = { props: { level: { type: Number, required: true } }, setup(props, { slots }) { return () => h( `h${props.level}`, { class: 'dynamic-heading' }, slots.default() ) } } // 使用JSX const JsxComponent = { setup() { const count = ref(0) return () => ( <div> <button onClick={() => count.value++}> Count: {count.value} </button> <DynamicHeading level={2}> JSX in Vue </DynamicHeading> </div> ) } }10. 测试与调试策略
10.1 组件测试方法
为模板逻辑编写测试可以确保其可靠性:
import { mount } from '@vue/test-utils' import TaskManager from './TaskManager.vue' describe('TaskManager', () => { it('adds new task', async () => { const wrapper = mount(TaskManager) const input = wrapper.find('input[type="text"]') const form = wrapper.find('form') await input.setValue('New task') await form.trigger('submit') expect(wrapper.vm.tasks).toHaveLength(1) expect(wrapper.vm.tasks[0].text).toBe('New task') }) it('filters tasks', async () => { const wrapper = mount(TaskManager, { data() { return { tasks: [ { id: 1, text: 'Task 1', completed: false }, { id: 2, text: 'Task 2', completed: true } ] } } }) const filterButtons = wrapper.findAll('.filters button') await filterButtons[2].trigger('click') // 点击"已完成" expect(wrapper.vm.filteredTasks).toHaveLength(1) expect(wrapper.vm.filteredTasks[0].text).toBe('Task 2') }) })10.2 调试技巧
使用Vue DevTools可以高效调试模板问题:
- 检查组件层次结构
- 查看实时props和状态
- 跟踪事件触发
- 分析性能瓶颈
对于复杂的样式问题,可以使用浏览器开发者工具的以下功能:
- 元素检查器查看应用的实际样式
- 强制元素状态(:hover, :active等)
- 禁用/启用特定样式规则
- 查看CSS变量计算值
11. 性能监控与优化
11.1 渲染性能分析
使用Vue提供的性能API监控渲染性能:
import { onMounted } from 'vue' onMounted(() => { const unwatch = watchEffect(() => { // 组件渲染逻辑 }, { onTrack(e) { console.log('依赖被跟踪', e) }, onTrigger(e) { console.log('依赖触发更新', e) } }) })11.2 大型应用优化策略
对于大型Vue应用,考虑以下优化措施:
- 使用v-once处理静态内容
- 合理使用v-memo优化重复渲染
- 实现虚拟滚动处理长列表
- 按需加载组件
<template> <div> <!-- 静态内容只渲染一次 --> <header v-once> <h1>{{ title }}</h1> </header> <!-- 优化列表渲染 --> <ul> <li v-for="item in largeList" :key="item.id" v-memo="[item.id, item.status]" > {{ item.name }} </li> </ul> <!-- 异步加载组件 --> <Suspense> <template #default> <AsyncComponent /> </template> <template #fallback> Loading... </template> </Suspense> </div> </template> <script setup> import { defineAsyncComponent } from 'vue' const AsyncComponent = defineAsyncComponent(() => import('./AsyncComponent.vue') ) const largeList = ref([/* 大量数据 */]) </script>12. 生态集成与扩展
12.1 与UI框架协同工作
当使用Element Plus、Vuetify等UI框架时,模板写法需要相应调整:
<template> <el-form :model="form" @submit.prevent="handleSubmit"> <el-form-item label="用户名" prop="username"> <el-input v-model="form.username" /> </el-form-item> <el-form-item label="密码" prop="password"> <el-input v-model="form.password" type="password" /> </el-form-item> <el-button type="primary" native-type="submit"> 登录 </el-button> </el-form> </template> <script setup> const form = reactive({ username: '', password: '' }) const handleSubmit = () => { // 处理表单提交 } </script>12.2 集成状态管理
当应用规模增长时,考虑使用Pinia管理状态:
// stores/tasks.js import { defineStore } from 'pinia' export const useTaskStore = defineStore('tasks', { state: () => ({ tasks: [], filter: 'all' }), actions: { addTask(text) { this.tasks.push({ id: Date.now(), text, completed: false }) }, removeTask(id) { this.tasks = this.tasks.filter(t => t.id !== id) } }, getters: { filteredTasks() { switch (this.filter) { case 'active': return this.tasks.filter(t => !t.completed) case 'completed': return this.tasks.filter(t => t.completed) default: return this.tasks } } } })在组件中使用:
<template> <div> <input v-model="newTask" @keyup.enter="addTask" > <ul> <li v-for="task in taskStore.filteredTasks" :key="task.id"> {{ task.text }} </li> </ul> </div> </template> <script setup> import { useTaskStore } from '@/stores/tasks' const taskStore = useTaskStore() const newTask = ref('') const addTask = () => { if (newTask.value.trim()) { taskStore.addTask(newTask.value) newTask.value = '' } } </script>13. 项目结构与代码组织
13.1 大型项目模板组织建议
对于大型项目,推荐按功能组织模板文件:
src/ components/ common/ # 通用组件 tasks/ # 任务相关组件 users/ # 用户相关组件 composables/ # 组合式函数 views/ # 页面级组件 assets/ # 静态资源 stores/ # 状态管理 router/ # 路由配置 utils/ # 工具函数13.2 单文件组件结构优化
优化SFC结构提高可维护性:
<template> <!-- 模板内容 --> </template> <script setup> // 导入语句分组 import { ref, computed } from 'vue' import { useRouter } from 'vue-router' import { useTaskStore } from '@/stores/tasks' // 组件props定义 const props = defineProps({ userId: { type: String, required: true } }) // 组合式函数调用 const router = useRouter() const taskStore = useTaskStore() // 响应式状态 const newTask = ref('') const isLoading = ref(false) // 计算属性 const userTasks = computed(() => taskStore.tasks.filter(t => t.userId === props.userId) ) // 方法 const addTask = async () => { // 方法实现 } </script> <style scoped> /* 样式内容 */ </style>14. 模板安全最佳实践
14.1 防止XSS攻击
在动态渲染内容时,注意防范XSS攻击:
<template> <!-- 安全方式 --> <div v-html="sanitizedContent"></div> <!-- 危险方式 --> <div v-html="userProvidedContent"></div> </template> <script setup> import DOMPurify from 'dompurify' const userProvidedContent = ref('<script>恶意代码<\/script>') const sanitizedContent = computed(() => DOMPurify.sanitize(userProvidedContent.value) ) </script>14.2 输入验证与过滤
对所有用户输入进行验证:
// 表单验证工具函数 export function validateInput(input, rules) { const errors = [] if (rules.required && !input.trim()) { errors.push('必填字段') } if (rules.minLength && input.length < rules.minLength) { errors.push(`至少需要${rules.minLength}个字符`) } if (rules.pattern && !rules.pattern.test(input)) { errors.push('格式不正确') } return errors } // 在组件中使用 const username = ref('') const usernameErrors = ref([]) const validateUsername = () => { usernameErrors.value = validateInput(username.value, { required: true, minLength: 3, pattern: /^[a-zA-Z0-9_]+$/ }) }15. 国际化与可访问性
15.1 多语言支持
实现模板国际化:
<template> <div> <h1>{{ $t('welcome') }}</h1> <p>{{ $t('description') }}</p> <select v-model="locale"> <option value="en">English</option> <option value="zh">中文</option> </select> </div> </template> <script setup> import { useI18n } from 'vue-i18n' const { locale } = useI18n() </script>15.2 可访问性增强
改进模板可访问性:
<template> <div role="main"> <h1 id="page-title">任务管理</h1> <form @submit.prevent="addTask" aria-labelledby="task-form-title" > <h2 id="task-form-title">添加新任务</h2> <label for="task-input">任务描述</label> <input id="task-input" v-model="newTask" aria-required="true" :aria-invalid="!!taskError" aria-describedby="task-error" > <span id="task-error" v-if="taskError" class="error"> {{ taskError }} </span> <button type="submit"> 添加 </button> </form> </div> </template>16. 模板调试与错误处理
16.1 错误边界处理
使用错误边界组件捕获模板错误:
<template> <ErrorBoundary> <ComponentThatMightThrow /> </ErrorBoundary> </template> <script setup> import { onErrorCaptured, ref } from 'vue' const error = ref(null) onErrorCaptured((err) => { error.value = err return false // 阻止错误继续向上传播 }) </script>16.2 开发环境辅助工具
添加开发环境专用功能:
<template> <div> <!-- 开发环境专用调试按钮 --> <button v-if="isDev" @click="fillMockData" class="debug-btn" > 填充测试数据 </button> <!-- 主应用内容 --> </div> </template> <script setup> const isDev = import.meta.env.MODE === 'development' const fillMockData = () => { if (isDev) { // 填充测试数据逻辑 } } </script>17. 模板代码生成与自动化
17.1 基于配置生成模板
对于高度重复的模板代码,可以考虑生成:
// 生成表单字段的实用函数 function generateFormField(fieldConfig) { return ` <div class="form-group"> <label for="${fieldConfig.id}">${fieldConfig.label}</label> <input type="${fieldConfig.type || 'text'}" id="${fieldConfig.id}" v-model="formData.${fieldConfig.name}" ${fieldConfig.required ? 'required' : ''} > </div> ` } // 使用示例 const formTemplate = [ { id: 'username', name: 'username', label: '用户名', required: true }, { id: 'email', name: 'email', label: '邮箱', type: 'email' } ].map(generateFormField).join('')17.2 代码片段与快捷方式
利用编辑器片段提高模板编写效率:
// VS Code snippets.json { "Vue Template with Setup": { "prefix": "vtemplate", "body": [ "<template>", " <div>", " $1", " </div>", "</template>", "", "<script setup>", "$2", "</script>", "", "<style scoped>", "$3", "</style>" ] } }18. 未来趋势与演进
18.1 组合式API最佳实践
随着组合式API的普及,推荐将模板相关逻辑组织为可复用的组合:
// useForm.js export function useForm(initialData, validationRules) { const formData = reactive({ ...initialData }) const errors = reactive({}) const isSubmitting = ref(false) const validate = () => { let isValid = true for (const [key, rules] of Object.entries(validationRules)) { const error = validateField(formData[key], rules) errors[key] = error if (error) isValid = false } return isValid } const handleSubmit = async (submitFn) => { if (!validate()) return false isSubmitting.value = true try { await submitFn(formData) return true } catch (error) { console.error('提交失败:', error) return false } finally { isSubmitting.value = false } } return { formData, errors, isSubmitting, handleSubmit } }18.2 编译时优化探索
了解Vue 3的编译时优化可以写出更高效的模板:
<!-- 编译时静态提升的优化写法 --> <template> <div> <!-- 静态内容会被提升 --> <header> <h1>应用标题</h1> </header> <!-- 动态内容 --> <main> {{ dynamicContent }} </main> </div> </template>19. 模板设计模式
19.1 容器与展示组件模式
分离逻辑与展示:
<!-- 容器组件 TasksContainer.vue --> <template> <TasksList :tasks="filteredTasks" @add="handleAdd" @remove="handleRemove" /> </template> <script setup> import { computed } from 'vue' import { useTaskStore } from '@/stores/tasks' import TasksList from './TasksList.vue' const taskStore = useTaskStore() const filteredTasks = computed(() => taskStore.tasks) const handleAdd = (text) => taskStore.addTask(text) const handleRemove = (id) => taskStore.removeTask(id) </script> <!-- 展示组件 TasksList.vue --> <template> <div> <form @submit.prevent="$emit('add', newTask)"> <input v-model="newTask"> </form> <ul> <li v-for="task in tasks" :key="task.id"> {{ task.text }} <button @click="$emit('remove', task.id)">删除</button> </li> </ul> </div> </template> <script setup> import { ref } from 'vue' const newTask = ref('') defineProps(['tasks']) defineEmits(['add', 'remove']) </script>19.2 渲染委托模式
将复杂渲染逻辑委托给子组件:
<!-- 父组件 --> <template> <DataRenderer :data="complexData"> <template #default="{ item }"> <div class="custom-render"> {{ item.name }} - {{ item.value }} </div> </template> </DataRenderer> </template> <!-- DataRenderer.vue --> <template> <div> <div v-for="item in processedData" :key="item.id"> <slot :item="item" /> </div> </div> </template> <script setup> import { computed } from 'vue' const props = defineProps(['data']) const processedData = computed(() => { // 复杂数据处理逻辑 return props.data.map(item => ({ ...item, processed: true })) }) </script>20. 模板工程化实践
20.1 自动化测试策略
为模板编写全面的测试:
import { mount } from '@vue/test-utils' import TaskList from './TaskList.vue' describe('TaskList', () => { it('renders tasks correctly', () => { const wrapper = mount(TaskList, { props: { tasks: [ { id: 1, text: 'Task 1' }, { id: 2, text: 'Task 2' } ] } }) expect(wrapper.findAll('li')).toHaveLength(2) expect(wrapper.text()).toContain('Task 1') expect(wrapper.text()).toContain