跳到正文
前端知识库
HTML

HTML 语义、可访问性与资深面试

深入文档解析、语义结构、表单、响应式媒体、焦点管理、SEO 与可访问性工程实践。

9 分钟HTML · 语义化 · 可访问性 · 面试

HTML 负责表达内容的结构和含义。标签选得准确,浏览器、搜索引擎和辅助技术才能正确理解页面。

最小文档结构

<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>页面标题</title>
    <meta name="description" content="页面内容的简短摘要" />
  </head>
  <body>
    <header>站点页头</header>
    <main>页面主要内容</main>
    <footer>站点页脚</footer>
  </body>
</html>
  • lang 帮助读屏软件选择正确的发音规则。
  • charset 应尽早声明,避免字符被错误解析。
  • viewport 让移动浏览器按设备宽度布局。
  • 每个页面应有唯一、能说明内容的 title
  • 一个页面通常只有一个承担主要内容的 main

用语义元素表达结构

优先根据内容含义选择元素,而不是根据默认样式选择元素:

  • header:页面或区块的介绍性内容。
  • nav:主要导航链接集合。
  • main:页面独有的主要内容。
  • article:可独立分发或复用的内容。
  • section:有明确主题、通常带标题的区块。
  • aside:与主要内容间接相关的补充信息。
  • footer:页面或区块的尾部信息。
<article>
  <header>
    <h1>浏览器渲染机制</h1>
    <p>从 HTML 到像素的完整过程。</p>
  </header>

  <section aria-labelledby="parse-title">
    <h2 id="parse-title">解析阶段</h2>
    <p>浏览器分别构建 DOM 与 CSSOM。</p>
  </section>
</article>

不要为了“看起来像按钮”而给 div 绑定点击事件。导航使用 a,触发动作使用 button,这样键盘行为和可访问语义天然正确。

标题层级

标题描述文档大纲,而不是字号:

  • h1 表示页面主题。
  • h2 表示主要章节。
  • h3 表示章节内的子主题。
  • 不要为了视觉效果从 h2 跳到 h5

需要不同字号时使用 CSS,不要改变语义层级。

链接与按钮

<!-- 导航到另一个地址 -->
<a href="/articles/html">阅读 HTML 文档</a>

<!-- 执行当前页面中的动作 -->
<button type="button">打开筛选器</button>

链接文本应能脱离上下文被理解,避免连续使用“点击这里”。新窗口会改变用户预期,除非有明确理由,否则不要添加 target="_blank"

图片与媒体

<figure>
  <img
    src="render-pipeline.webp"
    alt="浏览器从 DOM 和 CSSOM 构建渲染树的流程图"
    width="1280"
    height="720"
    loading="lazy"
  />
  <figcaption>浏览器渲染流水线</figcaption>
</figure>
  • 信息图片的 alt 描述其目的,而不是重复“图片”。
  • 纯装饰图片使用空文本 alt=""
  • 声明宽高可以提前保留空间,减少布局偏移。
  • 首屏关键图片不应懒加载;非首屏图片可使用 loading="lazy"

可访问表单

每个输入控件都需要可感知的名称。占位符不能替代标签,因为输入后它会消失。

<form>
  <div>
    <label for="email">邮箱</label>
    <input
      id="email"
      name="email"
      type="email"
      autocomplete="email"
      aria-describedby="email-help"
      required
    />
    <p id="email-help">用于接收登录链接。</p>
  </div>

  <button type="submit">发送登录链接</button>
</form>

优先使用原生输入类型,例如 emailurlnumberdate。浏览器会提供移动端键盘、基础验证和辅助技术支持。

列表与数据表格

同类并列内容使用 ul,有先后步骤使用 ol。真正的二维数据才使用 table,不要用表格做页面布局。

<table>
  <caption>构建脚本执行时间</caption>
  <thead>
    <tr>
      <th scope="col">脚本</th>
      <th scope="col">耗时</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th scope="row">类型检查</th>
      <td>4.2 秒</td>
    </tr>
  </tbody>
</table>

常见误区

  • 用大量无语义 div 代替原生控件。
  • 为图片省略 alt、宽度和高度。
  • 用占位符代替表单标签。
  • 点击区域只能使用鼠标,键盘无法聚焦和触发。
  • 重复使用同一个 id
  • 只根据颜色传达错误、成功等状态。

HTML 解析与 DOM 构建

HTML 解析器具有容错能力,会对不合法嵌套执行错误恢复。因此“浏览器能显示”不代表 DOM 与源码一致:

<p>第一段<div>块级内容</div>第二段</p>

解析器会在 div 前隐式关闭 p,后续 DOM 结构可能出乎预期。表格、段落和交互元素的嵌套尤其需要遵循内容模型。应通过 Elements 面板和 HTML 校验器检查最终 DOM,而不只看模板源码。

解析主流程可概括为 tokenizer 读取字符并产生 token,tree builder 根据插入模式构建 DOM。预加载扫描器会尝试提前发现样式、脚本和图片等资源,减少严格串行的网络等待,但动态插入或隐藏在脚本中的 URL 难以被提前发现。

脚本加载方式直接影响解析:

  • 普通经典脚本会等待下载并阻塞解析与执行。
  • defer 脚本并行下载,在文档解析完成后按顺序执行,早于 DOMContentLoaded
  • async 脚本下载完成即执行,不保证相互顺序,适合独立脚本。
  • Module 脚本默认类似 defer,并遵循模块依赖图;async 仍会改变其等待策略。

可访问性树与名称计算

浏览器根据 DOM、CSS 和 ARIA 构建可访问性树,辅助技术读取的是这棵语义树,而不是视觉像素。元素是否可访问通常要检查四件事:角色、名称、状态和值。

可访问名称可能来自元素文本、关联的 labelaria-labelaria-labelledby。不要重复提供相互冲突的名称:

<h2 id="dialog-title">删除项目</h2>
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
  <!-- 对话框内容 -->
</div>

ARIA 的首要原则是:能使用原生元素,就不要重新实现等价角色。<button> 自带键盘触发、禁用语义和焦点行为;<div role="button"> 仍需自行补全 tabindex、Enter/Space 处理和禁用状态。

隐藏方式也有不同语义:

  • display: nonehidden 通常同时从布局和可访问性树移除。
  • visibility: hidden 保留布局空间,但通常不可交互也不会被辅助技术读取。
  • aria-hidden="true" 只影响可访问性树,不能用于仍可聚焦的内容。
  • 视觉隐藏类可以让文本只对读屏可用,但要避免使用 display: none

键盘与焦点管理

键盘可达不等于体验完整。还应保证焦点顺序符合阅读顺序、焦点指示清晰,并在界面结构改变时把焦点移动到合理位置。

模态对话框需要:打开后将焦点移入;Tab 焦点留在对话框内;Escape 可关闭;背景内容不可交互;关闭后焦点回到触发元素。优先使用原生 dialog,同时验证目标浏览器中的行为:

<button type="button" id="open-dialog">删除项目</button>

<dialog id="confirm-dialog" aria-labelledby="confirm-title">
  <h2 id="confirm-title">确认删除</h2>
  <p>此操作无法在当前页面撤销。</p>
  <form method="dialog">
    <button value="cancel">取消</button>
    <button value="confirm">删除</button>
  </form>
</dialog>

不要用正数 tabindex 人工重排焦点。它很快会与 DOM 顺序失去一致。tabindex="0" 让自定义控件进入自然顺序,tabindex="-1" 允许脚本聚焦但不加入 Tab 顺序。

表单提交、校验与错误恢复

原生表单不仅是输入布局,还定义了提交协议。只有具备 name 且未禁用的成功控件会进入 FormData。按钮在表单中默认可能是提交按钮,因此非提交动作必须写 type="button"

form.addEventListener('submit', async (event) => {
  event.preventDefault()

  if (!form.reportValidity()) return

  const data = new FormData(form)
  await saveProfile(Object.fromEntries(data))
})

客户端约束验证改善即时反馈,但不能替代服务端验证。错误体验应满足:

  • 错误与具体控件通过 aria-describedbyaria-errormessage 建立关联。
  • 控件暴露 aria-invalid="true",文本说明如何修复,而不只写“格式错误”。
  • 提交失败后聚焦错误摘要或第一个无效字段。
  • 用户已填写的合法内容不应因失败而丢失。
  • 异步校验处理请求竞态,不把旧结果应用到新输入。

disabled 控件不可聚焦且不会提交;readonly 控件通常可聚焦并会提交。只是暂时禁止修改但仍需传值时,两者不能混用。

响应式图片与资源优先级

srcsetsizes 让浏览器根据视口、设备像素比和布局宽度选择资源。sizes 描述的是图片将占据的 CSS 宽度,不是原图宽度:

<picture>
  <source
    type="image/avif"
    srcset="hero-640.avif 640w, hero-1280.avif 1280w"
  />
  <source
    type="image/webp"
    srcset="hero-640.webp 640w, hero-1280.webp 1280w"
  />
  <img
    src="hero-1280.jpg"
    srcset="hero-640.jpg 640w, hero-1280.jpg 1280w"
    sizes="(min-width: 64rem) 60rem, 100vw"
    width="1280"
    height="720"
    alt="性能面板中的页面加载瀑布图"
    fetchpriority="high"
  />
</picture>

格式切换使用 picture/source,分辨率选择使用 srcset/sizes。首屏 LCP 图片通常不懒加载,可在确实较晚发现时预加载;不要给大量资源都设置高优先级,否则优先级本身失去意义。

视频需要字幕轨道,自动播放应默认静音并允许暂停。纯音频内容还应提供文本稿,不能把关键信息只放在听觉通道中。

页面元数据、SEO 与分享

SEO 的基础仍是可抓取链接、准确标题、清晰正文和稳定响应。常见元数据包括:

<link rel="canonical" href="https://example.com/articles/rendering" />
<meta name="robots" content="index,follow" />
<meta property="og:title" content="浏览器渲染机制" />
<meta property="og:description" content="从导航到像素的完整链路" />
<meta property="og:image" content="https://example.com/og/rendering.png" />

canonical 用于声明多个相似 URL 的首选版本,不是重定向。结构化数据应与页面可见内容一致,不能编造用户看不到的信息。客户端渲染站点还要验证爬虫能否获得完整正文、状态码和每页独立元数据。

内容安全与嵌入边界

HTML 层面的安全设计包括:

  • 不可信内容不得直接进入 innerHTML;必须渲染富文本时使用成熟清洗器和明确白名单。
  • 外链新窗口使用 rel="noopener";现代浏览器通常已隐式处理,但显式声明能表达边界。
  • iframe 使用最小化的 sandboxallow 权限,不同时随意开启 allow-scriptsallow-same-origin
  • 第三方脚本可配合 CSP、SRI 与来源白名单,但 SRI 只验证静态资源完整性,不能约束脚本运行权限。

安全不是补一个属性就结束;输入编码、输出转义、内容清洗与响应头需要共同形成边界。

资深面试追问

语义化带来什么实际价值?

它为浏览器和辅助技术提供稳定结构,减少自定义交互代码;让键盘、表单提交、验证等平台能力直接可用;改善抓取和可维护性。语义化不等于“多用几个新标签”,而是让元素的角色和行为与用户预期一致。(深入阅读:用语义元素表达结构

DOMContentLoadedload 有什么区别?

DOMContentLoaded 在 HTML 解析完成且延迟脚本执行后触发,不等待图片等大部分子资源;load 要等待页面依赖资源完成。实际应用通常不需要等 load 才初始化交互,模块脚本放在合理位置即可。(深入阅读:HTML 解析与 DOM 构建

如何评审一个自定义下拉框?

先确认原生 select 是否足够。若必须自定义,要明确采用 combobox、listbox 还是 menu 模式;检查可访问名称、展开状态、活动选项、方向键/Home/End/Escape 行为、输入法、焦点策略、禁用项、窗口化列表与读屏反馈。能点击只是最低标准。(深入阅读:键盘与焦点管理

如何制定可访问性验收标准?

自动检查用于发现缺少名称、对比度和错误属性等确定性问题;键盘走查验证核心流程;至少选择一种读屏软件检查名称、状态和动态反馈;再覆盖缩放、减少动画、高对比度和触摸目标。自动化不能证明页面完全可访问。(深入阅读:可访问性树与名称计算

页面检查

  • 关闭 CSS 后,内容顺序仍然合理。
  • 只用键盘可以完成核心流程,焦点清晰可见。
  • 标题层级连续,主要地标明确。
  • 表单标签、错误提示和帮助文本建立了正确关联。
  • 图片替代文本说明信息目的,装饰图不会被读屏重复朗读。
  • HTML 验证没有重复 ID、错误嵌套和未闭合元素。
  • 模态框、菜单和动态提示具备完整焦点与键盘策略。
  • 表单提交失败后保留输入,并能定位和解释具体错误。
  • 首屏媒体的格式、尺寸、优先级与真实布局匹配。