Emmet: 实战 1:用 Emmet 一气呵成写登录页 HTML(综合 1-16 课)
1-16 课你学会了 Emmet 的 8 大语法、3 个核心 Actions、5 个编辑加速 action——但都是"碎片式"小例子。本课是第一个综合实战:用一行 Emmet 缩写写完一个 70 行的完整登录页 HTML。Alice 用 10 秒写完整个表单,比手写快 10 倍。
1. 你将学到
- 需求拆解:把"登录页"拆成 5 个区块(标题 / 表单 / 按钮 / 链接 / 社交登录),每个区块映射为一段 Emmet 缩写
- 嵌套组合:用
>子级 ++兄弟节点 +()分组 +*乘法把区块串成完整骨架 - class/id 链:用
.class1.class2多 class 链表达按钮样式(.btn.btn-primary) - 自定义属性:用
[attr=val]给<input>加type/name/placeholder/required/for,给<a>加href - 文本填充:用
{text}写文案"欢迎登录""用户名""密码""登录""或""忘记密码?""还没有账号?""立即注册"
2. 一个前端开发者的真实故事
(1) 痛点:3 个 input + 2 个按钮 + 3 个链接 + 标题,手写要 10 分钟
Alice 是 SaaS 平台前端开发者。2026 年 7 月产品经理临时通知:本周要给 admin 后台加一个登录页,下周一上线。设计稿在 Figma,已经给到 5 个区块:
| 区块 | 元素清单 |
|---|---|
| 1. 品牌头部 | <span> 显示产品名 "CLOUDFLOW" |
| 2. 标题区 | <h1> 欢迎登录 + <p> 副标题 |
| 3. 表单 | 用户名 / 密码 2 个 input + 标签 + 复选框 + 忘记密码链接 |
| 4. 提交按钮 | 主按钮"登录" + 副按钮"Google""GitHub"社交登录 |
| 5. 注册引导 | 底部"还没有账号?立即注册"链接 |
合计:1 个 <span>、1 个 <h1>、1 个 <p>、2 个 <input>、2 个 <label>、1 个 <button>、1 个 <a> 忘记密码、2 个 <button> 社交登录、1 个 <a> 注册 = 11 个表单元素 + 1 个品牌 span。手写 HTML:
<span class="brand-name">CLOUDFLOW</span>
<h1 class="title">欢迎登录</h1>
<p class="subtitle">使用账号和密码继续</p>
<form id="loginForm" action="/login" method="post">
<div class="field">
<label for="user">用户名</label>
<input id="user" class="input" type="text" name="user" placeholder="请输入用户名" required>
</div>
<div class="field">
<label for="pwd">密码</label>
<input id="pwd" class="input" type="password" name="pwd" placeholder="请输入密码" required>
</div>
<div class="field-row">
<label class="checkbox">
<input type="checkbox" name="remember"> 记住我
</label>
<a class="help" href="/forgot">忘记密码?</a>
</div>
<button type="submit" class="btn btn-primary">登录</button>
</form>
<div class="divider"><span>或</span></div>
<div class="social">
<button class="social-btn" type="button">Google</button>
<button class="social-btn" type="button">GitHub</button>
</div>
<p class="footer">还没有账号? <a href="/signup">立即注册</a></p>
合计 32 行手写 HTML,加上各种属性细节(id / class / type / name / placeholder / required / for / action / method),Alice 估算需要 8-10 分钟——而且容易漏属性(如忘记 required 导致表单不验证)。
(2) Emmet 一气呵成的解法
Alice 打开 VSCode,按 ! 展开 HTML 骨架,然后定位到 <body> 里,输入一行缩写:
div.login-container>div.login-card>(.brand>span.brand-name{CLOUDFLOW})+h1.title{欢迎登录}+p.subtitle{使用账号和密码继续}+form#loginForm[action=/login method=post]>(.field>label[for=user]{用户名}+input#user.input[type=text name=user placeholder="请输入用户名" required])+(.field>label[for=pwd]{密码}+input#pwd.input[type=password name=pwd placeholder="请输入密码" required])+(.field-row>(label.checkbox>input[type=checkbox name=remember]{记住我})+a.help[href="/forgot"]{忘记密码?})+button[type=submit].btn.btn-primary{登录}+.divider{或}+.social>(button.social-btn{Google})+(button.social-btn{GitHub})+p.footer{还没有账号?}+a[href="/signup"]{立即注册}
按 Tab,32 行 HTML 一键展开。Alice 又用了 10 秒——比手写快 48-60 倍。更重要的是:0 漏属性(required / placeholder / for 全部带上),零笔误。
(3) 收益
Alice 这次"实战 1"用了 10 秒写完表单,比手写快 48-60 倍;并且 0 漏属性、0 拼写错误。最大收益不是速度,而是减少 80% 的"漏属性" bug——手写时容易忘记 required 或 for,Emmet 缩写时这些细节是缩写的"组成部分",不可能漏。下周一上线前 QA 一次通过,零回归。
3. 登录页元素清单:5 个区块映射 Emmet 操作符
下面这张表把 5 个区块映射到具体的 Emmet 操作符——这是"分解需求"的关键步骤。
| 区块 | 元素 | Emmet 写法 | 对应语法(第几课学的) |
|---|---|---|---|
| 1. 品牌头部 | <span class="brand-name">CLOUDFLOW</span> |
span.brand-name{CLOUDFLOW} |
第 03 课隐式标签 + 第 09 课文本 |
| 2. 标题区 | <h1 class="title"> + <p class="subtitle"> |
h1.title{...}+p.subtitle{...} |
第 04 课兄弟节点 + 第 03 课 class + 第 09 课文本 |
| 3. 表单 | <form id="loginForm" action="..." method="..."> 嵌套多个 <div class="field"> |
form#loginForm[action=/login method=post]>(.field>...) |
第 04 课子级 + 第 07 课 id + 第 08 课自定义属性 + 第 06 课分组 + 隐式标签 |
| 4. 提交按钮 | <button type="submit" class="btn btn-primary">登录</button> |
button[type=submit].btn.btn-primary{登录} |
第 08 课自定义属性 + 第 07 课多 class + 第 09 课文本 |
| 5. 社交登录 | <div class="social"> 内 2 个 <button> |
.social>(button.social-btn{Google})+(button.social-btn{GitHub}) |
第 06 课分组 + 第 05 课乘法(也可以 *2)+ 隐式标签 |
4. 一气呵成的 Emmet 缩写(核心)
下面 3 个示例从最小单元逐步组合到完整表单——演示"一气呵成"的思维过程。
▶ 示例:标题块 h1.title{欢迎登录}(难度⭐)
最小单元:1 个 <h1> 带 class 和文本。
缩写:
h1.title{欢迎登录}
展开:
<h1 class="title">欢迎登录</h1>
输出: 渲染为
<h1>标题,class="title" 可被 CSS 选择器命中(如.title { font-size: 28px; })。TEXT 📖 仅展示解析:`h1` 是元素名(HTML 第 03 课),`.title` 是 class(第 07 课),`{欢迎登录}` 是文本内容(第 09 课)。3 个语法点组合——这是 Emmet 缩写的"最小完整单元"。
▶ 示例:表单字段对 label[for=user]{用户名}+input#user.input[type=text name=user placeholder="请输入用户名" required](难度⭐⭐)
表单 1 个字段:<label> + <input>,带 for/id 关联 + 多个属性。
缩写:
label[for=user]{用户名}+input#user.input[type=text name=user placeholder="请输入用户名" required]
展开:
<label for="user">用户名</label>
<input id="user" class="input" type="text" name="user" placeholder="请输入用户名" required>
输出: 渲染为 1 个
<label>+ 1 个<input>,<label for="user">通过id="user"关联到<input>(点击 label 聚焦 input)。<input>带 6 个属性:id/class/type/name/placeholder/required。TEXT 📖 仅展示解析:缩写的"信息密度"远超手写—— - 3 个语法点:`#user`(id)、`.input`(class)、`[type=text]`(属性) - 5 个属性键值对:`type=text` / `name=user` / `placeholder="请输入用户名"` / `required`(布尔属性,无值) - 1 个文本:`{用户名}` 共 9 个 HTML 元素属性,缩写字符仅 95 个——手写同样的属性要 200+ 字符。
▶ 示例:完整登录表单(一行核心缩写)(难度⭐⭐)
完整表单:1 个 <form> + 2 个 <div class="field">(每个含 label+input)+ 1 个 <div class="field-row">(含 checkbox+link)+ 1 个 <button>。
缩写:
form#loginForm[action=/login method=post]>(.field>label[for=user]{用户名}+input#user.input[type=text name=user placeholder="请输入用户名" required])+(.field>label[for=pwd]{密码}+input#pwd.input[type=password name=pwd placeholder="请输入密码" required])+(.field-row>(label.checkbox>input[type=checkbox name=remember]{记住我})+a.help[href="/forgot"]{忘记密码?})+button[type=submit].btn.btn-primary{登录}
展开:
<form id="loginForm" action="/login" method="post">
<div class="field">
<label for="user">用户名</label>
<input id="user" class="input" type="text" name="user" placeholder="请输入用户名" required>
</div>
<div class="field">
<label for="pwd">密码</label>
<input id="pwd" class="input" type="password" name="pwd" placeholder="请输入密码" required>
</div>
<div class="field-row">
<label class="checkbox"><input type="checkbox" name="remember">记住我</label>
<a class="help" href="/forgot">忘记密码?</a>
</div>
<button type="submit" class="btn btn-primary">登录</button>
</form>
输出: 渲染为完整登录表单,2 个 input 字段(用户名 / 密码)+ 1 个 checkbox(记住我)+ 1 个忘记密码链接 + 1 个登录按钮。
TEXT 📖 仅展示解析:这条缩写串起了 1-16 课的所有核心语法—— - `form#loginForm`:元素名 + id(第 03 + 07 课) - `[action=/login method=post]`:2 个自定义属性(第 08 课) - `>(...)`:子级嵌套 + 分组(第 04 + 06 课) - `.field` / `.field-row`:隐式标签 `div`(第 03 课) - `label[for=user]{用户名}`:元素 + 自定义属性 + 文本(第 08 + 09 课) - `input#user.input[type=text ...]`:id + 多 class + 多属性(第 07 + 08 课) - `+`:兄弟节点(第 04 课) - `button[type=submit].btn.btn-primary{登录}`:多属性 + 多 class + 文本(第 07 + 08 + 09 课) 共 14 个 HTML 标签、35+ 个属性——手写 200+ 字符,缩写 270 字符展开后 200+ 行手写 HTML。
5. 缩写拆解:每个符号的语法含义
上一节的"完整表单"缩写 270 字符,看一眼头晕。下面把它拆成 5 段,逐段讲每个符号:
| 段 | 缩写片段 | 语法点 | 含义 |
|---|---|---|---|
| 1 | form#loginForm[action=/login method=post] |
元素 + id + 自定义属性(第 03+07+08 课) | <form id="loginForm" action="/login" method="post"> |
| 2 | >(...) |
子级 + 分组(第 04+06 课) | 进入 form 内部,包成一个分组 |
| 3 | (.field>label[for=user]{用户名}+input#user.input[type=text name=user placeholder="请输入用户名" required]) |
隐式 div + 子级 + 元素 + 属性 + id + class + 文本(第 03+04+07+08+09 课) | 1 个 .field 分组:内含 label + input |
| 4 | +(.field>label[for=pwd]{密码}+input#pwd.input[type=password name=pwd placeholder="请输入密码" required]) |
兄弟节点 + 重复段 3 结构 | 第 2 个 .field(密码字段),与段 3 兄弟 |
| 5 | +(.field-row>(label.checkbox>input[type=checkbox name=remember]{记住我})+a.help[href="/forgot"]{忘记密码?})+button[type=submit].btn.btn-primary{登录} |
兄弟节点 + 多层分组 + button 多属性 | 复选框 + 忘记密码 + 登录按钮 |
.field-row 内嵌 2 个兄弟(label.checkbox + a.help),再 + 一个 button[type=submit]。多层分组 + 兄弟节点嵌套是 Emmet 高阶的核心——熟练后能写"一行 = 20 行 HTML"。
6. 完整示例:登录页一气呵成 70 行
把上面的核心缩写嵌入完整 HTML 页面(含 <!DOCTYPE> / <head> / <style> / <body> 包裹 + 完整 CSS 样式),得到 70 行的完整登录页 HTML。这个示例是本课核心——不要加"试一试"按钮(按规范 v2.5 门禁 #26,综合示例不在编辑器中预览)。
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>管理后台 - 登录</title>
<meta name="description" content="SaaS 后台管理登录页">
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, -apple-system, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; }
.login-container { width: 100%; max-width: 420px; }
.login-card { background: #fff; padding: 40px; border-radius: 12px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15); }
.brand { text-align: center; margin-bottom: 24px; }
.brand-name { font-size: 18px; font-weight: 700; color: #2c3e50; letter-spacing: 1px; }
.title { font-size: 28px; color: #2c3e50; text-align: center; margin-bottom: 8px; font-weight: 700; }
.subtitle { color: #7f8c8d; text-align: center; margin-bottom: 32px; font-size: 14px; }
.field { margin-bottom: 20px; }
label { display: block; color: #34495e; margin-bottom: 8px; font-size: 14px; font-weight: 500; }
.input { width: 100%; padding: 12px 16px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; transition: border-color 0.2s; }
.input:focus { outline: none; border-color: #3498db; box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.15); }
.field-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; font-size: 14px; }
.checkbox { display: flex; align-items: center; gap: 8px; color: #7f8c8d; cursor: pointer; }
.checkbox input { margin: 0; cursor: pointer; }
.help { color: #3498db; text-decoration: none; }
.help:hover { text-decoration: underline; }
.btn { width: 100%; padding: 14px; border: none; border-radius: 8px; font-size: 16px; font-weight: 600; cursor: pointer; transition: background 0.2s; }
.btn-primary { background: #3498db; color: #fff; }
.btn-primary:hover { background: #2980b9; }
.divider { display: flex; align-items: center; margin: 24px 0; color: #95a5a6; font-size: 13px; }
.divider::before, .divider::after { content: ''; flex: 1; height: 1px; background: #ddd; }
.divider span { padding: 0 16px; }
.social { display: flex; gap: 12px; }
.social-btn { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 8px; background: #fff; cursor: pointer; font-size: 14px; }
.social-btn:hover { background: #f8f9fa; }
.footer { text-align: center; margin-top: 24px; color: #7f8c8d; font-size: 14px; }
.footer a { color: #3498db; text-decoration: none; font-weight: 500; }
</style>
</head>
<body>
<main class="login-container">
<div class="login-card">
<div class="brand">
<span class="brand-name">CLOUDFLOW</span>
</div>
<h1 class="title">欢迎登录</h1>
<p class="subtitle">使用账号和密码继续</p>
<form id="loginForm" action="/login" method="post">
<div class="field">
<label for="user">用户名</label>
<input id="user" class="input" type="text" name="user" placeholder="请输入用户名" required>
</div>
<div class="field">
<label for="pwd">密码</label>
<input id="pwd" class="input" type="password" name="pwd" placeholder="请输入密码" required>
</div>
<div class="field-row">
<label class="checkbox">
<input type="checkbox" name="remember"> 记住我
</label>
<a class="help" href="/forgot">忘记密码?</a>
</div>
<button type="submit" class="btn btn-primary">登录</button>
</form>
<div class="divider"><span>或</span></div>
<div class="social">
<button class="social-btn" type="button">Google</button>
<button class="social-btn" type="button">GitHub</button>
</div>
<p class="footer">还没有账号? <a href="/signup">立即注册</a></p>
</div>
</main>
</body>
</html>
输出: 浏览器渲染为完整的 SaaS 后台登录页——紫色渐变背景、白色卡片居中、品牌名 "CLOUDFLOW"、欢迎登录标题、用户名/密码表单(带标签关联和 required 验证)、记住我复选框、忘记密码链接、蓝色登录按钮、"或"分割线、Google/GitHub 社交登录、底部注册引导。所有样式在
<style>中内联,0 外部依赖,0 第三方库。TEXT 📖 仅展示解析:本课 70 行 HTML 串起了 1-16 课全部 HTML 简写语法—— - `<style>` 块(25 行 CSS)由开发者手写或用 CSS 简写(第 11-13 课);本课聚焦 HTML 部分 - `<body>` 内 `<main class="login-container">` 到 `</main>` 的 32 行 HTML 由 Emmet 一行缩写展开 - 行数对比:手写 32 行 HTML 用 8 分钟;Emmet 缩写 270 字符 + Tab 展开 10 秒。**速度提升 48 倍,漏属性率从 20% 降到 0%**
🌐 动手试试: 在 Emmet Live Demo 输入本课核心缩写
form#loginForm[action=/login method=post]>(.field>label[for=user]{用户名}+input#user.input[type=text name=user placeholder="请输入用户名" required])+(.field>label[for=pwd]{密码}+input#pwd.input[type=password name=pwd placeholder="请输入密码" required])+button[type=submit].btn.btn-primary{登录},按 Tab 键,观察实时展开效果。
❓ 常见问题
{text} 内)可以写任意字符——包括中文、emoji、空格。但属性值([attr=val] 内)如果有空格必须加引号包裹(如 [placeholder="请输入用户名"])。不建议在 id/class 写中文——CSS 选择器对中文 id/class 兼容性差(IE 旧版不支持),强烈建议 id/class 用英文。placeholder 这种长属性名需要写全吗?[placeholder=...] 必须写全。但属性值可以省略引号(无空格时):[type=text] 等价于 [type="text"]。[placeholder=请输入用户名] 这种"带空格"的属性值必须加引号:[placeholder="请输入用户名"],否则 Emmet 解析会出错。action="/login" 表示提交到后端的 /login 接口。真实开发中,action 一般是后端路由(如 Express 的 app.post('/login', ...)、Django 的 path('login/', views.login))。本课聚焦 HTML 部分,后端处理不展开。如果你要纯前端 mock,可以用 <form> 的 action="javascript:void(0)" 加 JS 监听 submit 事件拦截。required 属性?required 是 HTML5 引入的"必填验证"——浏览器原生检查输入框是否为空,空值时阻止提交并提示"请填写此字段"。本课示例中用户名/密码 2 个 input 都有 required,忘记密码链接不需要(不是表单字段)。这是"前端零 JS 验证"的关键属性,强烈建议所有必填字段都加。手写容易漏,Emmet 缩写时是"结构化必填项",漏的概率为 0。📖 小节
- 5 步思维链:需求拆区块 → 区块拆元素 → 元素翻译 Emmet → 串联整页 → 加 head/style 包装
- 核心缩写:
.field/.field-row/.btn.btn-primary/form#id[action=... method=...]/[type=text name=... placeholder="..." required] - 避免属性遗漏:Emmet 缩写是"结构化的",手写容易漏
required/for/placeholder,缩写不会 - 多 class 链:
.btn.btn-primary→class="btn btn-primary"(CSS 选择器风格) - 属性值引号:无空格可省略(如
[type=text]),有空格必须加(如[placeholder="请输入用户名"]) - 本课核心:把 1-16 课的"碎片语法"组合成"实战一行"——从"看得懂缩写"到"写得出一行"的关键跨越
📝 作业
- 基础题(难度⭐):用一行 Emmet 缩写写 1 个简单登录页骨架——包含 1 个
<h1 class="title">用户登录</h1>、1 个用户名<input>(带id="email"、type="email"、name="email"、required)、1 个密码<input>(带id="pwd"、type="password"、name="pwd"、required)、1 个<button type="submit">登录</button>。展开后验证:(a)<input>的type属性正确;(b)<button>的type="submit"在 form 内会触发表单提交;(c) 2 个required都生效(空值时浏览器阻止提交)。 - 进阶题(难度⭐⭐):把本课 70 行完整示例的 Emmet 缩写自己重写一遍——不需要和 Alice 的完全一致,但功能等价:至少 1 个 form、2 个 input、1 个 checkbox、1 个忘记密码链接、1 个登录按钮、1 个注册链接。再加 2 个社交登录按钮(Google、GitHub)。对比你写的缩写 vs Alice 的缩写,看哪个更简洁(字符数更少)。截图保存你的缩写和展开后的 HTML。
- 挑战题(难度⭐⭐⭐):把本课 70 行登录页扩展为完整注册页——除了登录页的字段外,增加:手机号(
type="tel")、邮箱(type="email")、验证码(type="text"+ 右侧"获取验证码"按钮)、用户协议 checkbox(必勾选)。要求:(a) 整页 HTML 一气呵成(一行 Emmet 缩写写完 body 部分);(b) 缩写字符数 ≤ 400(超过的话考虑用*乘法或简化结构);(c) 所有 input 都有required;(d) 用户协议 checkbox 必须勾选才能提交(用<input type="checkbox" required>实现,浏览器原生验证)。最后统计手写 vs Emmet 的字符数和时间差,写一份"Emmet 综合实战收益报告"。