第2次 · 单文件组件 SFC 与模板语法
第2次 · 单文件组件 SFC 与模板语法
第1/7页 · 先知道今天要做什么
上一课你已经把一整块代码粘进 App.vue 跑起来了。今天要弄明白:这个 .vue 文件为什么能同时装结构和样式? 以及数据是怎么被“印”到页面上的。
今天的场景
班主任要一张学员档案卡:上方是姓名和学号,下面是专业、入学年份、成绩单。如果用手写 HTML,改一次姓名就要改好几处。今天换一种写法——把学员信息放在一个对象里,页面上只写“在这里显示 student.name”,以后姓名一变,卡片上跟着变,一个字符都不用重复改。
下课时拿出这三样结果
| 做到什么 | 在哪里看 | 完成的证据 |
|---|---|---|
| 认得出 SFC 的三段 | src/App.vue |
能说出 <template>、<script setup>、<style scoped> 各自负责什么。 |
| 会用插值渲染数据 | 浏览器页面 | 卡片上的姓名、学号、专业全部来自 student 对象,改对象里的值页面就变。 |
| 会区分插值和属性绑定 | 浏览器 + 代码 | 文字内容用 {{ }},给标签传值用 :属性="表达式",两者不混用。 |
你只需要带着这些基础来
会写 HTML 标签和属性(<div class="box">);知道 JS 里对象用 { }、数组用 [ ]、取值用 obj.key;上一课的工程还能正常 npm run dev。
本课时 45 分钟怎么上
| 时间 | 老师带着做 | 学生留下什么 |
|---|---|---|
| 0—6 分钟 | 复习 npm run dev,打开上一课工程,指出这次只改 App.vue 一个文件。 |
工程能启动。 |
| 6—18 分钟 | 拆解 SFC 三段:结构、逻辑、样式。演示删掉 <script setup> 后页面标签会报错。 |
笔记里写下三段各自的职责。 |
| 18—30 分钟 | 讲插值 {{ }} 与三种指令简写:v-bind → :、v-on → @、v-model。演示 {{ 1 + 1 }}、{{ student.name }}。 |
能说出“哪里用双大括号、哪里用冒号”。 |
| 30—40 分钟 | 粘贴本课 App.vue,把 student 对象换成自己的信息,观察哪些地方自动跟着变。 |
卡片上显示自己的真实档案。 |
| 40—45 分钟 | 抽查:随便改 scores 里一个数字,问学生页面上哪两处会变。 |
全员完成并提交。 |
从哪里开始
继续用上一课的 lesson-vue-01 工程,或者复制一份改名为 lesson-vue-02。本课不需要新装任何依赖,Element Plus 上一课已经装好了。
今天先掌握到这里
今天只讲“怎么把数据放到页面上”,不讲“数据怎么变”。ref 的响应式原理、computed、watch、条件渲染 v-if、列表渲染 v-for 分别在第 3、4、5 课。
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档 · 组件依据 Element Plus 2.8 官方文档
第2/7页 · 把新词说清楚
新词与本课含义
| 名字 | 用熟悉的话理解 | 本课里长什么样 |
|---|---|---|
| SFC(单文件组件) | 一个 .vue 文件 = 一个组件。把一个组件的三件事写在同一个文件里,不用在 HTML/JS/CSS 三个文件之间来回跳。 |
src/App.vue 这一个文件。 |
<template> |
结构段。写页面长什么样,标签和属性都写在这里。只能有一个顶层根标签。 | 里面是 el-card、el-descriptions 等。 |
<script setup> |
逻辑段。写数据、函数、import。这里声明的东西模板可以直接用。 | const student = ref({...}) |
<style scoped> |
样式段。写 CSS。加了 scoped 后这些样式只作用于本组件,不会污染别的组件。 |
.name { font-size: 20px } |
插值 {{ }} |
双大括号,把 JS 表达式的结果当成文字显示在页面上。 | {{ student.name }}、{{ 1 + 1 }} |
| 表达式 | 任何能算出一个值的 JS 写法:变量、运算、函数调用、三元判断。 | {{ student.scores.length }} 门 |
v-bind(简写 :) |
把一个动态的值绑到标签属性上。加了冒号,右边的引号里就是 JS 而不是普通字符串。 | <el-tag :type="tagType"> |
v-on(简写 @) |
绑定事件。等号右边写事件发生时执行的代码。 | <el-button @click="save"> |
一张表记住“哪里用哪个”
| 想做的事 | 写法 | 例子 |
|---|---|---|
| 页面上显示一段文字 | {{ }} |
<span>{{ student.name }}</span> |
| 给标签传一个会变的值 | :属性="表达式" |
<el-tag :type="statusType"> |
| 给标签传一个固定字符串 | 直接写引号 | <el-tag type="success"> |
| 监听一个事件 | @事件="函数或语句" |
<el-button @click="dialogVisible = true"> |
| 写一段只在本组件生效的 CSS | <style scoped> |
.name { color: #303133 } |
把名字分清
{{ }}里放表达式,不放语句。{{ student.name }}可以,{{ let a = 1 }}不行,{{ if (x) {} }}也不行。要写判断就用三元:{{ ok ? '是' : '否' }}。- 冒号是关键。
type="success"传的是字符串success;:type="'success'"传的才是 JS 字符串;:type="myVar"传的是变量myVar的值。三者完全不同。 <template>只能有一个顶层根标签。想放多个并列元素,外面套一个<div>或用<template>包裹。
本课只用五个组件
| 组件 | 什么时候用 | 达到的效果 |
|---|---|---|
<el-card> |
装一张信息卡 | 白色圆角卡片,自带标题栏与阴影。 |
<el-descriptions> |
成对展示“标签 — 值” | 自动排出两列或三列的对照表,可加边框。 |
<el-descriptions-item> |
描述表里的一项 | 左边标签、右边内容。 |
<el-avatar> |
显示头像或首字母 | 圆形色块,没图时显示文字。 |
<el-tag> |
标记等级、状态 | 彩色小徽章。 |
只看这四行:数据是怎么变成文字的
<script setup>
const student = ref({ name: '小林', scores: [88, 92, 79] })
</script>
<template>
<div>{{ student.name }}</div> <!-- 插值:显示“小林” -->
<div>共 {{ student.scores.length }} 门</div> <!-- 表达式:显示“共 3 门” -->
<el-tag :type="student.scores.length > 2 ? 'success' : 'info'">达标</el-tag>
<!-- ↑ 冒号让右边变成 JS 表达式,算出 'success' 这个字符串 -->
</template>
先口头预测
如果只把 student.name 改成 '小周',页面上会出现几种不同的变化?
答案:只要是引用了 student.name 的地方都会变——卡片标题、可能还有头像首字母。而 {{ student.scores.length }} 这种只读 scores 的表达式不会变。这说明:页面上的每处文字,都对应着代码里的一处引用。
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档 · 组件依据 Element Plus 2.8 官方文档
第3/7页 · 先看一个小例子
完整代码:App.vue(学员档案卡)
<script setup>
import { ref } from 'vue'
import {
ElCard, ElAvatar, ElTag, ElDescriptions, ElDescriptionsItem, ElDivider
} from 'element-plus'
import 'element-plus/dist/index.css'
// 课程名(固定文字,也放进 ref,方便以后改)
const courseName = ref('Vue 3 + Element Plus 前端实训')
// 一个学员的完整信息
const student = ref({
username: '2510101',
name: '小林',
major: '软件工程',
grade: 2025,
scores: [88, 92, 79]
})
</script>
<template>
<div class="page">
<h1 class="title">{{ courseName }}</h1>
<p class="subtitle">今天的例子:一张学员档案卡</p>
<el-card shadow="hover">
<div class="card-head">
<!-- 插值 + 方法调用:取姓名第一个字做头像 -->
<el-avatar :size="56" style="background:#409eff">{{ student.name.slice(0, 1) }}</el-avatar>
<div class="head-text">
<div class="name">{{ student.name }}</div>
<div class="muted">学号:{{ student.username }}</div>
</div>
<!-- 冒号绑定 + 三元表达式 -->
<el-tag :type="student.scores.length >= 3 ? 'success' : 'info'" size="large">
已录 {{ student.scores.length }} 门成绩
</el-tag>
</div>
<el-divider />
<el-descriptions :column="2" border>
<el-descriptions-item label="姓名">{{ student.name }}</el-descriptions-item>
<el-descriptions-item label="学号">{{ student.username }}</el-descriptions-item>
<el-descriptions-item label="专业">{{ student.major }}</el-descriptions-item>
<el-descriptions-item label="入学年份">{{ student.grade }} 级</el-descriptions-item>
<!-- 数组方法:把成绩用斜杠连起来 -->
<el-descriptions-item label="成绩单">{{ student.scores.join(' / ') }}</el-descriptions-item>
<!-- 表达式里直接算平均数 -->
<el-descriptions-item label="平均分">
{{ (student.scores.reduce((a, b) => a + b, 0) / student.scores.length).toFixed(1) }}
</el-descriptions-item>
</el-descriptions>
<p class="tip">
提示:把上面 <code>student</code> 里的 <code>name</code> 改成你的名字并保存,
卡片标题与头像会同时变化 —— 但“已录 3 门成绩”不会变,因为它读的是 <code>scores</code>。
</p>
</el-card>
</div>
</template>
<style>
html, body, #app {
margin: 0; padding: 0; max-width: none; width: auto; height: auto;
display: block; place-items: initial; text-align: left; background: #f5f7fa;
}
</style>
<style scoped>
.page { max-width: 760px; margin: 32px auto; padding: 0 16px; }
.title { margin: 0 0 4px; font-size: 24px; color: #303133; }
.subtitle { margin: 0 0 20px; color: #909399; font-size: 14px; }
.card-head { display: flex; align-items: center; gap: 14px; }
.head-text { flex: 1; }
.name { font-size: 20px; font-weight: 600; color: #303133; }
.muted { color: #909399; font-size: 13px; margin-top: 4px; }
.tip { margin: 16px 0 0; color: #909399; font-size: 13px; line-height: 1.7; }
.tip code { background: #f4f4f5; padding: 1px 5px; border-radius: 4px; color: #c7254e; }
</style>
预期看见什么
- 页面顶部是蓝色标题与灰色副标题。
- 一张卡片,卡片内左上是蓝色圆形头像写着“小”,右边是“小林”,再右边是绿色小标签“已录 3 门成绩”。
- 分隔线下面是两列带边框的对照表,六项内容:姓名、学号、专业、入学年份、成绩单、平均分。
- 把
<script setup>里name: '小林'改成你自己的名字,保存后名字和头像同时变,标签上的“3 门”不变。
先猜一猜,读完代码再回答
代码里 <el-descriptions-item label="专业"> 的 label 没有加冒号,而 <el-avatar :size="56"> 的 size 加了冒号。为什么?
因为 label 要的就是一个固定的中文标题,写死就好,所以不加冒号,引号里就是普通字符串。而 size 要的是一个数字,如果不加冒号会被当成字符串 "56" 传进去;加冒号后 56 才是真正的数字。这一条记牢:引号里是普通文本,加了冒号才是 JS。
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档 · 组件依据 Element Plus 2.8 官方文档
第4/7页 · 一步一步做出来
第1步:打开工程并定位文件
cd lesson-vue-02
npm run dev
在编辑器左侧找到 src/App.vue,双击打开。你会看到文件被分成三段,从上到下依次是:
<script setup>
// 逻辑:数据、函数、import
</script>
<template>
<!-- 结构:页面上有什么 -->
</template>
<style scoped>
/* 样式:长什么样 */
</style>
注意:三段的顺序可以任意,但本课程统一用“逻辑 → 结构 → 样式”这个顺序,方便对照。
第2步:做一次删掉试试(不用保存)
把 <script setup> 整段用 <!-- --> 注释掉,保存。
预期:页面报错或整块空白,终端/Console 会提示相关变量未定义。
看到后立刻改回来。这一步是为了让你体会:模板本身不会凭空知道 student 是什么,是 <script setup> 把名字交给了它。
第3步:按 SFC 三段整段替换
把第 3 页那份完整代码整段覆盖 src/App.vue,保存。
预期:浏览器自动刷新,出现学员档案卡。此时不需要重启 npm run dev。
第4步:认识插值的效果
找到这一行:
<div class="name">{{ student.name }}</div>
把它临时改成:
<div class="name">{{ student.name }}({{ student.grade }} 级)</div>
预期:卡片上显示“小林(2025 级)”。这就是插值——{{ }} 里的东西被算成值,然后当成文字放进去。
再把它改成 {{ 1 + 1 }},页面显示 2。确认后改回原样。
第5步:认识属性绑定
找到这一行:
<el-tag :type="student.scores.length >= 3 ? 'success' : 'info'" size="large">
把 3 改成 5,保存。预期:标签从绿色变成灰色(因为 3 门成绩不满足 ≥5)。这证明 :type 真的在跟着表达式走,而不是写死的。
改回 3。
第6步:只改数据,看页面跟着变
在 <script setup> 里把:
scores: [88, 92, 79]
改成:
scores: [88, 92, 79, 66, 95]
保存。预期:三处同时变化——标签变成“已录 5 门成绩”、成绩单多了两项、平均分重算。
这就是本节课最核心的结论:你没有改模板,只改了数据,页面自己跟上了。
本课文件速查
| 文件 / 位置 | 本次作用 |
|---|---|
src/App.vue 的 <script setup> |
定义 courseName 和 student,是页面上所有数据的来源。 |
src/App.vue 的 <template> |
写 {{ }} 插值、:属性 绑定、@事件 监听。 |
src/App.vue 的 <style scoped> |
只作用于本组件的样式。 |
src/main.js |
不改动。 |
完成修改后检查
| 操作或输入 | 预期观察 |
|---|---|
改 student.name |
卡片标题与头像首字母同时变。 |
改 student.scores 里的数字 |
成绩单、平均分、标签文字都跟着变。 |
把 :type 的冒号删掉 |
标签颜色立刻不对(变成默认色),说明绑定断开了。 |
在 <style scoped> 里改 .name 字号 |
只有本组件的那一处变,其他页面不受影响。 |
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档 · 组件依据 Element Plus 2.8 官方文档
第5/7页 · 换成自己的代码
你的任务
- 换成你自己的档案:把
student对象里的username、name、major、grade全换成你自己的真实信息,scores换成你真实的五门课成绩(1—100 的整数)。 - 增加两项描述:在
el-descriptions里再加两项。班号:例如软件 2501。最高分:用表达式算出scores里的最大值。 提示:Math.max(...student.scores)可以直接算出最大值。
- 让标签显示及格情况:把第一个
el-tag改成判断——所有成绩都及格(≥60)时显示绿色success并写“全部及格”;否则显示红色danger并写“有不及格”。 提示:用student.scores.every(s => s >= 60)得到布尔值,再套三元表达式。 - 用插值算一个数:在副标题那一行,把文字改成带数据的,例如“本卡共展示 {{ 6 }} 项信息”,把
6换成靠表达式算出来的项数(可以手数后写成数字,也可以直接写数字——但要求旁边的文字必须包含至少一个{{ }})。
按这张操作单完成
| 步骤 | 改哪里 | 运行后应该看到什么 |
|---|---|---|
| 1 替换数据 | student 对象四个字段 + scores |
卡片显示自己的真实姓名、学号、专业、年份、五门成绩。 |
| 2 加班号 | el-descriptions 内新增一项 |
出现“班号:软件 2501”。 |
| 3 加最高分 | 新增一项,内容写 {{ Math.max(...student.scores) }} |
显示自己五门成绩里的最高分。 |
| 4 及格判断 | 第一个 el-tag 的 :type 与文字 |
成绩全及格时绿色写“全部及格”;改一个成绩为 58 后变红写“有不及格”。 |
| 5 副标题插值 | .subtitle 那一行 |
文字里能看到算出来的数字。 |
最低完成标准:先把这三项做实
| 检查项 | 通过条件 |
|---|---|
| 三段分明 | App.vue 里 <script setup>、<template>、<style scoped> 都在,且结构段只有一个根标签。 |
| 数据驱动 | 只改 <script setup> 里的数据,模板一行没动,页面显示也跟着变。 |
| 区分插值与绑定 | 文字用 {{ }},属性值用 :属性,且能解释为什么 :size="56" 要加冒号。 |
在本地写练习记录
在工程根目录的 练习记录.md 里追加本次内容:
- 写下 SFC 三段各自的职责(一句话一段)。
- 抄下你改过的
student对象全文,以及“及格判断”那一行的最终写法。 - 记录一个你亲自验证过的现象:例如“把
scores从 3 个改成 5 个后,页面上有哪三处变了”。
最后交什么
提交 src/App.vue 与 练习记录.md。本课没有新增依赖,不需要提交 package.json。
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档 · 组件依据 Element Plus 2.8 官方文档
第6/7页 · 出错时从哪里查
把现象和检查动作对上
| 看到什么现象 / 报错 | 先检查哪里 | 修好后怎样确认 |
|---|---|---|
页面整块空白,Console 报 Property "student" was accessed during render but is not defined |
student 是不是写在 <script setup> 里、名字是否拼错、有没有被注释掉。 |
变量定义补齐后页面正常。 |
页面直接显示 {{ student.name }} 这串文字 |
{{ }} 被包在了 HTML 注释里,或者外层被 <pre>、引号包住当成纯文本了。 |
去掉外层包裹,值被正确算出来。 |
报 Component is missing template or render function |
<template> 里没有根标签,或者多写了一层不闭合的标签。 |
保证结构段最外层只有一个根元素。 |
<el-tag> 颜色不对,一直是灰色 |
:type 右边算出来的不是 success/warning/danger/info 之一。检查三元表达式的两个分支拼写。 |
换成合法值后颜色正确。 |
size="56" 不生效或控制台警告 |
数字型属性忘了加冒号,被当成字符串。改成 :size="56"。 |
头像变大,警告消失。 |
| 样式改了却没反应 | 样式写在了另一个组件里,或选择器被 scoped 限制没命中。检查 .class 名与模板是否一致。 |
目标元素样式生效。 |
<style scoped> 里写 body、html 不生效 |
scoped 只作用于本组件的元素,body 不属于本组件。全局样式要单独写一个不带 scoped 的 <style>。 |
全局样式生效。 |
排错口诀
名字未定义看
script,双大括号显示成文本说明被包住; 颜色不对查:type的值,尺寸不生效先补冒号; 样式不动对 class,全局样式去掉scoped。
不要用这些办法“修复”
- 页面空白的时不要先怀疑 Element Plus 坏了,先看 Console 第一条红字,九成是变量名写错或标签没闭合。
- 不要用
innerHTML或document.querySelector手动往页面塞内容,那是放弃 Vue 的写法。 - 不要为了省事把所有 CSS 都去掉
scoped,组件一多必然互相污染。
需要老师帮忙时交这些
src/App.vue 全文;Console 第一条红色错误原文;你期望页面显示什么、实际显示什么。截图之外保留文字。
老师巡视时只问三句话
- 页面上“平均分”那个数字,是从代码的哪一行算出来的?(能指出
reduce那一行) :type前面的冒号去掉会怎样?(能说出会变成固定字符串,不再跟数据走)- 如果我把
scores数组清空,页面上哪一处会先报错?(能指出平均分那一处会得到NaN)
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档 · 组件依据 Element Plus 2.8 官方文档
第7/7页 · 用自己的话讲一遍
回到第 3 页:你刚才猜对了吗?
为什么只改 student.name,头像和标题都变了,但“已录 3 门成绩”不变?
参考解释:因为标题和头像首字母的表达式里都引用了 student.name,而标签读的是 student.scores.length。页面上的每一处显示,都对应代码里的一处引用;只改被引用的那个数据,被它牵着的显示才会变。
回答本课的两个问题
| 问题 | 参考解释 |
|---|---|
为什么 <template> 只能有一个根标签? |
Vue 在编译时需要一个明确的“本组件渲染的起始元素”,多个并列根会导致无法确定组件的边界和样式作用范围。 |
scoped 到底做了什么? |
编译时给本组件的元素加上唯一属性(类似 data-v-xxxx),样式选择器被同时改写成带该属性的形式,所以只命中本组件的元素。 |
五个必须能答上来的问题
| 考考自己 | 可以这样通俗回答 |
|---|---|
{{ }} 和 :属性 有什么区别? |
{{ }} 是把值当文字放进标签中间;:属性 是把值传给标签的属性。 |
为什么 label="专业" 不加冒号? |
因为要的就是固定中文,引号里是普通字符串,不需要 JS 参与。 |
{{ }} 里能写 if 语句吗? |
不能,只能写表达式。要判断就用三元 a ? b : c。 |
<style scoped> 有什么用? |
让样式只作用在当前组件,不会影响别处,避免组件多了以后样式互相打架。 |
为什么模板里写 student,而 JS 里写 student.value? |
ref 返回的是包装对象,模板会自动“拆包”帮你取 .value;JS 里要自己取。 |
一分钟现场演示指南
- 展示三段:编辑器里指出
<script setup>/<template>/<style scoped>,一句话说明职责。 - 演示插值:把
{{ student.name }}改成{{ student.name }}({{ student.grade }} 级),保存,指出页面上多出来的部分。 - 演示绑定:把
:size="56"改成:size="80",头像变大;再把冒号删掉,头像变回小号,说明冒号的作用。 - 只改数据:把
scores加两项,指出页面三处自动变化,收尾一句:“一个模板,不用改。”
交作业前最后检查
src/App.vue与练习记录.md已保存。- 确认结构段只有一个根标签。
- 确认页面里没有遗留的临时调试文字(例如
{{ 1 + 1 }})。
课后再做一个小对照
把 student 对象复制一份,改名 student2,用另一张 el-card 把第二份数据也渲染出来。观察:同一个模板写法,换一份数据就能渲染出第二张卡片。这正是第 5 课“列表渲染”要解决的问题——当你有 50 个学员时,不会写 50 个模板。
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档 · 组件依据 Element Plus 2.8 官方文档