Vue 的 <template> 标签:不仅仅是包裹容器
前言:被低估的 <template> 标签
很多 Vue 开发者只把 <template> 当作一个"必需的包裹标签",但实际上它功能强大、用途广泛 ,是 Vue 模板系统的核心元素之一。今天我们就来深入探索 <template> 标签的各种妙用,从基础到高级,让你彻底掌握这个 Vue 开发中的"瑞士军刀"。
一、基础篇:为什么需要 <template>?
1.1 Vue 的单根元素限制
vue
<!-- ❌ 错误:多个根元素 -->
<div>标题</div>
<div>内容</div>
<!-- ✅ 正确:使用根元素包裹 -->
<div>
<div>标题</div>
<div>内容</div>
</div>
<!-- ✅ 更好:使用 <template> 作为根(Vue 3)-->
<template>
<div>标题</div>
<div>内容</div>
</template>
Vue 2 vs Vue 3:
- Vue 2 :模板必须有单个根元素
- Vue 3 :可以使用
<template>作为片段根,支持多根节点
1.2 <template> 的特殊性
vue
<!-- 普通元素会在 DOM 中渲染 -->
<div class="wrapper">
<span>内容</span>
</div>
<!-- 渲染结果:<div class="wrapper"><span>内容</span></div> -->
<!-- <template> 不会在 DOM 中渲染 -->
<template>
<span>内容</span>
</template>
<!-- 渲染结果:<span>内容</span> -->
关键特性 :<template> 是虚拟元素,不会被渲染到真实 DOM 中,只起到逻辑包裹的作用。
二、实战篇:<template> 的五大核心用途
2.1 条件渲染(v-if、v-else-if、v-else)
vue
<template>
<div class="user-profile">
<!-- 多个元素的条件渲染 -->
<template v-if="user.isLoading">
<LoadingSpinner />
<p>加载中...</p>
</template>
<template v-else-if="user.error">
<ErrorIcon />
<p>{{ user.error }}</p>
<button @click="retry">重试</button>
</template>
<template v-else>
<UserAvatar :src="user.avatar" />
<UserInfo :user="user" />
<UserActions :user="user" />
</template>
<!-- 单个元素通常不需要 template -->
<!-- 但这样写更清晰 -->
<template v-if="showWelcome">
<WelcomeMessage />
</template>
</div>
</template>
优势 :可以条件渲染一组元素,而不需要额外的包装 DOM 节点。
2.2 列表渲染(v-for)
vue
<template>
<div class="shopping-cart">
<!-- 渲染复杂列表项 -->
<template v-for="item in cartItems" :key="item.id">
<!-- 列表项 -->
<div class="cart-item">
<ProductImage :product="item" />
<ProductInfo :product="item" />
<QuantitySelector
:quantity="item.quantity"
@update="updateQuantity(item.id, $event)"
/>
</div>
<!-- 分隔线(除了最后一个) -->
<hr v-if="item !== cartItems[cartItems.length - 1]" />
<!-- 促销提示 -->
<div
v-if="item.hasPromotion"
class="promotion-tip"
>
🎉 此商品参与活动
</div>
</template>
<!-- 空状态 -->
<template v-if="cartItems.length === 0">
<EmptyCartIcon />
<p>购物车是空的</p>
<button @click="goShopping">去逛逛</button>
</template>
</div>
</template>
注意 :<template v-for> 需要手动管理 key,且 key 不能放在 <template> 上:
vue
<!-- ❌ 错误 -->
<template v-for="item in items" :key="item.id">
<div>{{ item.name }}</div>
</template>
<!-- ✅ 正确 -->
<template v-for="item in items">
<div :key="item.id">{{ item.name }}</div>
</template>
<!-- 或者为每个子元素指定 key -->
<template v-for="item in items">
<ProductCard :key="item.id" :product="item" />
<PromotionBanner
v-if="item.hasPromotion"
:key="`promo-${item.id}`"
/>
</template>
2.3 插槽(Slots)系统
基础插槽
vue
<!-- BaseCard.vue -->
<template>
<div class="card">
<!-- 具名插槽 -->
<header class="card-header">
<slot name="header">
<!-- 默认内容 -->
<h3>默认标题</h3>
</slot>
</header>
<!-- 默认插槽 -->
<div class="card-body">
<slot>
<!-- 默认内容 -->
<p>请添加内容</p>
</slot>
</div>
<!-- 作用域插槽 -->
<footer class="card-footer">
<slot name="footer" :data="footerData">
<!-- 默认使用作用域数据 -->
<button @click="handleDefault">
{{ footerData.buttonText }}
</button>
</slot>
</footer>
</div>
</template>
<script>
export default {
data() {
return {
footerData: {
buttonText: '默认按钮',
timestamp: new Date()
}
}
}
}
</script>
使用插槽
vue
<template>
<BaseCard>
<!-- 使用 template 指定插槽 -->
<template #header>
<div class="custom-header">
<h2>自定义标题</h2>
<button @click="close">×</button>
</div>
</template>
<!-- 默认插槽内容 -->
<p>这是卡片的主要内容...</p>
<img src="image.jpg" alt="示例">
<!-- 作用域插槽 -->
<template #footer="{ data }">
<div class="custom-footer">
<span>更新时间: {{ formatTime(data.timestamp) }}</span>
<button @click="customAction">
{{ data.buttonText }}
</button>
</div>
</template>
</BaseCard>
</template>
高级插槽模式
vue
<!-- DataTable.vue -->
<template>
<table class="data-table">
<thead>
<tr>
<!-- 动态列头 -->
<th v-for="column in columns" :key="column.key">
<slot :name="`header-${column.key}`" :column="column">
{{ column.title }}
</slot>
</th>
</tr>
</thead>
<tbody>
<template v-for="(row, index) in data" :key="row.id">
<tr :class="{ 'selected': isSelected(row) }">
<!-- 动态单元格 -->
<td v-for="column in columns" :key="column.key">
<slot
:name="`cell-${column.key}`"
:row="row"
:value="row[column.key]"
:index="index"
>
{{ row[column.key] }}
</slot>
</td>
</tr>
<!-- 可展开的行详情 -->
<template v-if="isExpanded(row)">
<tr class="row-details">
<td :colspan="columns.length">
<slot
name="row-details"
:row="row"
:index="index"
>
默认详情内容
</slot>
</td>
</tr>
</template>
</template>
</tbody>
</table>
</template>
2.4 动态组件与 <component>
vue
<template>
<div class="dashboard">
<!-- 动态组件切换 -->
<component :is="currentComponent">
<!-- 向动态组件传递插槽 -->
<template #header>
<h2>{{ componentTitle }}</h2>
</template>
<!-- 默认插槽内容 -->
<p>这是所有组件共享的内容</p>
</component>
<!-- 多个动态组件 -->
<div class="widget-container">
<template v-for="widget in activeWidgets" :key="widget.id">
<component
:is="widget.component"
:config="widget.config"
class="widget"
>
<!-- 为每个组件传递不同的插槽 -->
<template v-if="widget.type === 'chart'" #toolbar>
<ChartToolbar :chart-id="widget.id" />
</template>
</component>
</template>
</div>
</div>
</template>
<script>
export default {
data() {
return {
currentComponent: 'UserProfile',
activeWidgets: [
{ id: 1, component: 'StatsWidget', type: 'stats' },
{ id: 2, component: 'ChartWidget', type: 'chart' },
{ id: 3, component: 'TaskListWidget', type: 'list' }
]
}
},
computed: {
componentTitle() {
const titles = {
UserProfile: '用户资料',
Settings: '设置',
Analytics: '分析'
}
return titles[this.currentComponent] || '未知'
}
}
}
</script>
2.5 过渡与动画(<transition>、<transition-group>)
vue
<template>
<div class="notification-center">
<!-- 单个元素过渡 -->
<transition name="fade" mode="out-in">
<template v-if="showWelcome">
<WelcomeMessage />
</template>
<template v-else>
<DailyTip />
</template>
</transition>
<!-- 列表过渡 -->
<transition-group
name="list"
tag="div"
class="notification-list"
>
<!-- 每组通知使用 template -->
<template v-for="notification in notifications" :key="notification.id">
<!-- 通知项 -->
<div class="notification-item">
<NotificationContent :notification="notification" />
<button
@click="dismiss(notification.id)"
class="dismiss-btn"
>
×
</button>
</div>
<!-- 分隔线(过渡效果更好) -->
<hr v-if="shouldShowDivider(notification)" :key="`divider-${notification.id}`" />
</template>
</transition-group>
<!-- 复杂的多阶段过渡 -->
<transition
@before-enter="beforeEnter"
@enter="enter"
@leave="leave"
:css="false"
>
<template v-if="showComplexAnimation">
<div class="complex-element">
<slot name="animated-content" />
</div>
</template>
</transition>
</div>
</template>
<script>
export default {
methods: {
beforeEnter(el) {
el.style.opacity = 0
el.style.transform = 'translateY(30px)'
},
enter(el, done) {
// 使用 GSAP 或 anime.js 等库
this.$gsap.to(el, {
opacity: 1,
y: 0,
duration: 0.5,
onComplete: done
})
},
leave(el, done) {
this.$gsap.to(el, {
opacity: 0,
y: -30,
duration: 0.3,
onComplete: done
})
}
}
}
</script>
<style>
/* CSS 过渡类 */
.fade-enter-active, .fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
.list-enter-active, .list-leave-active {
transition: all 0.5s;
}
.list-enter, .list-leave-to {
opacity: 0;
transform: translateX(30px);
}
.list-move {
transition: transform 0.5s;
}
</style>
三、高级篇:<template> 的进阶技巧
3.1 指令组合使用
vue
<template>
<div class="product-list">
<!-- v-for 和 v-if 的组合(正确方式) -->
<template v-for="product in products">
<!-- 使用 template 包裹条件判断 -->
<template v-if="shouldShowProduct(product)">
<ProductCard
:key="product.id"
:product="product"
@add-to-cart="addToCart"
/>
<!-- 相关推荐 -->
<template v-if="showRecommendations">
<RelatedProducts
:product-id="product.id"
:key="`related-${product.id}`"
/>
</template>
</template>
<!-- 占位符(骨架屏) -->
<template v-else-if="isLoading">
<ProductSkeleton :key="`skeleton-${product.id}`" />
</template>
</template>
<!-- 多重指令组合 -->
<template v-if="user.isPremium">
<template v-for="feature in premiumFeatures">
<PremiumFeature
v-show="feature.isEnabled"
:key="feature.id"
:feature="feature"
v-tooltip="feature.description"
/>
</template>
</template>
</div>
</template>
3.2 渲染函数与 JSX 对比
vue
<!-- 模板语法 -->
<template>
<div class="container">
<template v-if="hasHeader">
<header class="header">
<slot name="header" />
</header>
</template>
<main class="main">
<slot />
</main>
</div>
</template>
<!-- 等价的渲染函数 -->
<script>
export default {
render(h) {
const children = []
if (this.hasHeader) {
children.push(
h('header', { class: 'header' }, [
this.$slots.header
])
)
}
children.push(
h('main', { class: 'main' }, [
this.$slots.default
])
)
return h('div', { class: 'container' }, children)
}
}
</script>
<!-- 等价的 JSX -->
<script>
export default {
render() {
return (
<div class="container">
{this.hasHeader && (
<header class="header">
{this.$slots.header}
</header>
)}
<main class="main">
{this.$slots.default}
</main>
</div>
)
}
}
</script>
3.3 性能优化:减少不必要的包装
vue
<!-- 优化前:多余的 div 包装 -->
<div class="card">
<div v-if="showImage">
<img :src="imageUrl" alt="图片">
</div>
<div v-if="showTitle">
<h3>{{ title }}</h3>
</div>
<div v-if="showContent">
<p>{{ content }}</p>
</div>
</div>
<!-- 优化后:使用 template 避免额外 DOM -->
<div class="card">
<template v-if="showImage">
<img :src="imageUrl" alt="图片">
</template>
<template v-if="showTitle">
<h3>{{ title }}</h3>
</template>
<template v-if="showContent">
<p>{{ content }}</p>
</template>
</div>
<!-- 渲染结果对比 -->
<!-- 优化前:<div><div><img></div><div><h3></h3></div></div> -->
<!-- 优化后:<div><img><h3></h3></div> -->
3.4 与 CSS 框架的集成
vue
<template>
<!-- Bootstrap 网格系统 -->
<div class="container">
<div class="row">
<template v-for="col in gridColumns" :key="col.id">
<!-- 动态列宽 -->
<div :class="['col', `col-md-${col.span}`]">
<component :is="col.component" :config="col.config">
<!-- 传递具名插槽 -->
<template v-if="col.slots" v-for="(slotContent, slotName) in col.slots">
<template :slot="slotName">
{{ slotContent }}
</template>
</template>
</component>
</div>
</template>
</div>
</div>
<!-- Tailwind CSS 样式 -->
<div class="space-y-4">
<template v-for="item in listItems" :key="item.id">
<div
:class="[
'p-4 rounded-lg',
item.isActive ? 'bg-blue-100' : 'bg-gray-100'
]"
>
<h3 class="text-lg font-semibold">{{ item.title }}</h3>
<p class="text-gray-600">{{ item.description }}</p>
</div>
</template>
</div>
</template>
四、Vue 3 新特性:<template> 的增强
4.1 多根节点支持(Fragments)
vue
<!-- Vue 2:需要包装元素 -->
<template>
<div> <!-- 多余的 div -->
<header>标题</header>
<main>内容</main>
<footer>页脚</footer>
</div>
</template>
<!-- Vue 3:可以使用多根节点 -->
<template>
<header>标题</header>
<main>内容</main>
<footer>页脚</footer>
</template>
<!-- 或者使用 template 作为逻辑分组 -->
<template>
<template v-if="layout === 'simple'">
<header>简洁标题</header>
<main>主要内容</main>
</template>
<template v-else>
<header>完整标题</header>
<nav>导航菜单</nav>
<main>详细内容</main>
<aside>侧边栏</aside>
<footer>页脚信息</footer>
</template>
</template>
4.2 <script setup> 语法糖
vue
<!-- 组合式 API 的简洁写法 -->
<script setup>
import { ref, computed } from 'vue'
import MyComponent from './MyComponent.vue'
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
</script>
<template>
<!-- 可以直接使用导入的组件 -->
<MyComponent :count="count" />
<!-- 条件渲染 -->
<template v-if="count > 0">
<p>计数大于 0: {{ count }}</p>
</template>
<!-- 具名插槽简写 -->
<slot name="header" />
<!-- 作用域插槽 -->
<slot name="footer" :data="{ count, doubleCount }" />
</template>
4.3 v-memo 指令优化
vue
<template>
<!-- 复杂的渲染优化 -->
<div class="data-grid">
<template v-for="row in largeDataset" :key="row.id">
<!-- 使用 v-memo 避免不必要的重新渲染 -->
<div
v-memo="[row.id, row.version, selectedRowId === row.id]"
:class="['row', { 'selected': selectedRowId === row.id }]"
>
<template v-for="cell in row.cells" :key="cell.key">
<!-- 单元格内容 -->
<div class="cell">
<slot
name="cell"
:row="row"
:cell="cell"
:value="cell.value"
/>
</div>
</template>
</div>
</template>
</div>
</template>
五、最佳实践与性能考量
5.1 何时使用 <template>
| 场景 | 使用 <template> |
不使用 |
|---|---|---|
| 条件渲染多个元素 | ✅ | ❌ |
| 列表渲染复杂项 | ✅ | ❌ |
| 插槽定义与使用 | ✅ | ❌ |
| 单个元素条件渲染 | 可选 | ✅ |
| 简单的列表项 | 可选 | ✅ |
| 需要样式/事件的容器 | ❌ | ✅(用 div) |
5.2 性能优化建议
vue
<!-- 避免深度嵌套 -->
<!-- ❌ 不推荐:多层嵌套 -->
<template v-if="condition1">
<template v-if="condition2">
<template v-for="item in list">
<div>{{ item }}</div>
</template>
</template>
</template>
<!-- ✅ 推荐:简化逻辑 -->
<template v-if="condition1 && condition2">
<div v-for="item in list" :key="item.id">
{{ item }}
</div>
</template>
<!-- 缓存复杂计算 -->
<template>
<!-- 使用计算属性缓存 -->
<template v-if="shouldShowSection">
<ExpensiveComponent />
</template>
<!-- 使用 v-once 静态内容 -->
<template v-once>
<StaticContent />
</template>
</template>
<script>
export default {
computed: {
shouldShowSection() {
// 复杂计算,结果会被缓存
return this.complexCondition1 &&
this.complexCondition2 &&
!this.isLoading
}
}
}
</script>
5.3 可维护性建议
vue
<!-- 组件化复杂模板 -->
<template>
<!-- 主模板保持简洁 -->
<div class="page">
<PageHeader />
<template v-if="isLoggedIn">
<UserDashboard />
</template>
<template v-else>
<GuestWelcome />
</template>
<PageFooter />
</div>
</template>
<!-- 复杂的部分提取为独立组件 -->
<template>
<div class="complex-section">
<!-- 使用组件替代复杂的模板逻辑 -->
<DataTable
:columns="tableColumns"
:data="tableData"
>
<template #header-name="{ column }">
<div class="custom-header">
{{ column.title }}
<HelpTooltip :content="column.description" />
</div>
</template>
<template #cell-status="{ value }">
<StatusBadge :status="value" />
</template>
</DataTable>
</div>
</template>
六、常见问题与解决方案
问题1:<template> 上的 key 属性
vue
<!-- 错误:key 放在 template 上无效 -->
<template v-for="item in items" :key="item.id">
<div>{{ item.name }}</div>
</template>
<!-- 正确:key 放在实际元素上 -->
<template v-for="item in items">
<div :key="item.id">{{ item.name }}</div>
</template>
<!-- 多个元素需要各自的 key -->
<template v-for="item in items">
<ProductCard :key="`card-${item.id}`" :product="item" />
<ProductActions
v-if="showActions"
:key="`actions-${item.id}`"
:product="item"
/>
</template>
问题2:作用域插槽的 v-slot 简写
vue
<!-- 完整写法 -->
<template v-slot:header>
<div>标题</div>
</template>
<!-- 简写 -->
<template #header>
<div>标题</div>
</template>
<!-- 动态插槽名 -->
<template #[dynamicSlotName]>
<div>动态内容</div>
</template>
<!-- 作用域插槽 -->
<template #item="{ data, index }">
<div>索引 {{ index }}: {{ data }}</div>
</template>
问题3:<template> 与 CSS 作用域
vue
<!-- CSS 作用域对 template 无效 -->
<template>
<!-- 这里的 class 不受 scoped CSS 影响 -->
<div class="content">
<p>内容</p>
</div>
</template>
<style scoped>
/* 只会作用于实际渲染的元素 */
.content p {
color: red;
}
</style>
<!-- 如果需要作用域样式,使用实际元素 -->
<div class="wrapper">
<template v-if="condition">
<p class="scoped-text">受作用域影响的文本</p>
</template>
</div>
<style scoped>
.scoped-text {
/* 现在有作用域了 */
color: blue;
}
</style>
七、总结:<template> 的核心价值
<template> 的六大用途
- 条件渲染多个元素:避免多余的包装 DOM
- 列表渲染复杂结构:包含额外元素和逻辑
- 插槽系统的基础:定义和使用插槽内容
- 动态组件容器:包裹动态组件和插槽
- 过渡动画包装:实现复杂的动画效果
- 模板逻辑分组:提高代码可读性和维护性
版本特性总结
| 特性 | Vue 2 | Vue 3 | 说明 |
|---|---|---|---|
| 多根节点 | ❌ | ✅ | Fragment 支持 |
<script setup> |
❌ | ✅ | 语法糖简化 |
v-memo |
❌ | ✅ | 性能优化 |
| 编译优化 | 基础 | 增强 | 更好的静态提升 |
最佳实践清单
- 合理使用:只在需要时使用,避免过度嵌套
- 保持简洁:复杂逻辑考虑提取为组件
- 注意性能:避免在大量循环中使用复杂模板
- 统一风格:团队保持一致的模板编写规范
- 利用新特性:Vue 3 中善用 Fragments 等新功能
记住:<template> 是 Vue 模板系统的骨架 ,它让模板更加灵活、清晰和高效。掌握好 <template> 的使用,能让你的 Vue 代码质量提升一个档次。
思考题 :在你的 Vue 项目中,<template> 标签最让你惊喜的用法是什么?或者有没有遇到过 <template> 相关的坑?欢迎在评论区分享你的经验!