很多人以为 HTML 简单——不就是一堆尖括号吗?写几个 div、塞点文字、加个 img,页面就出来了。可真正把 HTML 写对的人并不多。浏览器面对一份残缺、嵌套错误、属性乱写的 HTML,会启动一整套容错机制去“猜”你的意图:table 里少了 tr 它会自动补、p 里嵌套 div 它会强行闭合、忘记声明编码就会乱码。这些“猜”出来的结构,和你脑子里想的结构往往不是一回事。HTML 不是“随便写写浏览器都能显示”的松散标记,而是一份有严格规则、会被解析成一棵文档树的正式文档。本篇 25 分钟长文,从 HTML 的本质讲起,依次拆解文档骨架、head 里的元信息、元素分类与文档流、文本与列表、链接与路径、图片与媒体、表格与表单、全局属性、浏览器解析流程,最后给出一份可落地的重构对比与检查清单。
一、HTML 是什么:一门描述结构的语言
HTML 的全称是 HyperText Markup Language,超文本标记语言。拆开看这三个词,其实已经把它的职责说清楚了:
- HyperText(超文本):文档之间可以通过链接互相跳转,这是 Web 之所以是“网”的根本。
- Markup(标记):用标签给内容“打标记”,说明这一段是什么、那一块是什么。
- Language(语言):它有语法、有规则、有解析器,写错了会有明确的处理方式。
需要注意的是,HTML 不是编程语言。它没有变量、没有循环、没有条件判断,不会“执行”,只会被“解析”。你写的每一个标签,最终都会变成一个 DOM 节点,组成一棵树,交给 CSS 去排版、交给 JavaScript 去操作。
从 HTML4 到 Living Standard
HTML 的版本演进史,本身就是一部 Web 的发展史:
“Living Standard”(活标准)意味着 HTML 不再有“HTML6”这种大版本发布,规范是持续演进的。dialog 的 closedby 属性、details 的 name 互斥、popover 属性,都是在近两年陆续进入规范的。所以你不需要等“下一版 HTML”,新特性会不断出现在你每天写的代码里。
一份 HTML 文档,本质是一棵树
这是理解 HTML 最重要的一句话。浏览器读到你的源码后,会把它解析成一棵节点树(DOM 树)。嵌套关系就是父子关系,兄弟关系就是同一层级的节点。看下面这段结构:
<header>
<h1>标题</h1>
</header>
<main>
<p>正文</p>
</main>
</body>
解析后,body 是 header 和 main 的父节点,header 是 h1 的父节点,h1 是 header 的子节点。你在 DevTools 里看到的那个可以逐层展开的面板,就是这棵树的直观呈现。
理解了树,很多事情就顺了:CSS 选择器沿树查找、JavaScript 用 parentNode / children 遍历、屏幕阅读器按树朗读、搜索引擎按树判断层级。你写下的嵌套关系,就是这棵树长什么样。
二、文档骨架:五秒钟写出一个合格的 HTML 页面
不管是手写还是编辑器一键生成,一个合格的 HTML 文档至少要包含下面这些部分。它们每一个都有明确的作用,删掉任何一个都会带来实际问题。
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>页面标题</title>
</head>
<body>
<h1>你好,世界</h1>
</body>
</html>
DOCTYPE:告诉浏览器用哪套规则
<!DOCTYPE html> 必须写在文档的第一行,且不能有任何内容在它前面(包括注释和 BOM 之外的空白)。它的唯一作用是触发标准模式(Standards Mode)。
如果漏写 DOCTYPE,浏览器会进入怪异模式(Quirks Mode),用一套模拟上世纪 90 年代浏览器的规则来渲染你的页面:box-sizing 的默认值变了、行内元素的高度计算方式变了、百分比宽度的解析方式变了。你的 CSS 会莫名其妙地“不对劲”,而排查起来极其痛苦。
HTML5 的 DOCTYPE 被简化到了极致,只有 html 四个字符,不再需要写 DTD 声明。这是对旧写法的一点点减负,但它的重要性丝毫没有降低。
html 与 lang:别小看这个属性
lang="zh-CN" 看起来只是个装饰,实际上它影响着好几件事:
- 屏幕阅读器:会依据
lang切换发音引擎。如果标成en却写着中文,读出来的是一串奇怪的拼音式发音。 - 浏览器翻译:Chrome 判断是否需要弹出“是否翻译此页面”的提示,靠的就是它。
- 断词与连字符:不同语言的断词规则不同,
hyphens依赖语言标记。 - 字体选择:部分系统会依据语言选择更合适的中文字形(简体 vs 繁体)。
- 语音合成与检索:搜索引擎和 TTS 引擎都会读取这个信号。
页面中如果有局部内容使用了其他语言,可以单独在元素上再标一次:
head 与 body 的分工
head 里放的是给机器看的信息:编码、标题、描述、样式表、脚本、图标。这些内容不会直接显示在页面上(title 显示在标签页,meta description 显示在搜索结果里)。
body 里放的是给用户看的内容:文字、图片、链接、表单、视频。所有可见内容都必须写在 body 里。
一个常见的低级错误是:把内容写在 body 之外。</body> 之后写的文字,浏览器解析时会“贴心地”把它们挪到 body 末尾,但此时文档结构已经乱了,某些情况下这些内容会被直接丢弃。
lang、漏写 viewport。前两个影响解析与朗读,第三个会让页面在手机上被缩放到一个虚拟的 980px 宽视口里,字小得看不清。
lang + charset + viewport + title,这五项是所有页面的最低配置,建议做成编辑器代码片段。
三、head 里的元信息:页面的“后台配置”
head 里的内容虽然用户看不到,却决定了页面如何被加载、被索引、被分享。一个完整的 head 通常包含以下几类内容。
字符编码:charset 必须靠前
<meta charset="UTF-8"> 应该出现在 head 的最前面,规范要求它必须位于文档的前 1024 字节内。原因是:浏览器需要先知道编码,才能正确解码后面的字节。如果编码声明太靠后,浏览器已经用默认编码(通常是 Latin-1 或系统编码)解析了一段内容,中文就会变成乱码。
编码声明有三种方式,优先级从高到低:HTTP 响应头 Content-Type、BOM 字节顺序标记、HTML 里的 meta charset。服务端配置优先于页面内声明,所以如果两者不一致,以响应头为准——这也是“本地正常、线上乱码”的常见原因。
视口与移动端
没有这一行,移动端浏览器会假设你的页面是为桌面设计的,用一个约 980px 宽的虚拟视口渲染,然后整体缩小,用户看到的就是一屏密密麻麻的小字。有了它,视口宽度等于设备宽度,CSS 媒体查询才能真正按预期工作。
另外两个常见取值:maximum-scale 和 user-scalable=no。强烈建议不要使用它们——禁止用户缩放会直接伤害低视力用户,这属于无障碍的严重问题,而且现代浏览器(如 iOS Safari)已经开始忽略这两个设置。
标题与描述
<meta name="description" content="从文档骨架、head 元信息、元素分类到浏览器解析流程,系统讲解 HTML 基础与文档结构。">
title 是页面唯一的“名字”,会显示在浏览器标签页、书签、历史记录和搜索结果标题里。它应该先写具体内容,再写站点名,并且每个页面都要不同。
description 不参与排名计算,但它常常被搜索引擎用作搜索结果里的摘要文案。写成一段 70~150 字、能概括页面核心价值的自然语句,比堆砌关键词有效得多。注意:搜索引擎不保证使用你写的 description,如果它判断页面正文里有更合适的内容,会自行截取。
样式表与脚本
<link rel="icon" href="favicon.ico">
<link rel="canonical" href="https://example.com/article">
<script src="js/app.js" defer></script>
defer 和 async 是 script 上最值得理解的两个属性:
经验法则:业务代码用 defer,第三方独立脚本(埋点、广告)用 async,内联脚本尽量别用。
社交分享与 theme-color
当页面被分享到社交平台时,平台会抓取 Open Graph 或 Twitter Card 元信息来生成卡片。这些不是“可选项”,而是内容型站点的标配:
<meta property="og:description" content="系统讲解 HTML 基础与文档结构。">
<meta property="og:image" content="img/7.avif">
<meta property="og:type" content="article">
<meta name="theme-color" content="#2563eb">
theme-color 会让移动端浏览器的地址栏跟随你的品牌色变化,是一个几乎零成本、效果却很明显的细节。
四、元素分类与文档流:理解布局的第一课
HTML 元素按默认的显示方式,大致可以分为三类。理解它们的差异,是理解 CSS 布局的前提。
块级元素:独占一行,可设宽高
块级元素默认从新的一行开始,并且尽可能撑满父容器的宽度。width、height、margin、padding 都完全生效。常见的块级元素有 div、p、h1~h6、ul、ol、li、section、article、header、footer、blockquote、table、form。
行内元素:跟随文字流,宽高无效
行内元素不会打断文字流,它会和前后文字排在同一行。给它设置 width 和 height 是无效的,垂直方向的 margin 也不生效(水平方向的 margin 和 padding 生效,但 padding 会溢出到行框外而不撑开行高)。常见行内元素有 span、a、strong、em、code、label、abbr、cite。
行内块元素:混合特性
img、input、button、select、textarea 属于行内块:它们像行内元素一样排在同一行,但又能像块级元素一样设置宽高和内边距。
不过,img 有一个非常经典的“坑”:它默认是 vertical-align: baseline,也就是图片底边与文字基线对齐。由于基线下方还留有字母降部(如 g、p 的下半部分)的空间,图片下方会出现一条几像素的缝隙。解决方法有三种:
img { display: block; }
/* 方案二:对齐到顶部 */
img { vertical-align: top; }
/* 方案三:让父容器字号为 0(不推荐,会影响子元素) */
.box { font-size: 0; }
display 可以改变一切
需要强调的是:上面说的“块级”“行内”只是默认样式,不是元素的固有属性。你完全可以让一个 span 变成块级:
div { display: inline; }
a { display: flex; }
所以,选择标签时应该看语义,而不是看它默认的显示方式。需要换行不代表要用 div,用 CSS 把 span 改成 block 同样可以。
HTML 的嵌套规则
元素之间能不能嵌套,规范里写得很清楚。违反规则时浏览器会自动“纠正”,但纠正结果不一定符合预期:
| 规则 | 说明 | 典型错误 |
|---|---|---|
p 只能包含行内元素 |
段落里不能放块级元素 | <p><div>…</div></p> |
a 不能嵌套 a |
交互式内容不能互相嵌套 | 整张卡片是链接,里面又有链接 |
button 里不能放交互元素 |
按钮内不能再有链接或按钮 | <button><a>…</a></button> |
ul / ol 只能包含 li |
列表项必须直接是 li | <ul><div>…</div></ul> |
dl 只能包含 dt / dd |
描述列表有固定结构 | 往 dl 里塞 p 或 div |
table 有严格子元素 |
只能是 caption / colgroup / thead / tbody / tfoot / tr | 直接往 table 里写 td |
最典型的例子是 <p><div>内容</div></p>。浏览器解析到 div 时,会自动闭合前面的 p,最终变成两个兄弟节点:一个空的 p 和一个 div。你写的嵌套关系被悄悄改写了,而 CSS 选择器 p > div 自然也就匹配不到任何东西。
不要用 br 制造间距
<br> 的语义是“此处有一个换行”,用于诗歌、地址、歌词这类确实需要强制换行的地方。用它来制造段落之间的空隙是错的:
第一段<br><br>第二段屏幕阅读器读不出段落停顿,搜索引擎看不到段落划分,样式也无法单独控制段间距。
<p>第一段</p><p>第二段</p>间距交给 CSS 的
margin,结构与表现彻底分离。
五、文本组织:标题、段落、列表与引用
页面里占比最大的永远是文字。如何组织文字,决定了页面的可读性与机器可理解性。
标题:h1 到 h6
标题标签表达的是内容的层级,不是字号大小。一个页面通常只有一个 h1(页面主题),下面按 h2 → h3 → h4 逐级展开,不要跳级。
屏幕阅读器和部分浏览器插件都提供“按标题跳转”的功能。用户打开长页面,第一件事往往是拉出标题列表,快速判断有没有自己要找的内容。层级一乱,这份列表就失去了价值。
段落与换行
p 表示一个段落。HTML 源码里的换行和连续空格在渲染时会被合并成一个空格,这叫“空白折叠”。想要控制间距,只能靠 CSS,不能靠回车和空格。
<p>
这一行和下一行
会被合并成一行显示。
</p>
<!-- 需要强制换行时用 br,但要克制 -->
<p>北京市海淀区<br>中关村大街 1 号</p>
hr 表示“主题转换”,是一条语义上的分隔线,不是画线工具。如果你只是想要一条装饰线,用 CSS 的 border 更合适。wbr 则表示“可选的换行机会”,常用于长 URL 或长单词,告诉浏览器实在放不下时可以在这里断开。
列表:ul、ol、dl
列表是导航菜单、目录、步骤说明、术语表的标准结构。三种列表各有分工:
<ul>
<li>HTML</li>
<li>CSS</li>
</ul>
<!-- 有序列表:顺序有意义,支持 start / reversed -->
<ol start="3">
<li>第三步</li>
<li>第四步</li>
</ol>
<!-- 描述列表:术语与解释成对出现 -->
<dl>
<dt>DOM</dt>
<dd>文档对象模型,HTML 解析后形成的节点树。</dd>
<dt>Doctype</dt>
<dd>位于文档首行,用于触发浏览器的标准模式。</dd>
</dl>
dl 是被严重低估的标签。FAQ 页面、参数表、术语表、键值对展示,用 dl 都比 div + span 更贴切。屏幕阅读器会朗读出“定义列表,包含 N 个项目”,帮助用户建立预期。
另外,ol 支持 reversed 属性,可以倒序编号。倒计时、排行榜(从高到低)、历史事件回顾等场景用得上。
引用:blockquote、q、cite
<p>结构先于表现,语义先于样式。</p>
<footer>—— <cite>某位前端工程师</cite></footer>
</blockquote>
<!-- 短引用用 q,浏览器自动加引号 -->
<p>他常说 <q>先写结构</q>。</p>
强调、代码与其它文本标签
文本级标签数量不少,但常用的就那么十几个。下面这张表可以当作速查:
| 标签 | 含义 | 使用场景 |
|---|---|---|
strong |
重要性、严重性 | 警告、必须注意的信息 |
b |
视觉加粗,无语义 | 产品名、关键词,仅需视觉突出 |
em |
重音强调,改变句意 | “是我说的”这类需要重读的词 |
i |
外语词、术语、心语 | 拉丁学名、外来词 |
mark |
与当前上下文相关的高亮 | 搜索命中词 |
code |
代码片段 | 行内变量名、函数名 |
pre |
保留空白格式 | 代码块、ASCII 图,常与 code 嵌套 |
kbd |
用户键盘输入 | 快捷键说明 |
samp |
程序输出 | 命令行结果 |
var |
变量或占位符 | 数学变量、模板占位符 |
abbr |
缩写,title 提供全称 | CSS、HTML、API |
time |
机器可读的时间 | 发布时间、事件日期 |
del / ins |
修订中的删除与插入 | 价格调整、条款修订 |
s |
内容已不再准确 | 原价划掉 |
small |
附属细则 | 版权、法律声明 |
sub / sup |
下标 / 上标 | H2O、x2 |
address |
联系信息 | 作者邮箱、电话,不是任意地址 |
wbr |
可选换行点 | 长 URL、长单词 |
其中 time 的性价比最高。它的 datetime 属性让机器可以精确解析时间,而页面显示可以是任意人读格式:
<time datetime="2026-09-19T14:30">今天下午两点半</time>
<time datetime="PT25M">二十五分钟</time>
搜索引擎会利用它判断内容时效性,社交平台抓取摘要时也会优先读取它。这是少数“写一行代码就能带来实际收益”的标签。
六、超链接与路径:Web 的血管
没有链接就没有 Web。a 元素看似简单,但 href 的写法、路径的解析规则、target 与 rel 的配合,处处都是细节。
href 的五种常见形式
| 形式 | 示例 | 说明 |
|---|---|---|
| 绝对 URL | https://example.com/a |
包含协议和域名,跳转到外部站点 |
| 根相对路径 | /css/main.css |
从域名根目录开始,与当前页面位置无关 |
| 文档相对路径 | ./about.html 或 ../img/a.png |
相对于当前文档所在目录,.. 表示上一级 |
| 片段标识符 | #section-2 |
跳转到本文档中 id 为 section-2 的元素 |
| 协议处理器 | mailto: tel: sms: |
唤起邮件、拨号、短信等系统应用 |
相对路径的解析规则值得单独记一下:不带斜杠开头、也不带协议的路径,一律相对于当前文档的 URL。假设当前页面是 https://example.com/blog/post/index.html:
<img src="a.png">
<!-- 解析为 https://example.com/blog/img/a.png -->
<img src="../img/a.png">
<!-- 解析为 https://example.com/img/a.png -->
<img src="/img/a.png">
注意最后一种:以 / 开头的路径与当前页面位置无关,永远从域名根开始。这也是为什么很多项目推荐使用根相对路径——页面无论放在哪一层目录,引用都不会出错。当然,前提是站点部署在域名根目录下;如果部署在子目录(如 /blog/),根相对路径就会指向错误位置,此时可以用 <base> 标签调整基准 URL。
target 与 rel
target="_blank" 会在新标签页打开链接。看起来很友好,但有一个必须注意的安全问题:新页面可以通过 window.opener 访问原页面的 window 对象,甚至把原页面重定向到钓鱼站点。解决办法是同时加上 rel:
现代浏览器已经默认给 target="_blank" 加上了 noopener 行为,但显式写上更稳妥,也更能表达意图。rel 的其它常用取值:
nofollow:告诉搜索引擎不要追踪这个链接,也不要把权重传递过去。sponsored:标明这是付费或赞助链接。ugc:标明这是用户生成内容里的链接(评论区、论坛)。noopener:切断新页面与原页面的window.opener引用。noreferrer:不发送 Referer 请求头,同时也包含 noopener 的效果。
下载与锚点
download 属性可以让浏览器下载而不是导航到目标资源,属性值可以指定保存的文件名:
注意 download 只对同源资源生效。跨域链接加上它会被浏览器忽略,因为跨域下载存在安全风险。
锚点跳转是另一个高频场景。点击 <a href="#main"> 时,浏览器会找到 id="main" 的元素并滚动过去。这也是“跳到主内容”链接的实现方式。
链接文本要能独立表意
屏幕阅读器用户可以拉出页面上的所有链接,形成一个列表快速浏览。如果列表里全是“点击这里”“了解更多”“阅读全文”,这个功能就完全失效了。正确做法是让链接文本本身携带信息:
<a href="/a">点击这里</a>脱离上下文后完全不知道会跳到哪里。
<a href="/a">查看 HTML 基础教程</a>单独读出来依然表意清晰。
如果设计上确实只能显示“了解更多”,可以在链接里加一个视觉隐藏的补充文本:
了解更多
<span class="visually-hidden">关于 HTML 文档结构</span>
</a>
七、图片与媒体:img 的属性详解
img 是一个很特殊的元素:它是行内元素,却可以设置宽高;它是自闭合标签,却有一个必需的“替代内容”。
src 与 alt
src 指定图片地址,alt 提供替代文本。关于 alt,有三条明确的规则:
- 有含义的图片:写出图片传达的信息,而不是描述图片长什么样。写“2026年第三季度访问量增长 40%”比“一张折线图”有用得多。
- 纯装饰的图片:写
alt=""(空字符串),屏幕阅读器会直接跳过。 - 千万不要省略 alt 属性:省略时,部分屏幕阅读器会朗读文件名,比如“IMG underscore 2026 underscore zero seven dot jpg”,体验极差。
还有一个常见场景:图片本身是链接。此时 alt 会成为链接的可访问名称,更应该写清楚:
<img src="logo.svg" alt="前端开发技巧首页">
</a>
width 与 height:不只是尺寸
给 img 写上 width 和 height 属性,除了指定尺寸,还有一个更重要的作用:让浏览器提前算出图片的宽高比,预留出空间。
没有这两个属性时,图片加载完成前占位高度是 0,加载完成后突然撑开,把下方内容猛地推下去——这就是累积布局偏移(CLS),Core Web Vitals 的三大指标之一。写了宽高,浏览器就能按比例预留空间,加载完成只是填充,不会引起跳动。
需要注意的是,如果同时用 CSS 设置了 width: 100%,要让高度自适应,应该配合:
width: 100%;
height: auto;
}
loading 与 decoding
loading="lazy" 让浏览器延迟加载视口外的图片,能显著减少首屏的网络请求和内存占用。但要注意:
- 首屏图片不要用 lazy:会延迟首屏渲染,反而拖慢 LCP。
- 不要给所有图片都加 lazy:少量图片时,懒加载的收益可能还抵不上它自身的开销。
decoding="async"告诉浏览器可以异步解码图片,避免解码阻塞主线程;decoding="sync"则用于必须立刻显示的关键图片。
srcset 与 sizes
同一张图在不同屏幕上需要的分辨率不同。手机不需要下载 2000px 宽的图,桌面 4K 屏则需要更清晰的版本。srcset 让浏览器自己挑:
src="hero.jpg"
srcset="hero-400.jpg 400w,
hero-800.jpg 800w,
hero-1600.jpg 1600w"
sizes="(max-width: 600px) 100vw, 800px"
alt="团队成员在会议室讨论方案"
width="1600" height="900">
w 描述符表示图片的固有宽度,sizes 告诉浏览器“在不同视口下,这张图在页面上实际会占多宽”。浏览器结合设备像素比,从候选列表里挑一个最合适的下载。
picture:艺术指导
srcset 解决的是“同一构图不同分辨率”,picture 解决的是“不同条件用不同构图或格式”:
<source srcset="hero.avif" type="image/avif">
<source srcset="hero.webp" type="image/webp">
<source media="(max-width: 600px)" srcset="hero-mobile.jpg">
<img src="hero.jpg" alt="描述" width="1600" height="900">
</picture>
浏览器会从上到下匹配第一个满足条件的 source。注意最后那个 img 是必需的,它承担实际的渲染、alt 文本与尺寸占位,也是所有 source 都不匹配时的兜底。
figure 与 figcaption
正文中的插图、图表、代码清单常常需要一段说明文字。figure 与 figcaption 正是为此而生:
<img src="chart.png" alt="访问量趋势图" width="800" height="400">
<figcaption>图 1:访问量在第三季度出现明显增长</figcaption>
</figure>
figure 里不一定是图片,代码块、引用、公式都可以。figcaption 必须是 figure 的第一个或最后一个子元素。
视频与音频
<source src="intro.webm" type="video/webm">
<source src="intro.mp4" type="video/mp4">
<track kind="captions" src="zh.vtt" srclang="zh" label="中文" default>
您的浏览器不支持视频播放,<a href="intro.mp4">点击下载</a>。
</video>
三个要点:controls 提供原生的无障碍播放控件,不要自己用 div 造;poster 指定封面图,避免出现黑屏;track 提供字幕,只要视频里有人说话就应该加上。
iframe 必须写 title 属性,否则屏幕阅读器只能读成“框架”,用户不知道里面是什么。嵌入第三方内容时,配合 loading="lazy" 与 sandbox 使用更安全。
八、表格:数据结构的正确表达
“不要用 table 布局”这句口号流行了十几年,结果让不少开发者走向另一个极端:连数据表格也用 div 拼。这是错的——数据表格用 div 搭建后,屏幕阅读器无法朗读“第 3 行第 2 列”,用户彻底失去行列关系。
正确的做法是:布局用 CSS,数据用 table。
一个完整的表格结构
<caption>2026 年第二季度各浏览器市场份额</caption>
<thead>
<tr>
<th scope="col">浏览器</th>
<th scope="col">份额</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Chrome</th>
<td>65.4%</td>
</tr>
<tr>
<th scope="row">Safari</th>
<td>18.2%</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="2">数据来源:第三方统计机构</td>
</tr>
</tfoot>
</table>
逐项说明:
caption:表格标题。屏幕阅读器进入表格时会优先朗读它,用户立刻知道这张表在讲什么。它必须紧跟table开始标签。thead/tbody/tfoot:区分表头、主体、表尾。长表格滚动或分页打印时,表头可以重复出现。th+scope:明确表头的作用范围。scope="col"表示这一列的表头,scope="row"表示这一行的表头。屏幕阅读器读到某个单元格时,会自动播报它的行表头和列表头。colspan/rowspan:跨列与跨行合并。合并会打乱行列对应关系,复杂表格应改用id+headers建立显式关联。
复杂表格的 headers 关联
当表头有多层合并时,scope 已经不够用了。此时给每个表头 th 一个 id,再在 td 上用 headers 列出所有相关的表头 id:
<th id="revenue">营收</th>
...
<td headers="q3 revenue">1,280 万</td>
这样屏幕阅读器读到这个单元格时,会播报“第三季度,营收,1280 万”,而不是孤零零的一个数字。
什么时候不该用表格
如果一段内容只是“看起来像表格”,而本质上是布局(比如左侧头像右侧文字的卡片),那就不要用 table。判断标准很简单:这份内容有没有“行”和“列”的对应关系?有就用表格,没有就用 CSS 布局。
九、表单基础:交互最密集的地方
表单是用户与页面交互最密集的地方,也是 HTML 基础不牢最容易暴露的地方。一个语义正确的表单,能自动获得:点击标签聚焦输入框、屏幕阅读器正确朗读、浏览器自动填充、移动端弹出合适的键盘类型。
<fieldset>
<legend>订阅资讯</legend>
<label for="email">邮箱地址</label>
<input
type="email"
id="email"
name="email"
autocomplete="email"
required>
<button type="submit">订阅</button>
</fieldset>
</form>
label 是表单的地基
label 有两种关联方式:
<label for="name">姓名</label>
<input id="name" type="text">
<!-- 方式二:包裹式,隐式关联 -->
<label>
姓名 <input type="text">
</label>
显式关联更灵活,样式更好控制,也便于把标签和控件分开布局,是推荐做法。有了 label,点击文字就能聚焦输入框,这在移动端和触摸屏上尤其重要——小小的输入框很难点准。
placeholder 不能替代 label。它在用户开始输入后就消失了,用户无法回头确认这个字段要填什么;屏幕阅读器对它的支持也参差不齐。正确做法是始终提供可见的 label,placeholder 只作为格式示例。
input 的 type 决定一切
HTML5 新增了十几种 type,每一种都有自己的校验规则和移动端键盘:
| type | 移动端键盘 | 内置校验 |
|---|---|---|
email |
带 @ 的邮箱键盘 | 格式校验 |
tel |
数字拨号键盘 | 无(格式因地区而异) |
url |
带 .com 的键盘 | URL 格式校验 |
number |
数字键盘 | 配合 min / max / step |
date / time |
系统日期时间选择器 | 范围校验 |
search |
带搜索键 | 无 |
password |
普通键盘 | 无 |
checkbox / radio |
— | 配合 required 分组 |
file |
系统文件选择器 | 配合 accept 限制类型 |
单选按钮有一个关键细节:同一组的 radio 必须共享同一个 name,否则它们不会互斥,用户能同时选中多个。
button 的 type 陷阱
这是最容易被忽略、也最容易出问题的地方:在 form 内部,不写 type 的 button 默认是 submit。也就是说,一个看起来只是“打开弹窗”的按钮,点击后会把整个表单提交掉,页面刷新,用户填的内容全没了。
<button>查看详情</button>在表单里等同于提交按钮,点击即触发提交。
<button type="button">查看详情</button>显式声明类型,避免意外提交。
原生校验属性
不写一行 JavaScript,就能获得基础的表单校验:
required:必填。为空时浏览器会阻止提交并提示。minlength/maxlength:字符数限制。min/max/step:数值或日期范围。pattern:正则表达式校验(注意是隐式全匹配)。autocomplete:告诉浏览器和密码管理器这个字段是什么,取值有标准枚举,如email、tel、name、street-address、one-time-code。inputmode:只影响移动端键盘类型,不做校验。比如inputmode="numeric"用于身份证号、验证码。
原生校验的提示文案由浏览器提供,无法完全自定义样式。如果需要定制化提示,可以用 JavaScript 的 Constraint Validation API,或者用 novalidate 关闭原生校验后自行实现。但无论如何,服务端校验永远不能省——前端校验只是体验优化,不是安全措施。
十、全局属性:每个标签都能用的那些
有一批属性可以出现在任何元素上,它们构成了 HTML 的“通用能力层”。
| 属性 | 作用 | 使用建议 |
|---|---|---|
id |
文档内唯一标识 | 用于锚点、label 关联、JS 获取。全页不可重复 |
class |
可复用的分类名 | 用于样式与批量选择,可同时写多个 |
style |
行内样式 | 仅用于动态计算值或临时调试,不要用来组织样式 |
title |
补充说明(悬停提示) | 触屏与键盘不可靠,不要用它传关键信息 |
hidden |
隐藏元素 | 等价于 display:none,辅助技术也会忽略 |
lang |
语言标记 | 局部切换语言时使用 |
dir |
文字方向 | ltr / rtl / auto,多语言站点必需 |
tabindex |
键盘焦点顺序 | 用 -1 让元素可编程聚焦;正数请慎用 |
contenteditable |
内容可编辑 | 富文本编辑器的基础能力 |
data-* |
自定义数据 | JS 与 HTML 之间传值,通过 dataset 读取 |
aria-* |
无障碍语义补充 | 仅在原生语义不足时使用 |
id 与 class 的分工
初学者最容易混淆这两个属性。一句话区分:
- id 是身份证:全文档唯一,用于精确定位某一个元素。
- class 是标签分类:可以重复,一个元素可以有多个,用于批量应用样式。
样式优先用 class,因为 id 选择器的权重极高,一旦用它写样式,后续想覆盖就得用更长的选择器或 !important,维护成本很高。id 更适合承担“锚点”“label 关联”“JS 精确获取”这些职责。
data-* 的用法
data-* 属性让你可以在 HTML 上挂载任意自定义数据,而不用污染 class 名:
// JS 中通过 dataset 读取,连字符会转成驼峰
const btn = document.querySelector('[data-action="delete"]');
console.log(btn.dataset.id); // "1024"
// data-user-name 会变成 dataset.userName
命名规则:data- 后面不能有大写字母,多个单词用连字符分隔。这是规范要求,也是浏览器行为决定的(HTML 属性名不区分大小写,所以必须用连字符)。
tabindex 的正确用法
tabindex 有三个取值区间,含义完全不同:
tabindex="0":元素加入自然 Tab 顺序,按文档位置排序。用于让本来不可聚焦的元素(如div)可以获得焦点。tabindex="-1":元素不能通过 Tab 到达,但可以通过element.focus()编程聚焦。用于模态框、跳转目标容器。tabindex="1"及更大的正数:强烈不建议使用。它会打乱自然顺序,让键盘导航变得极其混乱,维护者很难追踪。
记住一条原则:能用原生可聚焦元素(a、button、input)就不要用 tabindex 改造别的元素。
十一、从字节到 DOM:浏览器如何解析你的 HTML
理解解析过程,才能理解很多“为什么”。比如为什么 script 会阻塞渲染,为什么编码声明必须靠前,为什么浏览器能容忍残缺的 HTML。点击下面的每个阶段查看细节。
Content-Type 判断编码。meta charset 必须靠前的原因。解析器的容错能力
HTML 解析器被设计成“永不报错”。规范里用大量篇幅定义了遇到各种错误时应该如何恢复。几个典型例子:
- 未闭合的标签:
<div><p>文字会被自动补上</p></div>。 - 嵌套错误:
<p><div>…</div></p>会被拆成兄弟节点。 - 表格结构缺失:往
table里直接写文本,解析器会自动创建tbody和tr来包裹。 - 重复的属性:同名属性只保留第一个,后面的被忽略。
- 未知的标签:会被当作普通元素处理,样式可以用 CSS 选择器命中(这正是 HTML5 语义标签能在旧浏览器里工作的原理)。
容错能力是 Web 生态能繁荣的重要原因——一份 1998 年写的页面,今天打开依然能看。但容错不等于你可以随便写:解析器补出来的结构,和你想的结构往往不同,而调试这种“结构错位”极其耗时。
阻塞渲染的两件事
解析过程中有两个环节会打断流程:
<link rel="stylesheet" href="main.css">
<!-- 2. 同步 script 阻塞解析 -->
<script src="app.js"></script>
<!-- 优化:异步 + 延迟执行 -->
<script src="app.js" defer></script>
所以最佳实践很明确:CSS 放 head 里(尽早开始下载,且不阻塞解析),脚本加 defer 放 head 里或放 body 末尾。
DOMContentLoaded 与 load
两个事件经常被混淆:
DOMContentLoaded:HTML 解析完成、DOM 树构建完毕时触发。此时图片、样式表、子框架可能还没加载完。绝大多数脚本应该监听它。load:页面上所有资源(图片、样式、脚本、字体)全部加载完成时触发。适用于依赖图片尺寸的场景。
// DOM 已就绪,可以安全地操作元素
});
window.addEventListener('load', () => {
// 所有资源加载完毕
});
十二、校验与调试:让工具帮你发现问题
HTML 的错误往往不会立刻暴露——页面能显示,只是显示得不太对。但错误会累积,最终以“样式怎么调都不生效”“JS 获取不到元素”的形式爆发。养成校验的习惯,能省下大量排查时间。
W3C Markup Validation Service
把页面 URL 或 HTML 源码贴到 validator.w3.org,它会逐行指出不符合规范的地方:标签未闭合、属性重复、嵌套非法、id 重复、alt 缺失等。这是最权威、最便宜的检查手段。
DevTools 的 Elements 面板
浏览器开发者工具的 Elements 面板显示的是解析后的 DOM,而不是你的源码。这非常有用:如果面板里的结构和源码不一样,说明你的 HTML 触发了容错机制。
// 1. 找出重复的 id
const ids = [...document.querySelectorAll('[id]')].map(el => el.id);
const dup = ids.filter((id, i) => ids.indexOf(id) !== i);
console.log('重复 id:', [...new Set(dup)]);
// 2. 找出没有 alt 的图片
[...document.images]
.filter(img => !img.hasAttribute('alt'))
.forEach(img => console.log(img.src));
// 3. 找出没有标签的表单控件
[...document.querySelectorAll('input, select, textarea')]
.filter(el => !el.labels || el.labels.length === 0)
.forEach(el => console.log(el));
Accessibility 面板
DevTools 的 Elements 面板右侧有一个 Accessibility 标签页,它会显示浏览器为当前元素计算出的“角色”和“可访问名称”。这是验证语义是否正确的直接方式:
- 选中一个
button,看 Role 是否为button,Name 是否合理。 - 选中一个
div加onclick的元素,你会发现它的 Role 依然是generic——屏幕阅读器不会把它当作按钮。 - 选中
nav,Role 会显示navigation,说明它被识别为地标。
五个高频错误的快速排查
十三、重构对比:从 div 汤到结构化文档
下面是一段典型的“div 汤”页面骨架。它用 class 名字描述了所有语义,但在 HTML 层面什么都没说。点击按钮对比改造前后的结构。
<div class="top">
<div class="logo">前端开发技巧</div>
<div class="menu">
<div class="menu-item"><a href="/">首页</a></div>
<div class="menu-item"><a href="/html">HTML</a></div>
</div>
</div>
<div class="content">
<div class="post">
<div class="post-title">HTML 基础与文档结构</div>
<div class="post-date">2026-09-19</div>
<div class="post-body">正文内容……</div>
</div>
<div class="side">作者简介</div>
</div>
<div class="bottom">© 2026</div>
</div>
改造带来的六个具体收益
- 屏幕阅读器:可以按地标跳转,一键直达 main 内容;导航被朗读为“导航,包含 2 个项目”。
- 阅读模式:浏览器的 Reader Mode 能准确提取正文,不再把侧边栏和页脚一起抓进去。
- 搜索引擎:正文边界清晰,摘要提取更准确;
time提供了可靠的发布时间。 - 可维护性:新人接手时,看标签就知道结构,不必逐个查 class 的用途。
- 体积:大量
class="post-title"之类的重复命名可以删掉,HTML 更短。 - 健壮性:即使 CSS 完全加载失败,页面依然保持正确的文档层级,可读性远高于纯 div 版本。
唯一需要注意的是:重构标签名会改变 CSS 选择器。class 大量依赖标签名的项目,需要同步调整样式。推荐的做法是保留原有 class 作为样式钩子,只把标签换成语义元素——这样 CSS 完全不用改,收益却立刻到手。
十四、HTML 基础与文档结构检查清单
把全文的结论浓缩成一份可以贴在工位上的清单。每次提交页面前扫一眼,能挡掉绝大多数问题。
- 文档首行是 DOCTYPE,且它之前没有任何内容(包括注释)。
html上有lang属性,值为zh-CN或对应语言。meta charset位于 head 最前面,且与服务端响应头一致。- 有
viewportmeta,且没有禁止用户缩放。 - 每个页面有唯一的
title,格式为“具体内容 + 站点名”。 - 有
description,写成自然语句而非关键词堆砌。 - 脚本使用 defer 或 async,没有阻塞解析的同步脚本。
- 页面只有一个
main,且没有被article/aside/nav包裹。 - 地标完整:页面有明确的
header、nav、main、footer区域。 - 标题层级不跳级:h1 → h2 → h3 依次递进,全页只有一个 h1。
- 没有把块级元素放进
p,没有a嵌套a。 - 列表只用
ul/ol/dl,其直接子元素符合规范。 - 图片有
alt,装饰性图片写alt=""。 - 图片有
width和height,避免布局偏移。 - 首屏以外的图片加
loading="lazy",首屏图片不加。 - 时间用
time+datetime,机器可读。 - 数据表格用
table,带caption与th scope。 - 表单控件有
label,for与id一一对应。 - 非提交按钮都写了
type="button",避免意外提交。 - 没有重复的
id,样式尽量不用 id 选择器。 - 不用
<br>制造间距,段落用p,间距交给 CSS。 - 不用
title传递关键信息,触屏与键盘用户拿不到。 - 外链
target="_blank"时带rel="noopener"。 - 只按 Tab 键走一遍页面,确认所有交互都能完成且焦点可见。
- 用 W3C 校验器跑一遍,把错误清空。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>页面标题 | 站点名</title>
<meta name="description" content="页面描述">
<link rel="stylesheet" href="css/main.css">
<script src="js/app.js" defer></script>
</head>
<body>
<a href="#main" class="skip-link">跳到主内容</a>
<header>
<a href="/" class="logo">站点名</a>
<nav aria-label="主导航">
<ul>……</ul>
</nav>
</header>
<main id="main">
<article>
<header>
<h1>文章标题</h1>
<p>
<time datetime="2026-09-19">2026年9月19日</time>
</p>
</header>
<section>
<h2>第一节</h2>
<p>正文……</p>
<figure>
<img src="a.png" alt="描述" width="800" height="400">
<figcaption>图 1:说明文字</figcaption>
</figure>
</section>
<footer>
<small>本文采用 CC BY 4.0 协议</small>
</footer>
</article>
<aside aria-label="相关内容">
<h2>相关阅读</h2>
</aside>
</main>
<footer>
<small>© 2026 前端开发技巧</small>
</footer>
</body>
</html>
HTML 基础之所以重要,是因为它处在整条前端链路的最上游。结构一旦写歪,后面的 CSS 和 JavaScript 都要为它买单。反过来,一份结构清晰、语义准确的 HTML,会让样式更容易写、脚本更容易找元素、屏幕阅读器读得更顺、搜索引擎理解得更准、半年后的你回顾起来也更轻松。
所以,下一次当你准备敲下 <div> 的时候,不妨先停半秒问自己一句:“这块内容,到底是什么?”——答案往往就是那个正确的标签。而当你写下 <!DOCTYPE html> 的那一刻,你也已经决定了这棵树会长成什么样子。