Skip to content

01|插值与表达式 ​

让数据上台,别让逻辑抢戏。

第一章概览 / Vue 3.5+ / 阅读约 15~25 分钟。目标:把状态安全、清楚地投射到界面上,并知道模板表达式在哪儿该刹车。

本页完整示例以 Vue 3.5.x + Vite 的 TypeScript 模板为运行环境;把代码保存为 src/App.vue,执行项目已有的 npm run dev 后在浏览器观察。若你使用 CDN 或 DOM 内模板,大小写与标签解析还会受浏览器 HTML 解析规则影响,本页不覆盖那条分支。

新手主线

先跑最小示例,再看选择边界,最后做练习。每读完一个小节,改一行代码并预测结果。

老手快车

直接跳到“60 秒选择”“安全边界”“深水区”和“排错手册”。你要找的是所有权、时机和信任边界,不是把花括号念得更响。

本页要拿下的能力 ​

层级你要会什么典型场景
必会{{ }}、JavaScript 表达式、文本展示标题、计数、空状态
熟练v-text、属性绑定、空值和表达式边界完整文本、提示属性、格式化展示
深入v-html 安全边界、渲染上下文、computed 的取舍可信富文本、性能排查、模板瘦身

本页暂时不把事件、表单和列表讲成完整章节;它们会在后面接班。本篇只负责回答:一份值怎样抵达屏幕,以及它应该以什么身份抵达。

读完后,试着不用查文档回答四个问题:

  1. 为什么 {{ message }} 会显示 HTML 字符,而不是把它变成标签?
  2. 为什么 :disabled="busy" 能控制按钮,disabled="{{ busy }}" 却不行?
  3. 为什么模板里可以写三元表达式,却不适合发请求或声明变量?
  4. 什么情况下 v-html 才有资格进入生产代码?

如果这四题还能讲出“因为”,而不只是背出写法,说明你已经从语法站台走到理解站台了。

最小示例:让数据第一次上台 ​

先看最小的 .vue 文件。这里故意不引入按钮和请求,先学会“值如何被显示”。

vue
<!-- src/App.vue -->
<script setup lang="ts">
const learner = '小白'
const score = 42
const message = '<strong>这仍然是文字</strong>'
</script>

<template>
  <h1>你好,{{ learner }}!</h1>
  <p>经验值:{{ score }}</p>
  <p>{{ message }}</p>
</template>

运行后你会看到三行文本,第三行会显示 <strong>这仍然是文字</strong>,而不是粗体。把 learner 改成自己的名字,再预测一次页面结果。

这里的双花括号叫文本插值:Vue 读取表达式的结果,把它放进文本节点,并自动转义 HTML。它像一个可靠的播报员,只负责报幕,不负责把后台的锅端上台。

逐行拆解:三块区域各自做什么 ​

代码它在做什么你现在可以先记住什么
<script setup>声明模板要读取的状态和函数顶层绑定会自动暴露给模板
const learner = '小白'创建一个普通的 JavaScript 值值不变时,普通常量已经够用
<h1>你好,&#123;&#123; learner &#125;&#125;!</h1>把表达式结果放入文本节点双花括号默认按文本处理
<p>&#123;&#123; message &#125;&#125;</p>将字符串原样作为文字内容展示字符串长得像 HTML,也不会自动变 HTML

这里故意没有 ref()。下一篇会先补响应式容器的基本用法,第三章再展开原理;本篇先把“显示”这件事单独看清楚。想做个预习实验时,在脚本顶部加上 import { ref } from 'vue',再把 score 改成 ref(42),然后观察:在模板中读取顶层 ref 时,不需要手写 .value,Vue 会帮你拆包。

动手改一行:预测再刷新 ​

把 message 依次改成下面三种值,并先猜页面会显示什么:

ts
const message = '<em>斜体?</em>'
const message = ''
const message = null

第一种会显示标签字符,第二种显示空白,第三种通常也显示为空。这个小实验比背“自动转义”四个字更牢靠:浏览器没有被 Vue 说服去执行任何 HTML。

60 秒选择:文本、属性还是 HTML ​

先辨别目标,再选择绑定通道。绑定不是越短越好,下一位读代码的人不用考古,才是最高礼貌。

你手上的内容优先写法Vue / 浏览器做什么不要误用成
普通文本、用户名、接口字段{{ value }}设置文本节点并转义 HTMLv-html
元素的完整文本内容v-text="message"设置 textContent,会覆盖原有子节点和子元素混用
href、disabled、aria-*、组件 prop:href="url"、:disabled="busy"更新属性或 prop把 {{ }} 塞进属性字符串
已明确净化的富文本v-html="sanitizedHtml"设置 innerHTML,不会再次编译 Vue 模板用户评论或 URL 原文
难读的展示计算computed 或命名函数给逻辑一个可测试、可复用的名字三层三元或渲染时请求数据

例如,<button :disabled="busy">保存</button> 在 busy 为真时会真的不能点;disabled="{{ busy }}" 则不是 Vue 的属性绑定方式。

再看一个容易混淆的边界:

vue
<!-- 文本内容:元素里面放什么字 -->
<p>{{ message }}</p>

<!-- 属性 / DOM property:元素本身有什么能力 -->
<button :disabled="busy">保存</button>
<a :href="profileUrl">查看资料</a>

{{ }} 的工作对象是文本节点;:(也就是 v-bind 的缩写)把表达式结果交给属性或组件 prop。可以把它们想成两个窗口:一个负责“说台词”,一个负责“递道具”。把台词塞进道具窗口,Vue 只好一脸困惑。

如果属性很多,可以使用对象绑定,但要先确认这确实属于属性层:

vue
<button v-bind="buttonAttrs">保存</button>
ts
const buttonAttrs = {
  type: 'button',
  'aria-label': '保存公告',
}

对象绑定很方便,也更容易把一堆属性整体传给组件;但它不会替你判断数据是否可信,href、style 等来自用户或接口的值仍然需要单独审查。

表达式边界:模板能算什么 ​

花括号和指令值接受单个 JavaScript 表达式,不接受语句。把“能算”与“该不该在模板算”分开,是本篇最重要的判断。

可以留在模板建议移到 <script setup>为什么
{{ count + 1 }}items.filter(...).sort(...).map(...)后者难读,渲染时可能反复执行,排序还可能误改原数组
{{ ok ? '已完成' : '待办' }}@click="if (...) { ... }"行为分支应该由命名函数负责
{{ user?.name ?? '访客' }}{{ await fetchProfile() }}模板不该启动异步副作用
{{ formatDate(createdAt) }}{{ new Intl.DateTimeFormat(...).format(...) }}格式化函数更容易复用;昂贵派生值再交给 computed

一个好判断法:如果读出表达式时需要换气两次,或者很难给它起名字,它大概率该离开模板。visibleTasks、formattedPublishedAt 比一串筛选、排序和映射更会说人话。

表达式和语句:长得像亲戚,工作完全不同 ​

模板需要的是“算出一个值”,而不是“执行一段流程”。下面左边可以计算,右边是语句,应该移到脚本中:

可以作为表达式不能直接塞进模板推荐改法
{{ count + 1 }}{{ const next = count + 1 }}在脚本中声明 next
{{ ok ? '已完成' : '待办' }}{{ if (ok) { return '已完成' } }}使用三元表达式或命名函数
:id="'task-' + id"@click="for (...) { ... }"在事件函数中处理循环
{{ user?.name ?? '访客' }}{{ await fetchProfile() }}在生命周期或事件中请求数据

模板表达式运行在 Vue 提供的受限上下文中,不能把任意 JavaScript 模块作用域、浏览器全局对象和控制流语句都搬进来。限制不是 Vue 故意为难你,而是为了让模板更容易编译、分析和重复执行。

把复杂展示逻辑交给 computed ​

这段是为后续动态页面准备的预览,本页基础题可以使用普通函数。ref(initial)保存可变值,脚本读写使用 .value;computed(() => expression)返回派生ref,读取时按依赖求值。模板对顶层ref省略 .value。当值由状态推导时,可用computed表达关系:

ts
import { computed, ref } from 'vue'

const tasks = ref([
  { id: 1, title: '读插值', done: true },
  { id: 2, title: '做练习', done: false },
])

const openTasks = computed(() => tasks.value.filter(task => !task.done))

模板只需要写 {{ openTasks.length }}。这样做有三个好处:逻辑有名字、结果可测试、依赖不变时可以复用缓存。注意 computed 的 getter 应该是纯函数,不要在里面发请求、改数组或偷偷修改别的状态。

安全与生产案例:v-html 先验票 ​

v-html 设置的是 innerHTML。Vue 不会替你清理脚本、事件属性或恶意 URL,内容也不会再被当成 Vue 模板编译。

vue
<!-- 不要这样:comment 可能来自用户输入。 -->
<div v-html="comment"></div>

<!-- 更稳:默认按文本展示;换行交给 CSS。 -->
<p class="comment">{{ comment }}</p>

<style scoped>
.comment { white-space: pre-wrap; }
</style>
内容来源可以直接 v-html 吗?更好的处理方式
受控 CMS,服务端已经白名单净化可以命名为 sanitizedHtml,写清净化责任在哪一层
用户评论、编辑器原始结果不可以先在服务端或可信库完成白名单净化
接口错误信息、搜索参数、昵称不可以使用文本插值
固定 SVG 片段通常不必优先组件化或静态模板

安全红线

“看起来像 HTML”不等于“可信 HTML”。没有明确的净化边界,就不要使用 v-html。

可以用一个“恶作剧评论”测试自己的判断:

ts
const comment = '<img src=x onerror="alert(\'糟糕\')">'

用插值渲染时,它只是文字;用 v-html 渲染时,浏览器可能把它当成真正的元素和事件。真实项目里还要警惕 href、src、style 等输入,因为风险不只藏在 <script> 标签里。

谁负责净化:前端不是孤岛 ​

安全边界最好写在数据命名和接口契约里,而不是藏在某个模板角落:

ts
// 这个字段的名字提醒读者:它已经经过白名单净化。
const sanitizedHtml = response.content

前端仍然应该默认不信任外部输入;“服务端已经净化”也应当是可验证的契约,而不是一句口头禅。若只是想展示换行、强调或链接,优先使用普通文本、CSS 或受控组件。

空值与未定义变量 ​

  • null 和 undefined 作为插值结果通常显示为空字符串;需要兜底时,使用 user?.name ?? '访客'。
  • 0、false 和空字符串是有效值,不要用 || 把它们误判成“没有数据”。
  • 模板找不到变量时,先检查 script setup 顶层声明、拼写和 v-for / 插槽作用域;开发环境通常会给出警告。

可以把空值处理写成一个小表格:

值插值通常显示常见意图
null / undefined空字符串数据还没到,准备显示占位符
''空字符串用户真的提交了空文本
00数量就是零,不是“缺失”
falsefalse开关关闭,不是“没有值”

因此,user.name || '访客' 和 user.name ?? '访客' 不是一回事:前者会把空字符串、0 和 false 也当成缺失,后者只兜底 null 与 undefined。选择运算符时,先问业务上“空”到底指什么。

深水区:模板不是字符串拼接器 ​

.vue 的 <template> 会被编译成渲染函数。<script setup> 的顶层绑定会自动暴露给模板,但模板不是任意 JavaScript 模块作用域的后门。

这解释了三个生产结论:

  1. 不要在渲染表达式里修改状态。渲染可能发生多次,副作用会像复印机卡纸一样重复。
  2. 展示所需但昂贵的派生值交给 computed;它按依赖缓存。一次无副作用的格式化函数调用通常没有问题。
  3. v-html 内容不会再次编译,也不会自动得到 SFC 的 scoped 样式选择器;富文本需要样式时,应设计受控内容模型。

从源码到屏幕:为什么模板值得被限制 ​

在构建型项目里,.vue 文件大致经历这条路径:

text
<template> → 模板编译器 → render 函数 → VNode → 浏览器 DOM

编译器会分析静态节点、绑定和更新提示,运行时再根据响应式状态变化更新需要变化的部分。你不必现在就读编译器源码,但这个模型能解释很多现象:

  • 模板表达式可能在一次更新中执行多次,所以不要把修改状态、发请求、写日志当成“展示计算”。
  • computed 把派生关系命名出来,也让依赖追踪更明确;它不是“把所有代码搬到脚本”的垃圾桶。
  • v-html 的内容是浏览器直接解析的 HTML,不会重新进入 Vue 模板编译流程。

如果你发现模板里出现了请求、排序、状态修改和三层条件判断,通常不是 Vue 变复杂了,而是职责该重新分房间了。

v-pre 与 v-cloak:两位救场同学 ​

v-pre 跳过一小段 Vue 编译,适合原样展示 {{ title }}。v-cloak 在 Vue 挂载前标记元素,主要服务于 CDN 或渐进式接入;VitePress 这类构建型项目通常不需要它。

html
<code v-pre>{{ notAnExpression }}</code>

<div v-cloak>{{ message }}</div>
<style>[v-cloak] { display: none; }</style>

不要把整块组件 v-pre,那会让这一区域的绑定一起放假。

v-cloak 也不是“让页面更快”的魔法。它只能在 Vue 挂载前隐藏带有 [v-cloak] 的元素;如果项目通过 Vite 构建,通常不会长时间暴露未编译模板。不要为了显得专业而给每个组件都贴一张隐身符。

排错手册:先查数据,再查绑定 ​

现场症状检查顺序修复方向
页面显示 <strong>重点</strong>先确认内容是否只是文本默认保持插值;确需 HTML 时使用已净化字段
模板里想写 const total = …确认是否把语句塞进表达式在脚本中定义变量、函数或 computed
一次渲染触发多次请求搜索模板里是否调用有副作用的函数请求放进事件、生命周期或 composable
数据明明有,模板却找不到检查暴露、拼写和作用域使用 script setup 顶层绑定,核对 v-for / 插槽变量
v-text 后子元素消失确认是否需要混合文本与子节点改用插值和正常模板结构

排查顺序可以固定成:数据 → 展示通道 → 表达式 → 生命周期与 DOM。先从源头走到屏幕,别一上来就和空 div 深情对望。

一个五分钟排错演练 ​

假设页面没有显示用户名:

vue
<script setup lang="ts">
const profile = { name: '小白' }
</script>

<template>
  <p>{{ user.name }}</p>
</template>

按固定顺序排查:

  1. 数据: 脚本里实际叫 profile,不是 user。
  2. 作用域: profile 是顶层绑定,可以被模板读取。
  3. 表达式: 改成 {{ profile.name }},不需要把对象复制一份。
  4. 通道: 这里要显示文字,插值就是合适的通道。

如果修正后仍然不更新,再去检查它是不是响应式状态,以及是否在异步数据到达前就需要占位符。排错的顺序越稳定,越不容易靠“刷新一下看看”碰运气。

练习:同一份公告的三种输入 ​

在 学习环境 建好的项目里,把公告保存在 src/App.vue。本题只使用本篇已经讲过的常量、表达式、文本插值、属性绑定、CSS 和 v-pre。

输入为 title: string | null、body: string、priority: number 和固定的 ISO 时间字符串。先用 title='维护通知'、body='<em>请保存</em>\n稍后恢复'、priority=7;再将标题改成 null、优先级改成 8;最后把标题改成空字符串、优先级改成 0。每次修改源文件并保存,先预测再观察页面。

  • 标题仅在 null 时显示“未命名公告”,空字符串必须保留为空,不能用 || 混淆二者。
  • 正文里的标签必须显示为文字,换行由 CSS 保留。
  • 始终显示一行级别文本:小于8是“普通”,大于等于8是“高优先级”;使用三元表达式或命名纯函数。
  • <time> 的 datetime 绑定原始ISO字符串;可见时间可以用命名格式化函数生成。
  • 用 v-pre 原样展示 {{ title }},并说明它为何不显示当前标题。

查看第01题答案。把输入从 null 改成 '' 是本题的独立变体:关键是解释空值合同,不能只比较页面是否有字。

点击提交、复选框与异步错误处理分别在事件、表单中学习。它们不属于本题的完成条件。

复习卡:

  • 文本用插值,完整文本可用 v-text。
  • 属性用 :prop,不是 {{ }}。
  • 模板放表达式,不放语句和副作用。
  • 用户内容默认按文本展示。
  • v-html 只接收可信且已净化的 HTML。
  • 复杂展示逻辑移到命名函数或 computed。

下一篇:02|属性绑定。下一站演员是 :href、:disabled 和动态 class,它们会把“文字”升级成真正的属性行为。

用心记录每一步成长