Vue.js: 模板语法

最后更新:2026-08-26

Vue 的模板语法是声明式的 HTML 扩展——在普通 HTML 之上加了一些"指令"和"插值",让你能把数据渲染到 DOM 上。整个模板语法和 HTML 90% 相似,学过 HTML 的人 30 分钟就能上手。

Vue 模板的核心是 3 个动作:插值(显示数据)+ 指令(绑定属性/事件)+ 表达式(运行 JS)。本课带你掌握这 3 个动作的标准写法。

1. 你将学到


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 模板后:


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 不能加其他键。

📖 小节


📝 作业

  1. 基础题(难度⭐) 写一个 Vue 3 组件,渲染一个用户卡片,包含:

    • 用户名(插值)
    • 头像(v-bind)
    • "Follow" 按钮(v-on)
    • 在线/离线状态(class 动态切换)

    数据用:用户对象 { name: 'Alice', avatar: 'alice.jpg', online: true }

  2. 进阶题(难度⭐⭐) 实现一个简单的 Todo 列表:

    • 输入框(v-model 暂不用,v-bind:value + @input 模拟)
    • 添加按钮(@click + 表达式)
    • 列表渲染(v-for,下一课学)
    • 每项有删除按钮(@click + 传参)

    数据:const todos = ref(['Learn Vue', 'Build app', 'Deploy'])

  3. 挑战题(难度⭐⭐⭐) 写一个完整的商品详情卡片组件,支持:

    1. 5 种商品状态徽章(在售/缺货/预售/促销/下架)
    2. 5 种状态对应不同 class 颜色(用对象语法批量绑定)
    3. 数量加减按钮(@click 累加/累减)
    4. 数量 ≥ 库存时禁用"加入购物车"按钮(:disabled 动态)
    5. 至少 3 个事件修饰符(@click.stop / @submit.prevent 等)
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏