本篇定位:模板语法是 Vue 的"视图层语言",是你每天编写 Vue 代码时打交道最多的部分。本篇将系统讲解 Vue 模板中的所有语法规则——从简单的文本插值到强大的指令系统,让你彻底掌握"如何把数据变成用户看到的界面"。读完本篇,你将能熟练阅读和编写任何 Vue 模板代码。
一、模板语法概览
Vue 使用一种基于 HTML 的模板语法,它允许你声明式地将组件实例的数据(JavaScript 状态)绑定到 DOM(页面结构)上。底层原理上,Vue 会将你的模板编译成高效的 JavaScript 渲染函数,并利用响应式系统在数据变化时智能地重新渲染。
简单来说,你只需要在模板中"声明"你想要的结果,Vue 会在背后自动完成所有 DOM 操作。
Vue 模板的两种编写方式:
| 方式 | 说明 | 适用场景 |
|---|---|---|
声明式模板(SFC 中的<template>) | 基于 HTML 扩展,包含指令和插值 | 绝大多数场景,直观、易读、可维护性强 |
渲染函数(render/h函数) | 使用 JavaScript 编程式地描述 VNode | 需要高度动态或逻辑复杂的场景 |
本系列教程默认使用声明式模板,这也是 Vue 官方推荐的主流写法。
Vue 模板中的所有动态能力,都可以归结为两大类:
- 插值(Interpolation)—— 在 HTML 内容中嵌入动态数据
- 指令(Directives)—— 以
v-开头的特殊属性,为 HTML 元素赋予动态行为
下面我们逐项深入学习。
二、文本插值:{{ }}Mustache 语法
最基本的数据绑定形式是文本插值,使用双大括号{{ }}(官方名称:Mustache 语法)。
<script setup lang="ts"> import { ref } from 'vue' const message = ref('Hello, Vue 3!') const count = ref(42) const user = ref({ name: '张三', age: 25 }) </script> <template> <div> <!-- 基本文本插值 --> <p>{{ message }}</p> <p>计数器当前值:{{ count }}</p> <!-- 访问对象属性 --> <p>姓名:{{ user.name }},年龄:{{ user.age }}</p> <!-- 插值中可以执行表达式(见后文详解) --> <p>明年年龄:{{ user.age + 1 }}</p> </div> </template>核心特征:
{{ }}中的内容会被 Vue 解析为 JavaScript 表达式(不是语句)- 当绑定的数据发生变化时,插值内容会自动更新(响应式)
- 插值只能用在文本节点中,不能用在 HTML 属性中(属性绑定需要用
v-bind)
2.1 插值中的表达式规则
{{ }}中只能使用单个 JavaScript 表达式(Expression),不能使用语句(Statement)。
✅ 正确(表达式):
<p>{{ number + 1 }}</p> <p>{{ isActive ? '已激活' : '未激活' }}</p> <p>{{ message.split('').reverse().join('') }}</p> <p>{{ user.name.toUpperCase() }}</p>❌ 错误(语句):
<!-- 赋值语句 --> <p>{{ let a = 1 }}</p> <!-- 流程控制 --> <p>{{ if (isActive) { return '激活' } }}</p> <!-- 声明变量 --> <p>{{ const message = 'hello' }}</p>💡 设计原理:Vue 模板中的表达式在沙箱环境中执行,只能访问受限的全局变量列表(如
Math、Date等),无法访问window对象。这意味着你无法在模板中调用window.alert()或访问自定义全局变量——这是出于安全考虑,强制你将逻辑放在组件代码中。
2.2 插值的 HTML 安全性
Vue 默认会对插值内容进行HTML 转义,以防止 XSS(跨站脚本攻击):
<script setup> const dangerousHtml = ref('<script>alert("恶意代码")<\/script>') </script> <template> <!-- 会被渲染为纯文本字符串,脚本不会执行 --> <p>{{ dangerousHtml }}</p> <!-- 输出:<script>alert("恶意代码")</script> --> </template>如果你明确知道内容是安全的 HTML 并希望直接渲染,需要使用v-html指令(见下文)。
三、原始 HTML:v-html指令
当你需要渲染包含 HTML 标签的字符串时,使用v-html指令:
<script setup lang="ts"> import { ref } from 'vue' const rawHtml = ref('<span style="color: red; font-weight: bold;">这是红色的加粗文字</span>') </script> <template> <div> <!-- 使用 v-html 渲染原始 HTML --> <div v-html="rawHtml"></div> <!-- 对比:使用插值只会显示纯文本 --> <div>{{ rawHtml }}</div> <!-- 输出:<span style="color: red;">这是红色的加粗文字</span> --> </div> </template>⚠️ 安全警告:
v-html的内容会直接插入为 HTML,不会经过 Vue 的模板编译(即其中的{{ }}和指令不会被解析)- 永远不要在
v-html中使用用户提供的内容(如 URL 参数、表单输入等),这会导致严重的 XSS 安全漏洞 - 仅在内容完全可信(如你自己编写的富文本内容)时使用
v-html
最佳实践:优先使用组件化渲染(如渲染函数或 VNode)替代v-html,这样既能保留 Vue 的响应式能力,又能保证安全。
四、属性绑定:v-bind指令
HTML 属性不能使用{{ }}插值,需要使用v-bind指令来动态绑定属性值。
4.1 基本用法
<script setup lang="ts"> import { ref } from 'vue' const dynamicId = ref('main-header') const imageUrl = ref('https://example.com/logo.png') const isDisabled = ref(true) const placeholderText = ref('请输入用户名') </script> <template> <!-- 完整写法:v-bind:属性名="表达式" --> <div v-bind:id="dynamicId">内容区域</div> <!-- 简写语法(最常用)::属性名="表达式" --> <img :src="imageUrl" alt="Logo" /> <input :disabled="isDisabled" :placeholder="placeholderText" /> </template>渲染结果:
<divid="main-header">内容区域</div><imgsrc="https://example.com/logo.png"alt="Logo"/><inputdisabledplaceholder="请输入用户名"/>4.2 动态绑定多个属性(对象语法)
如果你有一个包含多个属性的对象,可以使用v-bind的对象语法一次性全部绑定:
<script setup> import { ref } from 'vue' const attrs = ref({ id: 'submit-btn', class: 'btn primary', disabled: false, 'data-track': 'click-submit' // 带连字符的属性名需要用引号 }) </script> <template> <button v-bind="attrs">提交</button> </template>渲染结果:
<buttonid="submit-btn"class="btn primary"data-track="click-submit">提交</button>这在封装组件时非常有用(如透传未知属性给子组件)。
4.3 布尔类型属性的特殊处理
对于disabled、readonly、checked、selected等布尔属性,v-bind的行为与原生 HTML 一致:
- 值为真值(
true、非空字符串、对象等)时,属性被添加 - 值为假值(
false、null、undefined、空字符串)时,属性被移除
<script setup> const isActive = ref(true) const isEditable = ref(false) const emptyString = ref('') </script> <template> <button :disabled="isActive">禁用状态</button> <!-- 渲染:<button disabled>禁用状态</button> --> <input :readonly="isEditable" /> <!-- 渲染:<input /> (readonly 属性不存在) --> <input :disabled="emptyString" /> <!-- 渲染:<input disabled /> (空字符串是真值!) --> </template>⚠️ 常见陷阱:空字符串''在 JavaScript 中是真值(truthy),所以:disabled="''"会添加disabled属性。如果要表示"不添加该属性",请使用null或undefined。
4.4 动态绑定 class 和 style(高频场景)
class和style是开发中最常绑定的属性,Vue 为它们提供了专门的增强语法。
绑定 class
① 对象语法:根据条件动态切换类名
<script setup> import { ref } from 'vue' const isActive = ref(true) const hasError = ref(false) const isLarge = ref(true) // 可以用计算属性或普通对象 const classObject = { active: true, 'text-danger': false, 'size-large': true } </script> <template> <!-- 单个类名动态切换 --> <div :class="{ active: isActive }">内容</div> <!-- 渲染:<div class="active">内容</div> --> <!-- 多个类名组合 --> <div :class="{ active: isActive, 'text-danger': hasError, 'size-large': isLarge }">状态组件</div> <!-- 渲染:<div class="active size-large">状态组件</div> --> <!-- 对象在代码中定义(推荐复杂逻辑时使用) --> <div :class="classObject">复杂状态</div> </template>② 数组语法:绑定多个类名
<script setup> const isActive = ref(true) const activeClass = ref('active') const errorClass = ref('text-danger') const baseClass = 'btn' const isDisabled = ref(false) </script> <template> <div :class="[baseClass, activeClass, errorClass]">按钮</div> <!-- 渲染:<div class="btn active text-danger">按钮</div> --> <!-- 数组 + 对象混合 --> <div :class="[baseClass, { active: isActive, disabled: isDisabled }]">混合</div> </template>绑定 style
① 对象语法:绑定内联样式
<script setup> const activeColor = ref('red') const fontSize = ref(16) const styleObject = ref({ color: 'blue', backgroundColor: '#f0f0f0', padding: '10px' }) const baseStyle = ref({ color: 'blue', backgroundColor: '#f0f0f0', padding: '10px' }) const overrideStyle = ref({ color: 'red', backgroundColor: '#f0f0f0', padding: '10px' }) </script> <template> <!-- 直接绑定属性 --> <div :style="{ color: activeColor, fontSize: fontSize + 'px' }">文字</div> <!-- 绑定样式对象 --> <div :style="styleObject">样式对象</div> <!-- 数组语法:多个样式对象合并 --> <div :style="[baseStyle, overrideStyle]">合并样式</div> </template>② 自动添加浏览器前缀:Vue 会自动检测并添加必要的浏览器前缀(如-webkit-、-moz-),你无需手动处理。
③ 多值语法(Vue 3.5+ 支持):
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>Vue 会按顺序尝试,直到浏览器支持为止。
五、JavaScript 表达式支持
Vue 模板中所有数据绑定位置({{ }}和指令的属性值)都支持完整的 JavaScript 表达式。
5.1 支持的表达式类型
<script setup> import { ref } from 'vue' const count = ref(10) const price = ref(99.9) const names = ref(['Alice', 'Bob', 'Charlie']) const user = ref({ firstName: '张', lastName: '三' }) const now = new Date() </script> <template> <!-- 算术运算 --> <p>{{ count + 5 }}</p> <!-- 三元运算符 --> <p>{{ count > 10 ? '大于10' : '小于等于10' }}</p> <!-- 方法调用(注意:每次渲染都会重新执行,慎用) --> <p>{{ price.toFixed(2) }}</p> <!-- 数组方法 --> <p>{{ names.join(' - ') }}</p> <!-- 对象解构 --> <p>{{ user.firstName + user.lastName }}</p> <!-- 逻辑运算 --> <div :class="[isActive && 'active', isDisabled || 'enabled']">逻辑</div> </template>5.2 访问全局 API
Vue 模板中可访问的全局变量是受限的,主要包括:
Math、Date、Array、Object、String、Number、Boolean等原生构造器Infinity、undefined、NaN、isFinite、isNaN、parseFloat、parseInt、decodeURI、decodeURIComponent、encodeURI、encodeURIComponent
如果需要访问window上的自定义全局变量,需要在组件中显式暴露。
5.3 设计哲学:为什么限制表达式复杂度?
Vue 推荐复杂逻辑放在计算属性或方法中,而不是写在模板里。原因有三:
- 可读性:模板应该清晰直观,一眼能看出 UI 结构
- 可测试性:计算属性和方法可以独立编写单元测试
- 性能:表达式在每次渲染时重新求值,复杂计算应该被缓存(
computed)
<!-- ❌ 不推荐:模板中写复杂逻辑 --> <div>{{ userList.filter(u => u.age > 18).map(u => u.name).join(', ') }}</div> <!-- ✅ 推荐:逻辑放在计算属性中 --> <script setup> import { computed } from 'vue' const adultNames = computed(() => userList.value.filter(u => u.age > 18).map(u => u.name).join(', ') ) </script> <template> <div>{{ adultNames }}</div> </template>六、指令系统概览
Vue 的指令(Directives)是带有v-前缀的特殊 HTML 属性。指令的职责是:当其绑定的表达式的值发生变化时,响应式地对 DOM 应用特定的行为。
6.1 指令的完整语法结构
v-指令名 :参数 .修饰符 = "表达式"| 组成部分 | 说明 | 示例 |
|---|---|---|
| 指令名 | 指令的核心标识 | v-on、v-bind、v-for |
| 参数(可选) | 用冒号:指定 | v-bind:href→ 参数是href |
| 修饰符(可选) | 用点.指定,用来改变指令行为 | v-on:click.stop→.stop修饰符阻止事件冒泡 |
| 表达式(部分指令可省略) | 指令的值 | v-if="isVisible"→ 表达式是isVisible |
6.2 核心指令速查表
| 指令 | 简写 | 用途 | 本篇涉及 |
|---|---|---|---|
v-bind | : | 动态绑定 HTML 属性 | ✅ 已讲 |
v-on | @ | 绑定事件监听器 | 第 6 篇 |
v-model | 无 | 双向绑定表单输入 | 第 6 篇 |
v-if/v-else-if/v-else | 无 | 条件渲染 | 第 5 篇 |
v-show | 无 | 基于 CSS 的显示/隐藏 | 第 5 篇 |
v-for | 无 | 列表渲染 | 第 5 篇 |
v-html | 无 | 渲染原始 HTML | ✅ 已讲 |
v-text | 无 | 设置元素的 textContent(较少用,{{ }}更常用) | 本系列不单独展开 |
v-once | 无 | 只渲染一次,后续不更新 | 第 32 篇(性能优化) |
v-memo | 无 | 缓存渲染结果(Vue 3.2+) | 第 32 篇 |
v-pre | 无 | 跳过编译,输出原始内容 | 极少使用 |
v-cloak | 无 | 编译前隐藏未编译的模板(CDN 场景) | 极少使用 |
本系列后续安排:
v-on和v-model将在第 6 篇详解,v-if/v-show/v-for将在第 5 篇详解。
6.3 动态参数(Vue 3 新特性)
指令的参数可以是动态的,用方括号[]包裹:
<script setup> import { ref } from 'vue' const attributeName = ref('href') const url = ref('https://vuejs.org') const eventName = ref('click') </script> <template> <!-- 动态绑定属性名 --> <a :[attributeName]="url">Vue 官网</a> <!-- 等价于 :href="url" --> <!-- 动态事件名 --> <button @[eventName]="handleClick">点击</button> <!-- 等价于 @click="handleClick" --> </template>动态参数的注意事项:
- 动态参数表达式最终必须返回一个字符串(
null表示移除该绑定) - 避免在动态参数中使用非字符串(如对象、数字等),Vue 会发出警告
- 动态参数中不能使用空格和引号,如
:[key + 'Value']是合法的,但:[key value]不合法
6.4 指令的优先级(深入理解)
当一个元素上同时存在多个指令时,Vue 按照以下优先级顺序处理:
v-pre > v-once > v-for > v-if/v-else-if/v-else > v-on > v-bind > v-model > v-show实际影响:
v-for的优先级高于v-if,如果在同一元素上同时使用,v-if会在每次迭代中重复执行(性能浪费)。最佳实践:避免在同一元素上同时使用v-for和v-if,应使用计算属性过滤数据或将v-if放在外层容器上。v-on的优先级高于v-bind,这意味着@click可以覆盖:click绑定(虽然基本不会这么做)。
七、综合示例:一个完整的组件模板
让我们综合运用本篇所学的所有语法,编写一个相对完整的组件模板:
<script setup lang="ts"> import { ref, computed } from 'vue' // 响应式数据 const user = ref({ name: '王小明', age: 28, email: 'wangxm@example.com', isActive: true }) const theme = ref('dark') const customClass = ref('highlight') const userAvatar = ref('https://example.com/avatar.jpg') const isExternal = ref(true) const dynamicEventName = ref('click') // 计算属性 const userDisplayName = computed(() => `${user.value.name}(${user.value.age}岁)` ) // 方法 function getUserCardStyle() { return { backgroundColor: theme.value === 'dark' ? '#1a1a2e' : '#f5f5f5', color: theme.value === 'dark' ? '#ffffff' : '#333333', padding: '20px', borderRadius: '12px' } } function handleAction() { console.log('点击了按钮') } </script> <template> <div :class="['user-card', { active: user.isActive, highlight: customClass }]" :style="getUserCardStyle()" v-bind="{'data-user-id': user.id || 'new'}" > <!-- 文本插值 + 表达式 --> <h2>{{ userDisplayName }}</h2> <!-- 属性绑定 --> <img :src="userAvatar" :alt="`${user.name}的头像`" class="avatar" /> <!-- 原始 HTML(仅在内容可信时使用) --> <div v-html="`<span class='badge'>${user.isActive ? '在线' : '离线'}</span>`"></div> <!-- 动态属性 --> <a :href="`mailto:${user.email}`" :target="isExternal ? '_blank' : '_self'"> {{ user.email }} </a> <!-- 动态参数(高级) --> <button :[dynamicEventName]="handleAction" class="action-btn"> 操作 </button> </div> </template> <style scoped> .user-card { transition: all 0.3s; } .user-card.active { border-left: 4px solid #4caf50; } .user-card.highlight { box-shadow: 0 4px 12px rgba(0,0,0,0.15); } .avatar { width: 60px; height: 60px; border-radius: 50%; } .badge { background: #4caf50; color: white; padding: 2px 8px; border-radius: 12px; } </style>八、本篇小结
| 知识点 | 核心内容 | 关键注意点 |
|---|---|---|
| 文本插值 | {{ expression }} | 只能写表达式,不能写语句;自动转义 HTML |
| 原始 HTML | v-html="expression" | ⚠️ 有 XSS 风险,仅用于可信内容 |
| 属性绑定 | v-bind:attr="expr"简写:attr | 布尔属性用null/undefined移除,空字符串是真值 |
| class 绑定 | 对象语法:class="{ active: isActive }"和数组语法 | 最常用的动态样式绑定方式 |
| style 绑定 | 对象语法:style="{ color: colorVar }" | 自动处理浏览器前缀 |
| 动态参数 | :[dynamicKey]="value" | 表达式必须返回字符串 |
| 指令系统 | v-*前缀,包含参数和修饰符 | 优先级需了解,避免在v-for上同时用v-if |
九、思考与练习
基础练习:在上一节创建的项目中,修改
App.vue的模板,创建一个个人信息卡片。要求:- 使用
ref定义姓名、年龄、职业、头像 URL - 使用插值显示姓名和年龄(年龄后加"岁")
- 使用
v-bind绑定头像src - 使用
:class绑定一个名为card的样式类,使其居中、带阴影
- 使用
陷阱排查:分析以下模板代码,指出所有潜在问题并给出修正方案:
<template> <div> <p>{{ let username = 'admin' }}</p> <p>{{ userInfo }}</p> <div v-html="userInput"></div> <input :disabled="''" /> <div v-for="item in list" v-if="item.isActive"> {{ item.name }} </div> </div> </template>动态参数实验:在 Vue 项目中尝试用动态参数绑定一个属性名,观察渲染结果。思考:如果动态参数的值是
null会发生什么?深度思考:为什么 Vue 模板中的表达式不能访问
window对象?这种设计带来的利弊分别是什么?
下篇预告
第 4 篇我们将深入 Vue 的响应式系统——这是 Vue 最核心、最精髓的部分。你将学到:
ref和reactive的本质区别和选型建议- 为什么需要用
.value访问ref的值 - 模板中自动解包的规则
- 响应式数据在 JavaScript 中如何触发视图更新
准备好揭开 Vue 响应式的神秘面纱!🎯