模板语法与指令:Vue 的"视图层注解"

先立一个心智模型

后端开发者看 Vue 模板时,最容易把它和 Thymeleaf / JSP / FreeMarker 画等号。这个类比一半对一半错

一句话:

Vue 模板是运行时响应式的视图层,Thymeleaf 是一次性的字符串拼接。

理解这点,下面所有指令才不会看成"Thymeleaf 的抄袭"。


1. 插值表达式 {{ }}:基础中的基础

<template>
  <p>你好,{{ username }}</p>
  <p>下单总价:{{ price * count }} 元</p>
  <p>大写:{{ username.toUpperCase() }}</p>
</template>

Java 对照(Thymeleaf)

<p th:text="'你好,' + ${username}"></p>
<p th:text="${price * count} + ' 元'"></p>

关键点:

安全提醒:想渲染原始 HTML 得用 v-html,这等于 Thymeleaf 的 th:utextXSS 风险自担


2. v-bind:把 JS 值绑到 HTML 属性

<template>
  <!-- 完整写法 -->
  <a v-bind:href="url">链接</a>

  <!-- 简写(常用)-->
  <a :href="url">链接</a>

  <!-- 绑定多个属性:传一个对象 -->
  <img :src="user.avatar" :alt="user.name" :title="user.bio" />

  <!-- 布尔属性:值为 true 才渲染 -->
  <button :disabled="isLoading">提交</button>

  <!-- class 绑定三种写法 -->
  <div :class="'card'"></div>                          <!-- 字符串 -->
  <div :class="{ active: isActive, error: hasError }"></div>  <!-- 对象:true 才加 -->
  <div :class="['card', isActive && 'active']"></div>  <!-- 数组 -->

  <!-- style 类似 -->
  <p :style="{ color: themeColor, fontSize: size + 'px' }">文本</p>
</template>

Thymeleaf 对照

<a th:href="@{${url}}">链接</a>
<button th:disabled="${isLoading}">提交</button>
<div th:classappend="${isActive} ? 'active' : ''"></div>

看起来都差不多,但 Vue 的 class / style 对象语法非常常用——条件拼 class 比字符串拼接清爽得多。


3. v-on:事件监听

<template>
  <!-- 完整写法 -->
  <button v-on:click="handleClick">点我</button>

  <!-- 简写(更常用)-->
  <button @click="handleClick">点我</button>

  <!-- 内联表达式 -->
  <button @click="count++">{{ count }}</button>

  <!-- 传参 -->
  <button @click="remove(item.id)">删除</button>

  <!-- 原生事件对象 $event -->
  <input @input="onInput($event.target.value)" />

  <!-- 事件修饰符(Vue 特色)-->
  <form @submit.prevent="save">...</form>     <!-- 阻止默认提交 -->
  <div @click.stop="handle">...</div>         <!-- stopPropagation -->
  <button @click.once="init">只触发一次</button>
  <input @keyup.enter="submit" />             <!-- 按键修饰符 -->
</template>

Java 对照

推荐写法:所有项目统一用简写(:@),别混用。


4. v-if / v-else-if / v-else:条件渲染

<template>
  <div v-if="user.role === 'admin'">管理员视图</div>
  <div v-else-if="user.role === 'editor'">编辑视图</div>
  <div v-else>游客视图</div>

  <!-- 多个元素整体控制:用 <template> 包一下,template 不会渲染成真标签 -->
  <template v-if="isLoggedIn">
    <h1>欢迎回来</h1>
    <p>{{ user.name }}</p>
  </template>
</template>

Thymeleaf 对照

<div th:if="${user.role == 'admin'}">管理员视图</div>
<div th:unless="${user.role == 'admin'}">非管理员视图</div>

Vue 独有的 v-show

<div v-show="isVisible">切换显示</div>

选择原则:频繁切换用 v-show,极少切换用 v-ifv-if 的初始化成本高,v-show 的首渲染成本高。


5. v-for:列表渲染

<template>
  <!-- 数组:(item, index) -->
  <li v-for="(note, index) in notes" :key="note.slug">
    {{ index + 1 }}. {{ note.title }}
  </li>

  <!-- 对象:(value, key, index) -->
  <li v-for="(value, key) in userInfo" :key="key">
    {{ key }}: {{ value }}
  </li>

  <!-- 范围:1 ~ 10 -->
  <span v-for="n in 10" :key="n">{{ n }}</span>
</template>

Thymeleaf 对照

<li th:each="note, iter : ${notes}" th:text="${iter.index + 1} + '. ' + ${note.title}"></li>

关键坑::key 不能省

Vue 依赖 :key 做"虚拟 DOM diff"——列表更新时靠 key 识别哪些是同一个元素,从而复用 DOM 而不是重建。

规则

反例(会引发奇怪 bug):

<!-- ❌ 列表顺序变化时,输入框里的值会"错位" -->
<div v-for="(item, i) in items" :key="i">
  <input v-model="item.text" />
</div>

不要在同一个标签上同时写 v-forv-if

Vue 3 里 v-if 优先级更高,会导致 v-if 里访问不到 v-for 的变量。正确做法是把 v-if 挪到外层 <template> 或内层子元素:

<!-- ✅ 先过滤,再渲染 -->
<li v-for="note in visibleNotes" :key="note.slug">...</li>

<script setup>
const visibleNotes = computed(() => notes.value.filter((n) => n.published))
</script>

这也是为什么 src/pages/HomePage.vue 里用了 filteredNotes 这个 computed——先筛后渲。


6. v-model:双向绑定

<template>
  <!-- 文本输入 -->
  <input v-model="searchQuery" placeholder="搜索" />

  <!-- 复选框 -->
  <input type="checkbox" v-model="agreed" />

  <!-- 下拉选择 -->
  <select v-model="selectedCategory">
    <option v-for="c in categories" :key="c" :value="c">{{ c }}</option>
  </select>

  <!-- 修饰符 -->
  <input v-model.trim="name" />       <!-- 自动去首尾空格 -->
  <input v-model.number="age" />      <!-- 自动转 number -->
  <input v-model.lazy="bio" />        <!-- change 事件才同步,而不是 input -->
</template>

v-model 本质是语法糖,等价于:

<input :value="searchQuery" @input="searchQuery = $event.target.value" />

后端没有直接类比——Thymeleaf 是单向模板渲染,没有"输入变化自动回写变量"这回事。这是 Vue / 前端 MVVM 框架的核心卖点之一。

首页的搜索框就是一行 v-model="searchQuery" 完成的,Java 视角等价于:

// 伪代码:输入框的值和字段自动保持同步
@TwoWayBind
private String searchQuery;

7. v-html / v-text:较少用

<!-- v-text 等价于 {{ }},不转义也安全(文本节点)-->
<span v-text="message"></span>

<!-- v-html:渲染原始 HTML,XSS 风险自担 -->
<div v-html="note.html"></div>

v-html 只在信任数据源时用(比如自己写的 Markdown → HTML)。笔记详情页的 MarkdownRenderer 里就用了 v-html,因为内容是我们自己写的 Markdown 经 markdown-it + Shiki 渲染,没有用户输入。


速查表

指令 用途 Thymeleaf 对照
{{ x }} 文本插值 th:text
v-bind:foo / :foo 绑定属性 th:attr / th:foo
v-on:click / @click 事件监听 (无直接对应)
v-if / v-else 条件渲染(真增删) th:if / th:unless
v-show 条件显示(切 display) (无直接对应)
v-for 列表渲染 th:each
v-model 表单双向绑定 (无,Thymeleaf 只读)
v-html 渲染原始 HTML th:utext

小练习:在 in4vue 里观察

  1. 打开 src/pages/HomePage.vue,找出所有用到的指令,对照速查表能一一说出作用
  2. filteredNotes 外层尝试把 v-for:keynote.slug 改成 index,搜索时观察卡片有没有奇怪闪烁(小列表可能看不出来,但原理要记住)
  3. 把搜索框的 v-model="searchQuery" 改成 v-model.lazy="searchQuery",体会"边打字边筛" vs "失焦才筛"的差异

延伸阅读