Vue.js: 模板语法
最后更新:2026-08-26
Vue 的模板语法是声明式的 HTML 扩展——在普通 HTML 之上加了一些"指令"和"插值",让你能把数据渲染到 DOM 上。整个模板语法和 HTML 90% 相似,学过 HTML 的人 30 分钟就能上手。
Vue 模板的核心是 3 个动作:插值(显示数据)+ 指令(绑定属性/事件)+ 表达式(运行 JS)。本课带你掌握这 3 个动作的标准写法。
1. 你将学到
- Mustache 插值
{{ }}的 4 种用法 - v-text、v-html、v-once 3 个文本指令的区别
- v-bind 属性绑定的缩写
:和对象语法 - v-on 事件绑定的缩写
@和修饰符 - 模板中可用的 JS 表达式限制(不能写语句)
- v-pre、v-cloak 2 个边界指令的用途
2. 一个电商商品卡片的渲染需求
(1) 痛点:30 个商品,复制粘贴 30 次?
Alice 正在构建一个电商后台。她需要展示 30 个商品卡片。最初的做法:
HTML
<!-- 复制粘贴 30 次,噩梦... -->
<div class="product-card">
<h3>iPhone 15 Pro</h3>
<p>Price: $999</p>
<button>Edit</button>
</div>
<div class="product-card">
<h3>MacBook Pro</h3>
<p>Price: $2499</p>
<button>Edit</button>
</div>
<!-- ... 还有 28 个 ... -->
产品经理 Charlie 说"把价格格式改成带货币符号":
"Alice,你不可能手动改 30 个文件。下个月要是 5,000 个商品呢?我们需要动态模板。"
(2) Vue 模板解法:1 个组件,30 个数据
VUE
<template>
<div v-for="product in products" :key="product.id" class="product-card">
<h3>{{ product.name }}</h3>
<p>Price: ${{ product.price }}</p>
<button @click="editProduct(product.id)">Edit</button>
</div>
</template>
<script setup>
const products = ref([
{ id: 1, name: 'iPhone 15 Pro', price: 999 },
{ id: 2, name: 'MacBook Pro', price: 2499 },
// ... 30 个商品只需 1 行 1 个 ...
])
function editProduct(id) {
console.log('Editing product', id)
}
</script>
5 行模板代码 × 30 个商品 = 0 复制粘贴。当产品名/价格变化时,Vue 自动更新 DOM。
(3) 收益
使用 Vue 模板后:
- 代码量:30 × 15 行 = 450 行 → 1 个模板 + 30 条数据 = 60 行(-87%)
- 维护成本:改 1 个模板样式 → 30 个商品同步更新
- 新增商品:加 1 行数据 → 模板自动渲染
3. 插值:Mustache {{ }}
(1) 4 种插值写法
| 写法 | 效果 | 示例 |
|---|---|---|
{{ var }} |
显示变量值 | <p>{{ username }}</p> → Alice |
{{ expr }} |
显示 JS 表达式结果 | <p>{{ count + 1 }}</p> → 6 |
{{ fn() }} |
调用函数返回值 | <p>{{ formatDate(date) }}</p> → 2026-07-02 |
{{ obj.prop }} |
访问对象属性 | <p>{{ user.name }}</p> → Alice |
(2) 插值 5 大特性
VUE
<template>
<!-- 1. 自动响应式:变量变化时自动更新 -->
<p>{{ message }}</p>
<!-- 2. 支持 JS 表达式(不能写语句)-->
<p>{{ count * 2 + 1 }}</p>
<p>{{ isVip ? 'VIP' : 'Regular' }}</p>
<p>{{ items.length > 0 ? items[0].name : 'Empty' }}</p>
<!-- 3. 支持三元表达式、算术、函数调用 -->
<p>{{ new Date().getFullYear() }}</p>
<!-- 4. 不支持 if/for/变量声明(用 v-if/v-for 替代)-->
<!-- ❌ {{ if (x) { y } }} // 错 -->
<!-- ✅ <p v-if="x">y</p> // 对 -->
<!-- 5. 支持模板字符串 -->
<p>{{ `Hello, ${name}!` }}</p>
</template>
(3) 插值 vs v-text 区别
VUE
<template>
<!-- 插值:可以拼接其他内容 -->
<p>Welcome, {{ name }}!</p>
<!-- v-text:替换整个元素内容 -->
<p v-text="`Welcome, ${name}!`"></p>
<!-- 上面等价于 <p>Welcome, {{ name }}!</p> -->
</template>
4. 文本指令:v-text / v-html / v-once
(1) 三指令对比
| 指令 | 作用 | 场景 | 安全性 |
|---|---|---|---|
{{ }} 插值 |
文本插值 | 默认首选 | ✅ 安全 |
v-text |
设置元素 textContent | 替代插值的指令写法 | ✅ 安全 |
v-html |
设置元素 innerHTML | 渲染富文本(慎用) | ⚠️ XSS 风险 |
v-once |
只渲染一次,后续不更新 | 静态优化 | ✅ 安全 |
(2) v-html 警告
VUE
<template>
<!-- ✅ 安全:自己控制的内容 -->
<div v-html="markdownHtml"></div>
<!-- ❌ 危险:用户输入的 HTML -->
<div v-html="userComment"></div>
<!-- 用户输入 <script>alert('hacked')</script> 会执行! -->
<!-- ✅ 推荐:用 DOMPurify 清理 -->
<div v-html="DOMPurify.sanitize(userComment)"></div>
</template>
(3) v-once 性能优化
VUE
<template>
<!-- 这个 productId 不会变 → 用 v-once 减少响应式开销 -->
<span v-once>{{ productId }}</span>
<!-- 但这个 currentPrice 会变 → 不要用 v-once -->
<span>{{ currentPrice }}</span>
</template>
5. 属性绑定:v-bind 与缩写 :
(1) v-bind 基础
VUE
<template>
<!-- 完整写法 -->
<img v-bind:src="imageUrl" v-bind:alt="imageAlt">
<!-- 缩写(推荐):去掉 v-bind,只剩 : -->
<img :src="imageUrl" :alt="imageAlt">
<!-- 动态属性名(v-bind 特殊语法)-->
<img :[dynamicAttr]="value">
</template>
(2) class 与 style 绑定(重点)
VUE
<template>
<!-- 1. 对象语法:根据条件切换 class -->
<div :class="{ active: isActive, 'text-danger': hasError }">
Conditional classes
</div>
<!-- 2. 数组语法:同时应用多个 class -->
<div :class="[activeClass, errorClass]">
Multiple classes
</div>
<!-- 3. 三元表达式(数组 + 对象)-->
<div :class="[isActive ? 'active' : '', { 'has-error': hasError }]">
Mixed
</div>
<!-- 4. 数组中嵌套对象 -->
<div :class="[{ active: isActive }, errorClass]">
Nested
</div>
</template>
(3) style 绑定
VUE
<template>
<!-- 1. 对象语法 -->
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }">
Styled
</div>
<!-- 2. 数组语法(合并多个 style 对象)-->
<div :style="[baseStyles, overridingStyles]">
Multi styles
</div>
<!-- 3. CSS 变量(Vue 3 推荐)-->
<div :style="{ '--main-color': mainColor }">
With CSS variables
</div>
</template>
6. 事件绑定:v-on 与缩写 @
(1) v-on 基础
VUE
<template>
<!-- 完整写法 -->
<button v-on:click="handleClick">Click</button>
<!-- 缩写(推荐)-->
<button @click="handleClick">Click</button>
<!-- 内联处理器(简单逻辑直接写)-->
<button @click="count++">Increment</button>
<!-- 传递参数 -->
<button @click="handleClick('arg1', $event)">Click</button>
</template>
(2) 常用事件修饰符(9 个)
VUE
<template>
<!-- 1. .stop:阻止冒泡(e.stopPropagation())-->
<button @click.stop="handleClick">Stop propagation</button>
<!-- 2. .prevent:阻止默认行为(e.preventDefault())-->
<form @submit.prevent="handleSubmit">Submit</form>
<!-- 3. .once:只触发一次 -->
<button @click.once="handleClick">Click once</button>
<!-- 4. .self:只有 event.target 是当前元素才触发 -->
<div @click.self="handleClick">Self only</div>
<!-- 5. .capture:使用捕获阶段 -->
<div @click.capture="handleClick">Capture</div>
<!-- 6. .passive:不阻止默认行为(性能优化)-->
<div @scroll.passive="onScroll">Passive scroll</div>
<!-- 7. 键盘修饰符 -->
<input @keyup.enter="submit"> <!-- Enter 键 -->
<input @keyup.esc="cancel"> <!-- Esc 键 -->
<input @keyup.tab="nextField"> <!-- Tab 键 -->
<input @keyup.delete="deleteItem"> <!-- Delete 键 -->
<!-- 8. 鼠标修饰符 -->
<button @click.left="leftClick">Left</button>
<button @click.right.prevent="rightClick">No right click</button>
<button @click.middle="middleClick">Middle</button>
<!-- 9. 系统修饰符(组合键)-->
<button @click.ctrl="save">Ctrl + Click</button>
<button @click.ctrl.exact="save">精确 Ctrl(不能加其他键)</button>
</template>
(3) 修饰符链
VUE
<template>
<!-- 多个修饰符组合(按顺序执行)-->
<button @click.stop.prevent="handle">Stop + Prevent</button>
<!-- 实际等价于 -->
<button @click="(e) => { e.stopPropagation(); e.preventDefault(); handle(); }">
Manual
</button>
</template>
7. 表达式限制(不能写语句)
(1) 模板表达式 vs JS 语句
VUE
<template>
<!-- ✅ 可以的(表达式,有返回值)-->
<p>{{ count + 1 }}</p>
<p>{{ isVip ? 'VIP' : 'Regular' }}</p>
<p>{{ items.filter(i => i.active).length }}</p>
<!-- ❌ 不行的(语句,无返回值)-->
<!-- <p>{{ const x = 1 }}</p> // 错:变量声明 -->
<!-- <p>{{ if (x) { y } }}</p> // 错:if 语句 -->
<!-- <p>{{ for (i of arr) {} }}</p> // 错:for 循环 -->
<!-- ✅ 改用 v-if / v-for 指令 -->
<p v-if="x">y</p>
<div v-for="i in arr" :key="i.id">{{ i.name }}</div>
</template>
(2) 表达式 vs 计算属性
| 场景 | 用表达式 | 用计算属性 |
|---|---|---|
| 简单计算(1-2 个运算符) | ✅ | ❌ |
| 复杂逻辑(多步、循环) | ❌ | ✅ |
| 需要缓存 | ❌ | ✅ |
| 需要传参数 | ✅ | ❌(用方法代替) |
VUE
<template>
<!-- 简单:表达式 -->
<p>{{ price * quantity }}</p>
<!-- 复杂:计算属性(computed)-->
<p>{{ totalWithTax }}</p>
</template>
8. 边界指令:v-pre / v-cloak
(1) v-pre:跳过编译
VUE
<template>
<!-- 跳过这个元素的编译,{{ }} 原样显示 -->
<span v-pre>{{ this is literal, not interpolated }}</span>
</template>
场景:展示 Vue 模板语法本身(如教学、文档站)。
(2) v-cloak:编译前隐藏
VUE
<template>
<!-- v-cloak 让元素在编译完成前不可见 -->
<p v-cloak>{{ message }}</p>
</template>
<style>
[v-cloak] { display: none; }
</style>
场景:防止编译前用户看到 {{ message }} 闪烁。
9. 完整示例:动态商品卡片
▶ 示例:基础商品卡片
HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id="app">
<div class="product-card">
<h3>{{ product.name }}</h3>
<p class="price">${{ product.price }}</p>
<span :class="['badge', stockStatusClass]">
{{ stockStatusText }}
</span>
<button :disabled="product.stock === 0" @click="addToCart">
Add to Cart
</button>
</div>
</div>
<script>
const { createApp, ref, computed } = Vue
createApp({
setup() {
const product = ref({ name: 'iPhone 15 Pro', price: 999, stock: 5 })
const stockStatusClass = computed(() => {
if (product.value.stock === 0) return 'out-of-stock'
if (product.value.stock < 10) return 'low-stock'
return 'in-stock'
})
const stockStatusText = computed(() => {
if (product.value.stock === 0) return 'Out of Stock'
if (product.value.stock < 10) return `Only ${product.value.stock} left`
return 'In Stock'
})
function addToCart() {
console.log('Added', product.value.name)
}
return { product, stockStatusClass, stockStatusText, addToCart }
}
}).mount('#app')
</script>
▶ 示例:多种插值写法
HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id="app">
<!-- 变量插值 -->
<p>变量: {{ username }}</p>
<!-- 表达式插值 -->
<p>表达式: {{ count * 2 + 1 }}</p>
<!-- 三元表达式 -->
<p v-if="score >= 60">Passed (≥60)</p>
<p v-else>Failed (<60)</p>
<!-- 函数调用 -->
<p>日期: {{ formatDate(new Date()) }}</p>
<!-- 对象访问 -->
<p>用户: {{ user.profile.name }}</p>
</div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const username = ref('Alice')
const count = ref(5)
const score = ref(75)
const user = ref({ profile: { name: 'Alice' } })
function formatDate(d) {
return d.toISOString().slice(0, 10)
}
return { username, count, score, user, formatDate }
}
}).mount('#app')
</script>
▶ 示例:5 种 v-bind 写法
HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id="app">
<!-- 1. 基础绑定 -->
<p>1. 基础: <img :src="imageUrl" width="50"></p>
<!-- 2. 动态属性名 -->
<p>2. 动态属性: <img :[attrName]="value" width="50"></p>
<!-- 3. 对象语法(class)-->
<p>3. 对象语法: <span :class="{ active: isActive }">A</span></p>
<!-- 4. 数组语法(class)-->
<p>4. 数组语法: <span :class="[activeClass, errorClass]">B</span></p>
<!-- 5. 字符串拼接(href)-->
<p>5. 字符串拼接: <a :href="`https://example.com/users/${userId}`">Profile</a></p>
</div>
<script>
const { createApp, ref, computed } = Vue
createApp({
setup() {
const imageUrl = ref('https://via.placeholder.com/50')
const attrName = ref('alt')
const value = ref('Logo')
const isActive = ref(true)
const activeClass = ref('active')
const errorClass = ref('error')
const userId = ref(1001)
return { imageUrl, attrName, value, isActive, activeClass, errorClass, userId }
}
}).mount('#app')
</script>
▶ 示例:5 种 v-on 写法
HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id="app">
<!-- 1. 简单点击 -->
<p><button @click="handleClick">1. Click</button></p>
<!-- 2. 内联语句 -->
<p><button @click="count++">2. + (count: {{ count }})</button></p>
<!-- 3. 传参 + event 对象 -->
<p><button @click="handleDelete(item.id, $event)">3. Delete {{ item.name }}</button></p>
<!-- 4. 修饰符链 -->
<p><button @click.stop.prevent="handle">4. Save (阻止冒泡+默认行为)</button></p>
<!-- 5. 键盘组合 -->
<p><input @keyup.ctrl.enter="submit" placeholder="5. Ctrl+Enter 提交" value=""></p>
</div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const count = ref(0)
const item = ref({ id: 1, name: 'Item A' })
function handleClick() { console.log('clicked') }
function handleDelete(id, evt) { console.log('delete', id, evt) }
function handle() { console.log('Save 按钮被点击') }
function submit() { console.log('Ctrl+Enter 提交') }
return { count, item, handleClick, handleDelete, handle, submit }
}
}).mount('#app')
</script>
▶ 示例:模板 vs HTML 速查
| 元素 | HTML 写法 | Vue 写法 | 说明 |
|---|---|---|---|
| 静态文本 | <p>Hello</p> |
<p>{{ msg }}</p> |
改动态 |
| 静态属性 | <img src="x.jpg"> |
<img :src="url"> |
改动态 |
| 静态 class | <div class="box"> |
<div :class="cls"> |
改动态 |
| 静态 style | <div style="color: red"> |
<div :style="sty"> |
改动态 |
| 点击事件 | <button onclick="fn()"> |
<button @click="fn"> |
改 Vue |
| 条件渲染 | 无 | <div v-if="ok"> |
Vue 独有 |
| 列表渲染 | 无 | <div v-for="i in list"> |
Vue 独有 |
▶ 示例:5 个常见错误
| 错误 | 现象 | 解决 |
|---|---|---|
忘记 .value |
模板里写 {{ count }},JS 里写 count.value |
模板自动解包,JS 需 .value |
写 if/for 在 {{ }} |
编译报错 | 用 v-if / v-for 指令 |
| v-html XSS | 用户输入 <script> 执行 |
用 DOMPurify 清理 |
| 内联函数未传 $event | 拿不到 event 对象 | @click="fn($event)" 显式传 |
| 修饰符顺序错 | 阻止冒泡失败 | .stop 写在 .prevent 前(按需) |
❓ 常见问题
Q
{{ }} 和 v-text 有什么区别?A 效果一样,都是设置元素的 textContent。区别是
{{ }} 可以和静态文本混用(<p>Hi, {{ name }}</p>),v-text 必须独占整个元素(<p v-text="msg"></p>)。推荐用 {{ }},更灵活。Q v-html 安全吗?
A 不安全。v-html 会把字符串作为 HTML 解析,恶意用户可以注入
<script> 攻击你的站点。除非你 100% 信任数据源(如自己写的 Markdown 渲染),否则用 {{ }} 插值或先 DOMPurify.sanitize() 清理。Q v-on 修饰符链的执行顺序?
A 从左到右。例如
@click.stop.prevent 先 stopPropagation() 再 preventDefault()。Q v-bind 可以缩写,v-on 也能吗?
A v-bind 缩写是
:(去掉 v-bind),v-on 缩写是 @(推荐用)。两个缩写都是 Vue 2 起就支持的官方写法,IDE 高亮和自动补全都支持。Q 模板里能写 if/else 吗?
A 不能。模板表达式要求有返回值,if/for 是语句不是表达式。用 v-if / v-else-if / v-else 指令实现条件渲染。
Q v-bind 怎么动态切换多个 class?
A 用对象语法
:class="{ active: isActive, 'text-danger': hasError }"。或数组语法 :class="[activeClass, errorClass]"。两种可以混用::class="[isActive && 'active', { 'error': hasError }]"。Q 怎么监听键盘 Ctrl+S?
A 用系统修饰符
@keyup.ctrl.s="save" 或 @keydown.ctrl.s.prevent="save"(加 .prevent 阻止浏览器默认保存对话框)。Vue 3 还支持精确修饰符 .exact,如 @click.ctrl.exact 要求只按 Ctrl 不能加其他键。📖 小节
- Vue 模板语法是声明式 HTML 扩展,核心是插值 + 指令 + 表达式
- Mustache 插值
{{ }}支持变量、表达式、函数调用、三元运算 - 模板表达式不能写 if/for/变量声明(用 v-if / v-for 指令)
- v-text 替换整个元素文本,v-html 渲染富文本(XSS 风险)
- v-once 性能优化(静态内容只渲染一次),v-pre 跳过编译
- v-bind 缩写
:,v-on 缩写@,3 个常用 class 绑定方式 - 9 个事件修饰符:.stop / .prevent / .once / .self / .capture / .passive + 键盘/鼠标/系统
- 模板表达式 vs 计算属性的选择:简单用表达式,复杂用 computed
📝 作业
-
基础题(难度⭐) 写一个 Vue 3 组件,渲染一个用户卡片,包含:
- 用户名(插值)
- 头像(v-bind)
- "Follow" 按钮(v-on)
- 在线/离线状态(class 动态切换)
数据用:用户对象
{ name: 'Alice', avatar: 'alice.jpg', online: true } -
进阶题(难度⭐⭐) 实现一个简单的 Todo 列表:
- 输入框(v-model 暂不用,v-bind:value + @input 模拟)
- 添加按钮(@click + 表达式)
- 列表渲染(v-for,下一课学)
- 每项有删除按钮(@click + 传参)
数据:
const todos = ref(['Learn Vue', 'Build app', 'Deploy']) -
挑战题(难度⭐⭐⭐) 写一个完整的商品详情卡片组件,支持:
- 5 种商品状态徽章(在售/缺货/预售/促销/下架)
- 5 种状态对应不同 class 颜色(用对象语法批量绑定)
- 数量加减按钮(@click 累加/累减)
- 数量 ≥ 库存时禁用"加入购物车"按钮(:disabled 动态)
- 至少 3 个事件修饰符(@click.stop / @submit.prevent 等)