第1章 1.2 模板语法与指令

🎯 开场:为什么要学这个?

上一章我们搞定了 Vue 3 的开发环境,现在你已经有了一个能跑起来的 Vue 项目。

但你有没有这种感觉:看着别人写的 Vue 代码,里面一堆 v-ifv-forv-model... 脑子里全是问号

举个例子,你想让页面根据用户是否登录显示不同内容,结果折腾了半天发现——哎,用 v-if 就行?

这一章就是来解决这个问题的。

学完之后,你将能够:
- 读懂任何 Vue 组件模板里的"火星文"指令
- 写出能根据数据动态变化的页面
- 把上一章的"静态页面"变成"会动"的页面


🧱 基础:核心概念(小白视角)

1. 指令是什么?——模板里的"魔法咒语"

是什么?

指令就是 HTML 标签上的特殊标记,以 v- 开头。它们告诉 Vue:"这个元素要怎么响应数据变化"

生活类比:

想象你是个指挥官,给士兵下达命令:

  • 普通 HTML = 写在纸上的命令(死板的)
  • 带指令的 HTML = 对着对讲机说的命令(活的,能根据情况变)

怎么用?

<p v-text="message"></p>

这行代码在说:"把这个 <p> 标签的文本内容,替换成 message 变量的值。"

2. v-text 和 v-html —— 文字显示的两把刀

是什么?

这两个指令控制标签里的文字内容。

为什么要用?

你想把 JavaScript 里的变量值显示到页面上?用它们就对了。

怎么用?

<template>
<div>
<!-- v-text:纯文本输出,像打字机一样 -->
<p v-text="plainText"></p>

<!-- v-html:解析 HTML,像打印机一样 -->
<div v-html="htmlContent"></div>
</div>
</template>

<script setup>
import { ref } from 'vue'

const plainText = ref('这是一段普通文字,<strong>加粗</strong>不会生效')
const htmlContent = ref('这段文字里 <strong>加粗</strong> 会真正生效')
</script>

输出效果:

这是一段普通文字,<strong>加粗</strong>不会生效
这段文字里 加粗 会真正生效

注意! v-html 有 XSS 风险,永远不要用它显示用户输入的内容!

Simple tech illustration explaining a key concept about "第1章 1.2 模板语法与指令", infographic style with cl

AI comic creation scene, creative workspace with holographic UI panels, soft futuristic aesthetic, n

3. v-show 和 v-if —— 显示/隐藏的两条路

是什么?

都是用来控制元素显示隐藏的,但原理不同。

为什么要用?

页面上很多元素不需要一直显示,比如弹窗、下拉菜单、错误提示...用它们来实现。

生活类比:

  • v-show = 把东西盖住(还在原地,只是看不见)
  • v-if = 把东西搬走(根本不存在了)

怎么用?

<template>
<div>
<!-- v-show:用 CSS display:none 切换,显示频繁时用 -->
<p v-show="isVisible">我是用 v-show 显示的</p>

<!-- v-if:用条件渲染,切换成本高时用(组件初始化、权限切换) -->
<p v-if="hasPermission">我是用 v-if 显示的</p>

<button @click="toggle">点我切换</button>
</div>
</template>

<script setup>
import { ref } from 'vue'

const isVisible = ref(true)
const hasPermission = ref(true)

function toggle() {
isVisible.value = !isVisible.value
}
</script>

4. v-for —— 循环渲染的法宝

是什么?

让一个元素重复渲染N次。

为什么要用?

当你有一组数据要显示时,比如购物清单、用户列表、表格行...总不能写 N 个 <div> 吧?

生活类比:

v-for 就像复印机:给你一个模板,哗哗哗复制 N 份,每份填上不同的内容。

怎么用?

<template>
<div>
<h3>小明的购物清单</h3>
<ul>
  <li v-for="(item, index) in shoppingList" :key="item.id">
    {{ index + 1 }}. {{ item.name }} - ¥{{ item.price }}
  </li>
</ul>
</div>
</template>

<script setup>
import { ref } from 'vue'

const shoppingList = ref([
{ id: 1, name: '牛奶', price: 5.5 },
{ id: 2, name: '面包', price: 8.0 },
{ id: 3, name: '鸡蛋', price: 12.0 }
])
</script>

输出:

1. 牛奶 - ¥5.5
2. 面包 - ¥8.0
3. 鸡蛋 - ¥12.0

敲黑板::key="item.id" 必须写! 用来告诉 Vue 每个节点的唯一身份,关系到性能和问题排查。

5. v-on —— 绑定事件的遥控器

是什么?

给元素绑定点击、输入等事件。

为什么要用?

页面要能响应用户操作啊!点按钮、填表单、滚动页面...全靠它。

怎么用?

<template>
<div>
<p>计数器的值:{{ count }}</p>

<!-- 完整写法 -->
<button v-on:click="increment">+1(完整写法)</button>

<!-- 简写:@click -->
<button @click="decrement">-1(简写)</button>
</div>
</template>

<script setup>
import { ref } from 'vue'

const count = ref(0)

function increment() {
count.value++
}

function decrement() {
count.value--
}
</script>

小贴士: 实际开发中用 @click 简写更香,省手指头。

6. v-bind —— 动态属性的桥梁

是什么?

把 JavaScript 变量的值,绑定到 HTML 属性的"管道"。

为什么要用?

HTML 属性的值经常需要动态变化,比如图片 src、链接 href、样式 class...

生活类比:

v-bind 就像翻译官:把 Vue 世界的话翻译成 HTML 世界的话。

怎么用?

<template>
<div>
<!-- 完整写法:v-bind:属性名 -->
<img v-bind:src="imageUrl" :alt="imageAlt">

<!-- 简写::属性名 -->
<a :href="pageUrl">{{ linkText }}</a>

<!-- 动态 class -->
<p :class="{ active: isActive, 'text-danger': hasError }">
  这段文字样式在动态变化
</p>
</div>
</template>

<script setup>
import { ref } from 'vue'

const imageUrl = ref('https://picsum.photos/200')
const imageAlt = ref('示例图片')
const pageUrl = ref('https://example.com')
const linkText = ref('点击访问')
const isActive = ref(true)
const hasError = ref(false)
</script>

7. v-model —— 双向绑定的魔法

是什么?

把表单输入和数据变量绑在一起,一边变另一边也跟着变。

为什么要用?

做表单处理时太方便了!输入框、复选框、下拉框...再也不用手动 document.getElementById 了。

生活类比:

v-model 就像对讲机:你对着话筒说话,对方同时能听到,双方是同步的。v-bind 只能单向传达。

怎么用?

<template>
<div>
<p>你输入的内容:{{ userInput }}</p>

<!-- 双向绑定:输入框和数据自动同步 -->
<input v-model="userInput" placeholder="随便输入点什么">

<hr>

<p>是否同意协议:{{ agreed }}</p>
<label>
  <input type="checkbox" v-model="agreed">
  我已阅读并同意协议
</label>
</div>
</template>

<script setup>
import { ref } from 'vue'

const userInput = ref('')
const agreed = ref(false)
</script>

试着在输入框打字,你会看到上面的 <p> 标签同步更新!


🔥 实战:3 个递进的小项目

项目 1:动态名片展示(5 分钟)

目标: 用 v-bind 和 v-text 做一个能换肤的名片

<template>
<div class="business-card" :class="themeClass">
<h2 v-text="name"></h2>
<p class="title" v-text="title"></p>
<p class="email" v-text="email"></p>

<div class="theme-switcher">
  <button @click="theme = 'light'">浅色主题</button>
  <button @click="theme = 'dark'">深色主题</button>
  <button @click="theme = 'blue'">蓝色主题</button>
</div>
</div>
</template>

<script setup>
import { ref, computed } from 'vue'

const name = ref('张小明')
const title = ref('全栈工程师')
const email = ref('zhangxiaoming@example.com')
const theme = ref('light')

const themeClass = computed(() => ({
'theme-light': theme.value === 'light',
'theme-dark': theme.value === 'dark',
'theme-blue': theme.value === 'blue'
}))
</script>

<style scoped>
.business-card {
padding: 20px;
border-radius: 10px;
margin: 20px;
transition: all 0.3s;
}
.theme-light { background: #f5f5f5; color: #333; }
.theme-dark { background: #2c3e50; color: #ecf0f1; }
.theme-blue { background: #3498db; color: white; }
.theme-switcher { margin-top: 15px; }
button { margin-right: 10px; padding: 5px 10px; cursor: pointer; }
</style>

预期输出: 点击不同按钮,名片背景色和文字颜色会切换

一句话解释: :class 绑定了一个计算属性,根据 theme 值切换不同的 CSS 类。


项目 2:待办清单(15 分钟)

目标: 用 v-for 渲染列表 + v-model 做输入 + v-if/v-show 处理空状态

<template>
<div class="todo-app">
<h1>小明的待办清单</h1>

<!-- 输入区域 -->
<div class="input-area">
  <input 
    v-model="newTask" 
    @keyup.enter="addTask"
    placeholder="输入新任务,按回车添加"
  >
  <button @click="addTask">添加</button>
</div>

<!-- 统计信息 -->
<p class="stats">
  共 {{ tasks.length }} 项任务,已完成 {{ doneCount }} 项
</p>

<!-- 空状态提示 -->
<p v-if="tasks.length === 0" class="empty-tip">
  清单是空的,添加一个任务吧!
</p>

<!-- 任务列表 -->
<ul class="task-list">
  <li v-for="task in tasks" :key="task.id" :class="{ done: task.completed }">
    <input 
      type="checkbox" 
      :checked="task.completed"
      @change="toggleTask(task.id)"
    >
    <span v-text="task.text"></span>
    <button class="delete-btn" @click="deleteTask(task.id)">删除</button>
  </li>
</ul>

<!-- 批量操作 -->
<div v-show="tasks.length > 0" class="batch-actions">
  <button @click="clearDone">清除已完成</button>
</div>
</div>
</template>

<script setup>
import { ref, computed } from 'vue'

const tasks = ref([
{ id: 1, text: '买菜', completed: false },
{ id: 2, text: '做饭', completed: true },
{ id: 3, text: '写代码', completed: false }
])

const newTask = ref('')
let nextId = 4

function addTask() {
if (!newTask.value.trim()) return

tasks.value.push({
id: nextId++,
text: newTask.value.trim(),
completed: false
})
newTask.value = ''
}

function toggleTask(id) {
const task = tasks.value.find(t => t.id === id)
if (task) task.completed = !task.completed
}

function deleteTask(id) {
tasks.value = tasks.value.filter(t => t.id !== id)
}

function clearDone() {
tasks.value = tasks.value.filter(t => !t.completed)
}

const doneCount = computed(() => tasks.value.filter(t => t.completed).length)
</script>

<style scoped>
.todo-app { max-width: 400px; margin: 30px auto; padding: 20px; }
.input-area { display: flex; gap: 10px; margin-bottom: 15px; }
input { flex: 1; padding: 8px; }
button { padding: 8px 15px; cursor: pointer; }
.stats { color: #666; font-size: 14px; }
.empty-tip { color: #999; font-style: italic; }
.task-list { list-style: none; padding: 0; }
.task-list li { padding: 10px; border-bottom: 1px solid #eee; display: flex; align-items: center; gap: 10px; }
.task-list li.done span { text-decoration: line-through; color: #999; }
.delete-btn { margin-left: auto; color: #e74c3c; }
</style>

预期输出:
- 页面加载显示 3 个预设任务
- 输入框输入内容按回车或点按钮添加新任务
- 勾选复选框切换完成状态
- 点删除移除任务
- 清除已完成按钮批量删除已完成任务

一句话解释: v-model 绑定输入框,v-for 渲染任务列表,v-if/v-show 处理空状态和批量操作按钮。


项目 3:天气小工具(15 分钟)

目标: 综合运用所有指令,做一个能切换城市查看天气的模拟工具

<template>
<div class="weather-app">
<h1>天气预报</h1>

<!-- 城市选择 -->
<div class="city-selector">
  <select v-model="selectedCity">
    <option v-for="city in cities" :key="city.id" :value="city.id">
      {{ city.name }}
    </option>
  </select>
</div>

<!-- 天气卡片 -->
<div v-if="currentWeather" class="weather-card" :class="weatherClass">
  <h2 v-text="currentWeather.cityName"></h2>
  <p class="temperature">{{ currentWeather.temp }}°C</p>
  <p class="condition" v-text="currentWeather.condition"></p>
  <p class="wind">风力:{{ currentWeather.wind }}</p>
</div>

<!-- 穿搭建议 -->
<div v-show="currentWeather" class="suggestion">
  <p>👕 穿搭建议:{{ clothingSuggestion }}</p>
</div>

<!-- 数据来源说明 -->
<p v-if="showSource" class="source">
  数据更新时间:{{ updateTime }}
</p>
</div>
</template>

<script setup>
import { ref, computed } from 'vue'

// 模拟天气数据
const weatherData = {
beijing: { cityName: '北京', temp: 15, condition: '晴', wind: '3-4级' },
shanghai: { cityName: '上海', temp: 22, condition: '多云', wind: '2级' },
guangzhou: { cityName: '广州', temp: 28, condition: '雷阵雨', wind: '4级' },
chengdu: { cityName: '成都', temp: 18, condition: '阴', wind: '1级' }
}

const cities = [
{ id: 'beijing', name: '北京' },
{ id: 'shanghai', name: '上海' },
{ id: 'guangzhou', name: '广州' },
{ id: 'chengdu', name: '成都' }
]

const selectedCity = ref('beijing')
const showSource = ref(true)
const updateTime = ref(new Date().toLocaleString())

const currentWeather = computed(() => weatherData[selectedCity.value])

const weatherClass = computed(() => {
const temp = currentWeather.value.temp
if (temp < 10) return 'cold'
if (temp > 25) return 'hot'
return 'mild'
})

const clothingSuggestion = computed(() => {
const temp = currentWeather.value.temp
const condition = currentWeather.value.condition

if (temp < 10) return '厚外套、毛衣、秋裤'
if (temp < 20) return '薄外套、长袖、牛仔裤'
if (temp < 25) return '短袖、薄裤、舒适鞋'
return '轻薄夏装、防晒、遮阳帽' + (condition.includes('雨') ? ',记得带伞!' : '')
})
</script>

<style scoped>
.weather-app { max-width: 400px; margin: 30px auto; text-align: center; }
.city-selector { margin-bottom: 20px; }
select { padding: 10px 20px; font-size: 16px; }
.weather-card { padding: 30px; border-radius: 15px; color: white; margin-bottom: 20px; }
.weather-card.cold { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
.weather-card.mild { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); }
.weather-card.hot { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); }
.temperature { font-size: 60px; font-weight: bold; margin: 10px 0; }
.condition { font-size: 20px; }
.suggestion { background: #f8f9fa; padding: 15px; border-radius: 10px; text-align: left; }
.source { color: #999; font-size: 12px; margin-top: 20px; }
</style>

预期输出:
- 下拉框选择不同城市
- 天气卡片动态显示对应城市的温度、天气状况、风力
- 卡片颜色根据温度变化(冷/适中/热)
- 穿搭建议根据温度和天气状况动态变化

一句话解释:v-model 绑定城市选择,v-if 控制天气卡片显示,computed 计算穿搭建议。


💪 进阶:常见坑 + 性能小贴士

坑 1:v-for 忘了加 key

<!-- ❌ 错误:没有 key,Vue 会报警告 -->
<li v-for="item in items">{{ item.name }}</li>

<!-- ✅ 正确:加唯一的 key -->
<li v-for="item in items" :key="item.id">{{ item.name }}</li>

坑 2:v-model 绑定的值是基本类型却想修改

<!-- ❌ 错误:直接改 props 会报错 -->
<input v-model="propsValue">

<!-- ✅ 正确:要用 ref/reactive -->
<script setup>
const props = defineProps(['initialValue'])
const localValue = ref(props.initialValue)
</script>
<input v-model="localValue">

坑 3:v-if 和 v-for 一起用

<!-- ❌ 错误:v-for 优先级更高,会先循环再判断,浪费性能 -->
<li v-for="item in items" v-if="item.show">{{ item.name }}</li>

<!-- ✅ 正确:先用计算属性过滤 -->
<li v-for="item in visibleItems" :key="item.id">{{ item.name }}</li>

<script setup>
const visibleItems = computed(() => items.value.filter(i => i.show))
</script>

坑 4:v-html 用了用户输入的内容

<!-- ❌ 危险:XSS 攻击风险! -->
<div v-html="userInput"></div>

<!-- ✅ 安全:永远不要对用户输入用 v-html -->
<div>{{ userInput }}</div>

坑 5:@click 里的函数没写括号

<!-- ❌ 错误:事件对象会作为参数传入 -->
<button @click="handleClick">点击</button>

<!-- ✅ 正确:需要传参时写括号 -->
<button @click="handleClick(123)">点击</button>

<!-- ✅ 正确:同时需要事件对象 -->
<button @click="(e) => handleClick(123, e)">点击</button>

性能小贴士:使用 v-memo 优化列表渲染

对于大列表,当列表项不频繁更新时:

<!-- 只有 item.value 或 item.category 变化时才重新渲染 -->
<div v-for="item in list" v-memo="[item.value, item.category]" :key="item.id">
{{ item.name }}
</div>

调试技巧:Vue DevTools

浏览器装上 Vue DevTools 插件,可以:
- 查看组件的 data、computed
- 时间旅行调试(回看状态变化)
- 性能分析

// 代码里也可以手动打印调试
console.log('当前状态:', myVariable)

// 或者在模板里临时加一段观察
{{ console.log('变化了', myVariable) || '' }}

✏️ 练习题

练习 1(1 分钟):改颜色

题目: 把项目 1 的名片,theme 默认值从 'light' 改成 'blue'

  • 输入:无
  • 预期输出:页面加载时直接显示蓝色主题
  • 提示:ref() 里的初始值就是默认值

练习 2(2 分钟):加条件判断

题目: 在项目 1 名片里,如果名字是"张小明",在名字后面加个"👑"标识。

  • 输入:无
  • 预期输出:名字显示为"张小明👑"
  • 提示::title 属性里可以用三元表达式

练习 3(3 分钟):处理新数据

题目: 给项目 2 的待办清单添加一个"优先级"功能,每条任务有个 priority 属性(1=低,2=中,3=高),高优先级任务显示红色。

  • 输入:添加 { id: 4, text: '重要会议', completed: false, priority: 3 }
  • 预期输出:这条任务显示为红色文字
  • 提示:用动态 class 绑定,根据 priority 值决定是否加 high-priority

练习 4(4 分钟):串起两个项目

题目: 把项目 2 的待办清单改成:当任务数 ≥ 5 时,显示"任务较多,注意休息!"的提示。

  • 输入:待办列表已有 5 条或更多任务
  • 预期输出:列表上方显示警告提示
  • 提示:用 v-show 结合 tasks.length >= 5 条件

练习 5(5 分钟):看懂报错

题目: 以下代码运行时报错"TypeError: Cannot read property 'value' of undefined",请修复。

<script setup>
import { ref } from 'vue'

const userName = '张三'  // 忘了 ref
</script>

<template>
<p>{{ userName.value }}</p>  <!-- 读取方式也不对 -->
</template>
  • 输入:上述代码
  • 预期输出:页面正常显示"张三"
  • 提示:需要两步修复:1. 用 ref() 包装 2. 模板里不需要 .value

作业:做一个「指令演练场」

需求描述:
做一个综合练习页面,把本文学到的所有指令都用一遍,加深理解。

功能点:
1. 数据绑定区:用 v-bind 动态切换图片,用 v-text/v-html 显示不同内容
2. 条件渲染区:用 v-if/v-show 实现选项卡切换
3. 列表渲染区:用 v-for 渲染一个排行榜数据(姓名、分数、排名)
4. 事件交互区:用 v-on(@) 绑定多个按钮,实现计数、切换等操作
5. 表单体验区:用 v-model 实现一个简单的用户信息表单(姓名、年龄、职业),实时显示填写内容

加分项:
1. 每个区域都有"清空/重置"按钮
2. 使用 CSS 让页面看起来更美观(不用框架,纯 CSS 即可)

验收标准:
- 能跑起来,没有报错
- 5 个区域都正常工作
- 代码有适量注释说明每个指令的作用

提交方式:
把代码贴到评论区,或者扔到 GitHub/Gitee 把链接发过来。


📚 总结 + 资源

本文学到的 3 个核心点

  1. 指令是模板的灵魂v- 开头的指令让 HTML 标签"活"起来,响应数据变化
  2. v-for 要配 :key — 列表渲染必加唯一 key,不然 Vue 会报警告,性能也会变差
  3. v-model 是双向绑定 — 表单输入用它最省事,一边改另一边自动同步

延伸学习资源

  1. Vue 3 官方文档 - 模板语法 — 最权威的参考
  2. Vue 3 官方文档 - 列表渲染 — 进阶内容看这里
  3. 《Vue.js 设计与实现》— 想深入原理的同学可以啃这本

互动钩子

你在实际项目里用过 v-model 吗?用它做过什么有意思的功能?
评论区聊聊你是怎么用的,踩过什么坑,老粉优先回复!👇


📢 下章预告:学会了模板语法和指令,但你有没有发现——我们的数据都是提前写死的?下一章我们来聊聊计算属性与侦听器,让数据自己会"思考",自动算出新结果!敬请期待~

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。