ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Emmet:从HTML缩写到高效前端开发的核心技巧实战指南

Emmet:从HTML缩写到高效前端开发的核心技巧实战指南

先说我前两天遇到的一件事。某位同事写一个带分页筛选的后台列表页,光<select>下面的<option>就手动复制粘贴了四十多行,改一个再复制一个,半个小时过去了还没排完版式。我过去看了一眼,跟他说你敲个select>option[value=$]*5,按一下展开键,五条 option 瞬间生成,再把 value 换成真实数据就行。他当场愣住,问我这是什么操作。这就是 Emmet——一个能直接把一行缩写翻译成完整 HTML、CSS 结构的效率工具。

Emmet 最初叫 Zen Coding,后来更名为 Emmet,现在主流的现代代码编辑器基本都内置或者可以轻松安装这个能力。它的核心用法就一句话:你写一段有点像 CSS 选择器的缩写,编辑器帮你把它展开成完整的标签结构。尤其是在前端写页面骨架、搭建表单、写列表和表格这种重复度极高的场景,它的提速效果不是"省几秒钟"的量级,而是"省掉一整段机械劳动"的量级。这篇文章不打算给你罗列官方文档,而是把我自己从入门到形成肌肉记忆的过程、踩过的坑、以及真正能在项目里落地的用法,完整拆开讲一遍。适合那些已经会写 HTML、CSS,但还在一个个尖括号手敲标签的人;也适合刚入门前端、想少加班的新手。

1. 手写HTML时那些被忽略的时间黑洞

很多人觉得自己写 HTML 挺快,无非就是标签配对、补类名、敲属性。但真到项目里,你面对的不是一个 div 里塞一行文字,而是一整块导航、一整张表格、一整组筛选条件。问题就出在这些重复性很强的结构上。

1.1 为什么大多数开发者没把缩写用起来

我在不同团队里观察过,真正每天在使用 Emmet 的人其实不到一半。原因不是学不会,而是没有在合适的时机把它纳入流程。多数人第一次接触 Emmet 是在教程里看到一个div>ul>li*3的例子,随手试了一下觉得挺神奇,然后就没有然后了。因为日常写业务代码时,脑子里想的是数据、交互、状态,很少有人会在写结构之前先想一下"这段能不能用缩写表达"。时间一长,就又回到手敲标签的状态。

另一个原因是很多编辑器默认没把展开快捷键调成好记的模式。比如在某些环境里,展开命令是 Ctrl+E,在另一些环境里是 Tab。如果你只知道一个键,换到别的编辑器就感觉失灵了,于是干脆不用。所以建议从第一天开始,就把展开快捷键固定在肌肉记忆里,不管去哪台电脑,第一件事是先确认当前编辑器的 Emmet 展开键是什么。

1.2 Emmet 到底是工具还是习惯

答案兼顾两者。它首先是一套被主流编辑器支持的解析引擎,把缩写文本翻译成结构文本。但对我来说,它更像一种写代码之前的"思维压缩方式":当你看到一段复杂结构时,先在心里把它压缩成一行简写,再用 Emmet 展开。这个过程练多了之后,你写 HTML 的速度反而不是瓶颈,真正的瓶颈只在业务逻辑本身。

举个例子,一个典型的三栏布局骨架:

<div class="container"> <header class="header"> <nav class="nav"> <ul class="nav-list"> <li class="nav-item"><a href="">链接</a></li> <li class="nav-item"><a href="">链接</a></li> <li class="nav-item"><a href="">链接</a></li> </ul> </nav> </header> <main class="main"></main> <aside class="aside"></aside> <footer class="footer"></footer> </div>

这段代码手打大概需要一到两分钟,里面最烦的不是标签名,而是结束标签和缩进层级。用 Emmet 写则是这一行:

.container>.header>.nav>.nav-list>.nav-item>a[href="#"]{链接}+.main+.aside+.footer

等等,上面这行其实有点绕,实际更常见的写法是分组和上爬配合。但核心意思已经出来了:同样的结构,输入量从几十行变成一行,而且不需要手动补</div>。这就是工具的意义:把机械劳动交给解析器,把注意力留给真正需要思考的地方。

1.3 谁适合把这份时间花在学习上

如果你是纯后端,偶尔改一两个模板页面,那我建议你只需要记住最基本的div>ul>li*3这类用法就够了,不值得为了冷门语法花时间。但如果你是前端、全栈,或者经常写静态页面、邮件模板、后台系统,那么 Emmet 值得当成正式技能对待。它能帮你节省的不只是打字时间,更是切换思路的时间。写结构写到一半被打断,回来之后看着半截缩写的恢复成本,远低于看着半截手打 HTML 的恢复成本。

2. 缩写展开的底层逻辑:一行文本到一棵节点树

讲用法之前,我觉得有必要把 Emmet 的展开原理讲明白。因为理解了它的解析模型,你就不用背语法表了,遇到任何没见过的缩写组合,自己也能推导出结果。

2.1 缩写解析的大致模型

你可以把缩写想象成一段压缩的"结构描述"。解析器拿到这段描述后,先按符号切分成一个个节点,再根据符号决定节点之间的父子关系、兄弟关系、重复次数,最终生成一棵节点树,最后按照格式化规则输出成带缩进的 HTML 文本。

举个具体例子:

div>ul>li

这段缩写的解析过程大致是:

  • 遇到div,创建根节点。
  • 遇到>,说明后面的ul要作为div的子节点。
  • 遇到ul,创建节点。
  • 再遇到>,说明后面的li要作为ul的子节点。

生成结果:

<div> <ul> <li></li> </ul> </div>

就是这么朴素。你不需要理解 AST 这种高级概念,只要记住四个核心符号的含义,基本上就掌握了 70% 的用法:

符号含义例子
>子节点div>p
+兄弟节点div+p
^回到上一层div>span^p
*重复li*3

2.2 隐式标签名:编辑器替你脑补了什么

Emmet 有个很有特色的规则叫隐式标签名。意思是有些标签不需要你写全,它会根据父容器自动补全。最经典的就是在ul或ol里直接写.item,它会生成li。

ul>.item*3

展开结果:

<ul> <li class="item"></li> <li class="item"></li> <li class="item"></li> </ul>

同样的规则还包括:

  • 在table、thead、tbody、tfoot里写.row或直接写.cell,会生成tr或td。
  • 在select里写.opt,会生成option。
  • 在map里写.area,会生成area。

第一次用的人可能会觉得有点"魔法",但用熟之后你会发现,这意味着你写列表时连li都不用打了,只要写类名。这对于批量生成结构非常顺手。

2.3 展开后的输出控制:格式化与换行逻辑

很多人关心一个问题:展开出来的代码缩进是几个空格?换行风格是什么?实际上,Emmet 并不负责代码格式化,它只生成带基础缩进的文本,最终样式取决于编辑器内置的格式化工具。比如有的编辑器默认展开后是 4 空格缩进,有的跟随你的设置。如果你遇到的展开结果缩进特别乱,不用在 Emmet 里找配置,直接检查编辑器的格式化配置文件就行。

这里有一个我自己常用的组合技巧:先写缩写,展开一次,再按一次格式化快捷键。这样即使缩写里因为有分组导致层级复杂,输出结果也会被整理得干干净净。省去手动对齐的功夫。

3. HTML缩写的十六个高频符号:从入门到组合拳

这一节我打算把真正高频的写法按场景拆开,每个都给出缩写和展开结果对照。看完你就能直接照抄到项目里。

3.1 最基础的 ID 与 class

在 Emmet 里,ID 用#表示,class 用.表示,跟 CSS 选择器一模一样的思路。

div#main.content

展开:

<div id="main" class="content"></div>

div标签名也可以省略。如果你直接写#main.content,默认补全为<div>。这是写页面骨架最高频的操作,没有之一。

3.2 属性与文本:把最常见的场景压缩成一行

有些标签需要带上属性,比如链接、图片、输入框。Emmet 用中括号表示属性,用花括号表示文本。

a[href="#"]{首页} img[src="logo.png"][alt="logo"] input[type="email"][placeholder="请输入邮箱"]

展开结果:

<a href="#">首页</a> <img src="logo.png" alt="logo"> <input type="email" placeholder="请输入邮箱">

这里提醒一个细节:多个属性可以写在同一组中括号里,用空格分隔;也可以连续写多组中括号,效果一样。我更推荐同一组,因为视觉上更紧凑。

注意img这种单标签展开后不会带自闭合斜杠,这跟 HTML 规范和编辑器配置有关。如果你在写 XHTML 风格模板需要自闭合,可以在编辑器的 Emmet 配置里调整输出选项。

3.3 乘法重复与序号:告别复制粘贴四十行

回到开头那个同事的例子。写下拉选项,传统做法是先写一个 option,然后复制粘贴,再手动改 value 和文本。Emmet 的做法是:

select>option[value=$]*5

展开:

<select> <option value="1"></option> <option value="2"></option> <option value="3"></option> <option value="4"></option> <option value="5"></option> </select>

这里的$就是序号占位符,它会根据重复次数自动从 1 递增到 N。如果想从 0 开始,可以用$@0。如果想倒序,可以用$@-。多写几个$$还可以生成带前导零的序号:

li.item$$$*3

展开:

<li class="item001"></li> <li class="item002"></li> <li class="item003"></li>

如果你还需要在文本里也带上序号,直接写{列表项 $}即可。这个能力在生成表格行、列表项、循环测试数据时非常实用。

3.4 嵌套、上爬与分组:复杂布局的组合拳

单靠>只能表达严苛的父子递进,实际页面经常需要在同一层级多个节点里继续嵌套。这时候要用^上爬和()分组。

先看上爬:

div>header>nav^^main

它的含义是:div下面有header,header下面有nav,但main不和nav同级,它要上爬到header这一层的上方,也就是和header并列,成为div的子节点。

展开:

<div> <header> <nav></nav> </header> <main></main> </div>

但你真在项目里写复杂头部时,纯用>和^会很难读。这时候就要用分组。分组的思路是把一段小结构先框起来,再跟其他兄弟节点组合:

header>(h1.logo>a[href="#"]{品牌名})+nav>ul>li*3>a[href="#"]{导航项}

展开:

<header> <h1 class="logo"><a href="#">品牌名</a></h1> <nav> <ul> <li><a href="#">导航项</a></li> <li><a href="#">导航项</a></li> <li><a href="#">导航项</a></li> </ul> </nav> </header>

看见没有,一个完整页头瞬间就出来了。这种结构对新手来说可能看一眼就晕,但你自己拆解一下:header是根节点,括号里的内容整体作为header的第一个子节点h1,括号外的nav作为第二个兄弟节点。分组的好处就是让逻辑层次一目了然,不至于被连续的^推出 bug。

3.5 一个完整页面骨架的实战推演

最后串一个完整例子,目标是一个后台管理页的常见布局:顶部 header,左侧 aside,右侧 main,main 里再放一个文章列表。

缩写可以这样写:

.page>(header>h1{后台管理}+.user-info)+aside>.menu>.menu-item*4{菜单项}+main>.article-list>.article-item*5>.article-title{文章标题 $}

展开之后就是:

<div class="page"> <header> <h1>后台管理</h1> <div class="user-info"></div> </header> <aside> <div class="menu"> <div class="menu-item">菜单项</div> <div class="menu-item">菜单项</div> <div class="menu-item">菜单项</div> <div class="menu-item">菜单项</div> </div> </aside> <main> <div class="article-list"> <div class="article-item"> <div class="article-title">文章标题 1</div> </div> <div class="article-item"> <div class="article-title">文章标题 2</div> </div> <div class="article-item"> <div class="article-title">文章标题 3</div> </div> <div class="article-item"> <div class="article-title">文章标题 4</div> </div> <div class="article-item"> <div class="article-title">文章标题 5</div> </div> </div> </main> </div>

到这里你可能会问,这缩写也很长啊。确实,它不是用来省字符的,是用来省"结束标签"和"层级缩进"的。输出结构确定后,剩下的工作就是往里面填内容和业务逻辑。我第一次把它用到真实项目时,光一个页面骨架的搭建从半小时压缩到三分钟,这种体感差异是促使我继续深挖的最大动力。

4. CSS缩写没有死记硬背的规则,理解代号就能上手

Emmet 不只管 HTML,它在 CSS 里同样能提速。CSS 缩写的思路和 HTML 不太一样,它更像一套智能补全:你写几个字母,它猜测你要写的属性或属性值。

4.1 属性名输入:模糊匹配背后的规则

CSS 属性名的 Emmet 缩写遵循一个简单的原则:一般取属性名中每个单词的首字母,或者取开头几个字母。比如:

  • pos展开为position
  • w展开为width
  • h展开为height
  • bg展开为background
  • ta展开为text-align
  • ff展开为font-family
  • d展开为display
  • c展开为color

你不需要背全量字典,因为绝大多数编辑器的 Emmet 支持模糊匹配。比如你写v-ali,它也能帮你联想到vertical-align。规则是:常用属性用极短缩写,生僻属性写几个字母足够联想。

4.2 属性值的缩写与单位补全

CSS 属性值除了要写值本身,还经常要带单位。Emmet 的规则是:不加单位默认补px,加字母指定其他单位。

w100

展开为:

width: 100px;

如果想补百分比:

w100p

展开为:

width: 100%;

单位字母对应关系:

字母单位例子
p%w50p→width: 50%
eemfz1.5e→font-size: 1.5em
rremm2r→margin: 2rem
xexlh2x→line-height: 2ex
vhvhh100vh→height: 100vh
vwvww50vw→width: 50vw

注意,这里的p只能出现在表示单位的位置,比如w50p是width: 50%,但如果你写p10,它优先理解为padding: 10px,这是因为属性名识别优先级高于值的单位识别。这个边界需要靠实际使用来掌握。

4.3 多个值与重要标记

写盒子模型时经常要一次性给四个方向的值。Emmet 支持用连字符分隔多值:

m10-20

展开为:

margin: 10px 20px;

m10-20-30-40展开为:

margin: 10px 20px 30px 40px;

这里的顺序跟 CSS 标准一致:上右下左。如果你记不清,可以只写两个值,或者写完再手动调整。

!符号用来标记!important:

c:red!

展开为:

color: red !important;

我自己的习惯是:只有在调试临时覆盖样式时才用 Emmet 生成!important,正式代码不太建议频繁用这个写法,因为后面维护的人看到一堆!important会头疼。

4.4 哪些CSS建议亲手写而不是依赖展开

Emmet 也并非越是快捷键越好。CSS 里有两类场景我建议手动写:一类是复杂的grid-template-areas和flex排列组合,因为缩写展开的结果往往是一组默认值,你还是要改,反而不如直接写清晰;另一类是包含渐变、动画关键帧的参数,缩写识别容易出错。概括一下就是:简单、高频、重复的属性适合 Emmet,复杂、自定义、多条件判断的样式适合手写。

CSS 缩写的真正价值在于把margin: 0; padding: 0;这种高频样板代码压缩成m0+p0,几秒搞定,而不在于炫技式地缩写一切。

5. 常见坑与编辑器协同经验

这里讲我在真实项目里用过之后总结出的几个问题。能避开的避开,避不开的至少心里有数。

5.1 展开快捷键冲突与中文输入法问题

最常遇到的问题是展开键被其他功能占用,比如有的编辑器把 Tab 键绑定为接受自动补全建议。你写完了缩写,按 Tab 没有反应,第一反应是"Emmet 坏了",其实不是。解决方案是在编辑器的快捷键设置里,把 Emmet 展开命令绑定到一个你顺手的键。我开始使用 Emmet 的几年里,一直用 Ctrl+E,因为它在绝大多数编辑器里都不会被其他功能抢占。

还有中文输入法的问题:有些输入法在全角状态下会把>符号拦截,导致div>p的缩写看起来没问题,展开却失败。遇到这种情况,先切成英文输入法再写缩写,不要怀疑是你的语法错了。

5.2 模板文件类型不生效的情况

Emmet 默认只对 HTML、CSS、XML 这类文件类型生效。如果你在一个后缀名不标准的模板文件里写缩写,比如.tpl、.vue里的 template 块,不同的编译器处理方式不一样。大部分情况下,你需要在编辑器的文件类型关联里,把这些模板后缀关联为html。Vue 单文件组件里如果没生效,通常需要在编辑器设置里开启对嵌入式语言的 Emmet 支持,或者在.vue文件的<template>块内确认语言模式为 html。

5.3 展开结果与代码风格冲突

有些团队代码规范要求没有子内容的标签自动写成自闭合形式,或者要求单引号、无引号属性值。Emmet 的默认输出通常是你输入的属性风格,如果你在缩写里写[href='#'],展开结果就会用单引号,如果你写[href="#"],结果就是双引号。这一点对风格敏感的项目很关键:先统一自己的属性书写习惯,再使用缩写,否则展开后还要全局跑一遍格式化。

另外,展开结果保留的缩进不一定符合团队的 Prettier 或风格指南。我的经验是不要试图在 Emmet 配置里精细控制缩进,直接展开后用编辑器的格式化功能统一风格,成本最低。

5.4 不要为了缩写而缩写:可读性边界

这是我最想强调的一点。Emmet 的缩写本身就是一种"压缩过的高密度信息"。一行缩写如果超过 80 个字符,其实已经很难一眼看懂它想生成什么了。我在项目里见过有人用一行超长的缩写生成整个页面头部,结果半年后他自己都读不懂,修改时只能重新展开成 HTML 再看。这就得不偿失。

我的处理方式是把缩写控制在两层嵌套以内。超过两层,就拆成两三个片段分别展开,然后用多光标快速调整。比如先展开外层容器,再把光标定位到内部节点,继续补内层缩写。每个缩写都短小、清晰、可读,后期维护成本也低。

6. 给自己建立一套常态化的工作流

最后分享一些我在实际工作中摸索出来的流程。不一定适用于每个人,但可以给你作参考。

第一,把所有和 Emmet 相关的配置固化下来。展开键、文件类型关联、是否自动格式化,这些一到新环境就检查一遍。别小看这个动作,它能避免你从一台开发机换到另一台时,突然发现 Emmet 不工作了。

第二,把重复性最多的业务结构做成自定义片段。Emmet 支持在编辑器配置里写自定义 snippets。比如我们团队经常要写一个标准卡片组件,我就在配置里加了一个card片段:

"html": { "snippets": { "card": ".card>(.card-header>.card-title+.card-meta)+.card-body+.card-footer" } }

之后在工作里敲card,按展开键,整个结构直接出来。每个团队都有自己的高频块,找到它们并封装成片段,比每次写全量缩写更省事。

第三,刻意练习。学 Emmet 不用专门抽时间背,只需要在日常写的每个页面里,强迫自己先用缩写表达一次,再展开,连续一周就能形成肌肉记忆。那个曾经手抄 option 的同事,两周后已经能流畅地用 Emmet 写后台 CRUD 页面,他说自己现在最怕的不是写结构,而是改别人的手写乱 HTML——这算是另一个话题了。

说到底,Emmet 能提高 HTML、CSS 编写效率,不是因为它是什么高深技术,而是因为它非常贴合前端"结构即文本"的本质。熟练之后你再回头看手敲标签的日子,会觉得自己当初浪费了太多时间在复制和配对上了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进