孟凡的博客blog.mengfanyaoapp.com
课程资料库›Vue 3 + Element Plus 前端开发›第2次 · 单文件组件 SFC 与模板语法
模块总览

第2次 · 单文件组件 SFC 与模板语法

2026-09-08 · Vue 3 + Element Plus 前端开发 · 课程笔记
课堂练习资料 02-单文件组件SFC与模板语法.zip
下载 ZIP 压缩包

第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>

预期看见什么

  1. 页面顶部是蓝色标题与灰色副标题。
  2. 一张卡片,卡片内左上是蓝色圆形头像写着“小”,右边是“小林”,再右边是绿色小标签“已录 3 门成绩”。
  3. 分隔线下面是两列带边框的对照表,六项内容:姓名、学号、专业、入学年份、成绩单、平均分。
  4. 把 <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页 · 换成自己的代码

你的任务

  1. 换成你自己的档案:把 student 对象里的 username、name、major、grade 全换成你自己的真实信息,scores 换成你真实的五门课成绩(1—100 的整数)。
  2. 增加两项描述:在 el-descriptions 里再加两项。
    • 班号:例如 软件 2501。
    • 最高分:用表达式算出 scores 里的最大值。 提示:Math.max(...student.scores) 可以直接算出最大值。
  3. 让标签显示及格情况:把第一个 el-tag 改成判断——所有成绩都及格(≥60)时显示绿色 success 并写“全部及格”;否则显示红色 danger 并写“有不及格”。 提示:用 student.scores.every(s => s >= 60) 得到布尔值,再套三元表达式。
  4. 用插值算一个数:在副标题那一行,把文字改成带数据的,例如“本卡共展示 {{ 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 里追加本次内容:

  1. 写下 SFC 三段各自的职责(一句话一段)。
  2. 抄下你改过的 student 对象全文,以及“及格判断”那一行的最终写法。
  3. 记录一个你亲自验证过的现象:例如“把 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。

不要用这些办法“修复”

  1. 页面空白的时不要先怀疑 Element Plus 坏了,先看 Console 第一条红字,九成是变量名写错或标签没闭合。
  2. 不要用 innerHTML 或 document.querySelector 手动往页面塞内容,那是放弃 Vue 的写法。
  3. 不要为了省事把所有 CSS 都去掉 scoped,组件一多必然互相污染。

需要老师帮忙时交这些

src/App.vue 全文;Console 第一条红色错误原文;你期望页面显示什么、实际显示什么。截图之外保留文字。

老师巡视时只问三句话

  1. 页面上“平均分”那个数字,是从代码的哪一行算出来的?(能指出 reduce 那一行)
  2. :type 前面的冒号去掉会怎样?(能说出会变成固定字符串,不再跟数据走)
  3. 如果我把 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 里要自己取。

一分钟现场演示指南

  1. 展示三段:编辑器里指出 <script setup> / <template> / <style scoped>,一句话说明职责。
  2. 演示插值:把 {{ student.name }} 改成 {{ student.name }}({{ student.grade }} 级),保存,指出页面上多出来的部分。
  3. 演示绑定:把 :size="56" 改成 :size="80",头像变大;再把冒号删掉,头像变回小号,说明冒号的作用。
  4. 只改数据:把 scores 加两项,指出页面三处自动变化,收尾一句:“一个模板,不用改。”

交作业前最后检查

  • src/App.vue 与 练习记录.md 已保存。
  • 确认结构段只有一个根标签。
  • 确认页面里没有遗留的临时调试文字(例如 {{ 1 + 1 }})。

课后再做一个小对照

把 student 对象复制一份,改名 student2,用另一张 el-card 把第二份数据也渲染出来。观察:同一个模板写法,换一份数据就能渲染出第二张卡片。这正是第 5 课“列表渲染”要解决的问题——当你有 50 个学员时,不会写 50 个模板。

来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档 · 组件依据 Element Plus 2.8 官方文档