1. 别急着写代码:先说说我们为什么会把 div 用成习惯
做前端的,谁没被代码审查说过几句“div 套 div,套娃呢?”——我最早写页面的时候也是这样,脑海里只有三个标签:div、span、p,剩下的全靠 class 命名硬撑。结果页面结构看起来就像一盘散沙,自己隔两周再看都得靠注释回忆“这块是啥来着”,更别提团队协作时别人接手代码时的崩溃表情。
div 本身不是坏东西,它是一个无语义的块级容器,它的设计初衷是“当你实在找不到合适的语义标签时,用我兜底”。但现实是,我们把它当成了默认选项,本该用 header、nav、article、footer 表达的页面骨架,全被“div + class”替代了。这带来什么问题呢?最直接的是可读性差、可维护性低,更深层的则是 SEO 不友好、无障碍支持缺失。
HTML5 推出语义化标签体系已经很多年了,从 IE9 时代开始就有 polyfill 方案,到如今所有现代浏览器原生支持,已经没有任何技术理由拒绝它们。这篇文章我打算从使用场景出发,把HTML5 语义化标签的选用逻辑、实际写法、常见误区和排查思路一次讲透,让你看完之后能直接上手改造自己的项目,而不是停留在“知道有这么回事”的层面。
这篇文章适合谁看呢?如果你已经能熟练写 div 布局,但不太清楚 header、main、section、article 这些标签到底该怎么分配;或者你负责的项目结构混乱,想借着语义化重构的机会把代码质量提上去;又或者你写了不少页面但总被评审说“语义不对”——那这篇就是给你准备的。
2. 语义化的底层逻辑:为什么浏览器和搜索引擎需要“看得懂”你的页面
2.1 div 表达不了的东西,恰恰是最重要的信息
我们得先搞清楚一个概念:HTML 标签本身是带“含义”的。浏览器、搜索引擎爬虫、屏幕阅读器,它们读你的页面时,并不像人类看渲染效果那样“一眼就知道这块是导航,那块是正文”。它们靠的是标签名+属性来理解内容结构。
比如<div class="nav">和<nav>,渲染出来的视觉效果可能一模一样,但在机器眼里有天壤之别。前者就是“一个容器”,具体是什么内容得靠 class 名称去猜;后者则明确告诉程序:“这里面是导航区域,链接集合。”这就是语义化标签存在的底层价值——把视觉结构背后的逻辑结构显式表达出来。
想想搜索引擎的处境:它每天要爬取海量页面,为了判断一个页面的内容质量、主题相关性,它需要快速定位页面主内容在哪里、侧栏在哪里、导航有哪些、文章正文从哪个标签开始。如果你的页面全是裸 div,爬虫就必须靠算法猜测。猜错的结果是什么?轻则关键词权重分配偏差,重则页面摘要取错内容。这就是为什么业内一直强调“语义化是 SEO 的地基”。
2.2 无障碍:不是“加分项”,而是“必答题”
这块我想多说几句,因为很多人对无障碍(Accessibility,简称 a11y)的认识停留在“给图片加个 alt”的程度。实际上,对于依赖屏幕阅读器的视障用户来说,你的页面是一串线性朗读的文本流。他们需要通过不同的标签来构建心理模型:按 h1 跳转到标题、按 nav 跳过导航、按 article 直接读正文。如果这些结构全部由 div 构成,读屏软件的唯一策略就是把所有内容从头到尾念一遍——信息获取效率极低。
我记得某团队做过一次无障碍审查测试,拿了一个纯 div 布局的老项目,用读屏软件操作,结果是:用户想找页面主要内容,需要在几百个“容器容器容器”的朗读声中摸索十几秒。这不是夸大其词,div 在无障碍树里默认是不产生任何角色的,只有显式指定 ARIA 角色才能弥补,但绝大多数项目根本没做这一步。所以我在项目规范里通常写得很直白:语义化标签是无障碍支持的最低成本实现路径,比任何 ARIA 补丁都自然。
2.3 语义化标签的“信息量”比你想的大
来看一份简单的对比表,更直观地感受语义标签带来的额外信息:
| 写法 | 机器理解 | 维护者理解 |
|---|---|---|
<div class="header"> | 一个普通容器,语义未知 | 需要阅读 class 名称 |
<header> | 页面级或区块级的头部区域 | 直接知道是头部 |
<div class="nav"> | 普通容器,内部可能是任意内容 | 需要结合上下文 |
<nav> | 导航链接集合 | 直接知道是导航 |
<div class="footer"> | 普通容器,语义未知 | 需要阅读 class 名称 |
<footer> | 底部信息区(版权、联系方式等) | 直接知道是底部 |
表格想表达的核心是:class 名到底只是“给人看的备注”,而标签名是“给机器和人共同阅读的结构契约”。两者差着整整一个维度。
3. 语义化标签全景拆解:每个标签该用在哪、怎么用
3.1 页面骨架的“四大金刚”:header、footer、main、nav
先从最顶层的结构标签说起。
<header>代表的是“介绍性内容的容器”,它可以是页面级的,也可以是某个区块内部的。比如文章开头的一组导语、logo 区域、作者信息,都可以放 header。注意一个容易混淆的点:<header>并不等同于“页面顶部那条栏”——一个页面里可以有多个 header,每个 section 或 article 里都能有自己的 header。
<footer>同理,它表示“末尾区域”,里面通常放版权声明、联系方式、相关链接。HTML5 标准对 footer 的定位是“包含关于它所在区块的信息”,所以不是只有页面最底部的栏才能叫 footer,文章末尾的“标签列表、作者简介、上一篇下一篇”也适合用 footer。
<main>是一个页面里只能出现一次的元素,标识文档的主内容区域。它有一个很实际的作用:帮助屏幕阅读器用户直接跳到主体内容,绕过导航和侧栏。main 里面不能再嵌套另一个 main,这个限制要记清楚。
<nav>专门用来包裹“主要导航链接集合”。但别把页面里所有链接都塞进 nav——比如文章正文里的某个超链接,那是正文内容的一部分,不属于导航。判断标准简单粗暴:这组链接是否承担“在站点/模块内跳转”的核心功能?是的话用 nav,不是就别用。
3.2 内容区的两大主角:article 与 section
这两个标签的区分是整个语义化体系里最容易出错的地方。
先看定义:<article>表示“文档、页面、应用或站点中一个自包含的独立内容组合”。换句话说,article 里的内容应当是完整独立的,把它单独拿出来,它依然有明确意义。典型场景:一篇博客文章、一条论坛帖子、一个商品卡片、一条评论。
<section>表示“文档中一个独立的、通常包含标题的区块”。它的核心特征是“有主题”,通常搭配一个 h1-h6 子标题出现。section 的定位比 article 低一层,article 可以由多个 section 组成,section 里也可以包含 article。
我实际操作时判断这两个标签,会用一句话自测:“这段内容如果脱离当前页面,单独丢到另一个页面里,还能不能自圆其说?”能,就是 article;不能,只是当前页面的一个分区,就用 section。
举个例子,一个新闻列表页:
<main> <h1>科技新闻</h1> <article> <h2>AI 辅助编程工具的最新进展</h2> <p>正文内容...</p> </article> <article> <h2>移动端框架选型实践</h2> <p>正文内容...</p> </article> </main>列表里的每篇新闻都完整独立,是典型的 article 场景。而如果是一个“产品介绍页”,分为“产品功能”和“用户评价”两个模块,这两个模块拆出去单独看就不够独立,应该用 section。
3.3 细粒度语义:aside、figure、time、address
往下细拆,还有一批常用但容易被忽略的语义标签。
<aside>表示与主体内容“间接相关”的内容。侧边栏是典型用法,但它也可以用在 article 内部,比如一篇文章里的“补充阅读”“术语解释”。关键点是 aside 的内容是辅助性的,删掉它不影响正文的完整性。
<figure>配合<figcaption>用来包裹插图、代码块、图表这类“独立引用的内容”。它的价值在于:figure 里的图片、表格不是正文叙事的一部分,而是被引用的素材,figcaption 则给这个素材提供说明文字。
<time>可以给日期时间标记机器可读格式,比如<time datetime="2025-01-15">2025年1月15日</time>。搜索引擎和读屏软件能够根据 datetime 属性准确识别时间,而不是靠猜自然语言字符串。
<address>不是用来写“页面底部通讯地址”的——它表示“附近最近的 article 或 body 的 contact info”。所以文章末尾的“作者邮箱、作者主页”适合放 address,网站底部的公司地址其实用 footer 内部普通文本或 address 均可,但要理解它的语义是关联最近的 article 而不是全局。
3.4 标题体系:h1-h6 不是用来调字号的
这一节虽然不讲新标签,但它是语义化的骨架。很多人把 h1-h6 当“字号选择器”,觉得 h1 太大,就用 h3 配合 CSS 改大字号——这是大忌。
标题标签的本质是定义“文档大纲”(heading outline),就像一本书的目录。读屏用户可以在标题之间跳转,搜索引擎也用标题结构推断主题层级。一个页面的 h1 应当表达页面核心主题,h2 是章节,h3 是章节内的小节,层级要清晰并减少跳级。比如一个新闻详情页:
<h1>某地举办开源技术交流大会</h1> <h2>大会亮点回顾</h2> <h3>主题演讲环节</h3> <h3>圆桌论坛环节</h2> // 这里是错误示范,h3 之后不能跳回 h2 的层级逻辑里做子项标题层级一旦乱了,文档大纲就是一团乱麻。反过来说,如果你发现自己为了“看起来更小”而用了低级标题,正确的做法是:坚持使用匹配层级关系的标题,用 CSS 去控制视觉效果。记住这句话:HTML 负责结构和意义,CSS 负责长相和布局,两者别混着来。
4. 实操落地:从零把一个 div 页面改造成语义化结构
4.1 改造前:一个典型的“div 套娃”页面
假设我们现在手上有一个个人博客首页的静态原型,结构大概长这样:
<div class="page"> <div class="top-bar"> <div class="logo">我的博客</div> <div class="menu"> <a href="/">首页</a> <a href="/archive">归档</a> <a href="/about">关于</a> </div> </div> <div class="main-content"> <div class="post-list"> <div class="post"> <div class="post-title">第一篇文章标题</div> <div class="post-meta">发布于 2025-01-10</div> <div class="post-excerpt">摘要内容……</div> </div> </div> <div class="sidebar"> <div class="widget">热门标签</div> </div> </div> <div class="copyright">版权信息</div> </div>说实话,这个结构本身不算太烂,class 命名也算清晰,但你直接告诉机器“哪里是导航、哪里是主内容、哪里是侧栏”了吗?没有。机器看到的只有一层层无身份的 div。读屏用户不看 CSS 的话,根本分不清上下左右的结构关系。
4.2 改造后:一版可参考的语义化结构
来,我们把这个结构逐层替换成对应的语义化标签:
<body> <header> <div class="logo">我的博客</div> <nav aria-label="主导航"> <a href="/">首页</a> <a href="/archive">归档</a> <a href="/about">关于</a> </nav> </header> <main> <section aria-labelledby="latest-posts"> <h1 id="latest-posts">最新文章</h1> <article> <h2>第一篇文章标题</h2> <p> 发布时间:<time datetime="2025-01-10">2025年1月10日</time> </p> <p>摘要内容……</p> <a href="/posts/1">阅读全文</a> </article> <article> <h2>第二篇文章标题</h2> <!-- 结构同上 --> </article> </section> <aside aria-labelledby="sidebar-title"> <h2 id="sidebar-title">侧栏</h2> <ul> <li><a href="/tags/html">HTML</a></li> <li><a href="/tags/css">CSS</a></li> </ul> </aside> </main> <footer> <address>联系邮箱:dev@example.com</address> <p>© 2025 我的博客</p> </footer> </body>对照一下改动点:
- 顶部栏 → header:明确“这是页面头部区域”。
- 菜单 → nav:告诉机器这是一组导航链接。
- 内容主体 → main:标记主内容独一份区域。
- 文章列表区 → section + article 组合:列表本身是一个内容分区,每篇文章是一个独立内容单元。
- 文章标题 → h2(配合 h1 的“最新文章”形成两层大纲)。
- 发布时间 → time[datetime]:让时间是机器可读的。
- 侧栏 → aside:明确辅助性内容。
- 底部版权 → footer + address:底部信息语义化。
这么一套改下来,这个页面的“文档大纲”就清晰了:页面标题是“我的博客”,第一层章节是“最新文章”,每个文章标题是第二层章节,侧边栏是独立的辅助区块——机器可以像读目录一样理解你的页面。
4.3 头部、导航、面包屑:细节里的门道
再补充几个常见场景。
关于<nav>,一个页面不一定只有一个。站内主菜单一个、页脚链接集合一个,都是可以的。用aria-label区分它们的含义,比如<nav aria-label="主导航">和<nav aria-label="页脚导航">,读屏用户在两个导航之间跳转时就能分辨“这组链接是什么”。
面包屑导航要不要用 nav?用。而且更规范的写法是在 nav 里配合<ol>有序列表,因为面包屑本质是一个层级路径,有序列表能表达“位置顺序”。
页面头部还有个常见的细节:logo 和站点名称。logo 如果是一张图片,用<img>加alt="站点名称";如果想把整个站点名称做成链接,通常放在 header 里的<a>包裹的<span>或<p>里,不必额外套个 h1——除非你确实希望“站点名”出现在文档大纲的顶层。
5. 重构中的“语义选择判断器”:section 到底该不该出现
写代码时我经常被同事问:“这个模块到底应该用 section 还是 article?还是两个都不用?”后来我总结出一套简单的判断流程,分享给你:
- 这段内容是不是独立的完整单元?—是→
<article> - 否。那么这段内容是不是当前页面主题下的一个“有主题的分区”?—是→
<section> - 否。那么它是不是一个纯粹的样式容器(比如为了 flex 布局嵌套而加的包裹层)?—是→
<div>
这套流程能解决八成疑问。特别要提醒的是:如果你用 section 的唯一目的是给它加个 CSS 类名做布局,那这个 section 就是多余的,不如直接用 div。语义化不是“用越多语义标签越好”,而是“在合适的层级用合适的标签”。
说到这顺带纠正一个流传挺广的误读:“section 必须包含标题。”这个说法在 HTML5 之前的旧规范讨论里出现过,现行标准里 section 通常应包含标题,但你硬套也会出现“为了包含标题而编造一个标题”的尴尬场景。我的建议是:能配上标题就配,让大纲结构更清晰;配不上标题的时候,重新审视一下到底是不是该用 div。
再聊下“纯样式容器”这个概念。语义化改造不代表不能有 div——flex 布局的包裹层、网格的占位容器、给 JS 做 Hook 的节点,这些场景 div 依然是唯一合理选择。语义化的目标不是消灭 div,而是消灭“本不该用 div 却用了 div”的情况。
5.1 语义化的辅助道具:ARIA 什么时候该上
ARIA(Accessible Rich Internet Applications)是一套为无障碍提供额外语义的属性体系。当原生语义标签表达不了需要的含义时,ARIA 可以“补刀”。
比如一个自定义下拉菜单,光是 div+click 事件,读屏用户根本不知道这是菜单。这时候需要role="listbox"、aria-expanded等属性来补充。但有一个核心原则:能用原生标签解决语义的,先用原生标签,ARIA 是补救方案,不是常规方案。拿导航举例子,如果已经用了<nav>,就不需要再写role="navigation",原生标签自带该角色。
很多人写语义化重构时,第一反应是“把 div 全换成 section,同时加一堆 role”,这其实是把语义化和 ARIA 混为一谈了。语义化标签是“天生自带角色”,ARIA 是“后天强行赋予角色”。后者在复杂组件里是必需的,但如果你发现自己的页面到处都需要 ARIA 来补语义,往往说明基础标签选错了。
6. 我踩过的坑:几个典型误用案例与排查要点
6.1 把 header 和 nav 理解成了“页面顶栏”
我见过不少新手重构页面,把“页面顶部那条深色栏”整个换成 header,里边又套了一个 nav。看起来没问题,但如果这条顶部栏同时包含 logo、搜索框、登录按钮呢?这时候 header 是正确的总容器,nav 只负责包裹链接集合就好,不要把搜索框也塞进 nav——搜索框虽然有交互,但它不是“导航链接集合”。
真正的坑在另一面:有的页面根本没有传统的“顶部栏主导航”,而是侧边栏导航或者底部导航。nav 跟视觉位置无关,哪怕导航栏出现在视觉上的页面底部,照样可以用 nav 包裹。语义标签描述的是“内容性质”,不是“长得像什么”。
6.2 article 嵌套 article:什么时候合法,什么时候是滥用
article 是可以嵌套 article 的。典型场景是评论列表:每条评论是一个 article,整个评论区用 section 包裹,每条评论下还可以有回复,回复又是 article。这看起来叠了好几层,但在语义上是合理的:每条评论本身就是“一个独立的内容组合”。
但如果你只是把一个卡片风格的模块堆到另一个卡片风格模块里,互相之间没有“独立内容单元”的关系,那就不该嵌套。核心判断还是那句老话:独立拆出去是否有完整意义。
6.3 标题层级乱跳的排查
这个问题的隐蔽性在于,浏览器渲染时 h3 和 h2 的默认字号差异不明显,肉眼不太看得出来。但文档大纲一旦乱了,读者用“按标题跳转”功能时就会像迷失在迷宫一样。排查方法很简单:用浏览器的无障碍树查看器(比如 Chrome DevTools 的 Accessibility 面板),看看页面大纲层级是否连续。
常见的跳级场景:页面主标题是 h1,模块大标题却直接用了 h3,没有 h2 过渡;或者文章页把每篇文章的标题都写成了 h1。记住:一个文档只有一个合理的“根标题”h1(它可以是站点名也可以是文章标题,看场景),其余层级依次为 h2、h3……而不是每个模块都抢 h1。
6.4 验证语义化成果的工具清单
折腾完了,怎么确认自己的语义化改造是真的有效?我的常规验证动作是:
- 用 W3C Nu HTML 校验器跑一遍,看有没有标签嵌套错误。
- 用 Chrome DevTools 的 Accessibility 面板检查页面大纲。
- 用读屏软件实测一下(Windows 可以用开源的 NVDA,macOS 自带 VoiceOver),听一听页面结构是否清晰。
- 必要时用 Lighthouse 的无障碍审计项跑一次,虽然它不是语义化的最终裁判,但能发现一些明显的漏网之鱼。
这几个步骤做完,基本能保证你的页面结构在机器视角下是通的。
7. 再往深一层:SEO 视角下的语义化收益与边界
7.1 爬虫的“抓重点”能力提升
搜索引擎从你的页面结构里抽主要内容的逻辑虽然各家不公开,但有一点在业内是共识:清晰的内容层级和语义标记,能有效帮助爬虫识别主内容区域,减少噪声干扰。title、h1、h2 这几个要素在搜索引擎确定页面主题时权重相对更高。
举个例子,两个同样内容的页面,一个把文章标题放在 h1 里,另一个把它放在 div 里靠 CSS 撑成大字——在搜索引擎眼里,前者的主题信号明确得多。这是不是说只要用了语义化标签就能排名飙升?当然不是。语义化是“内容被正确理解的必要条件,而不是排名靠前的充分条件”。它是基础建设,不保证绝对收益,但没有它,其他 SEO 工作的落地效果会打折扣。
语义化标签跟结构化数据(如 JSON-LD)经常被混为一谈,这里澄清一下:结构化数据是在 HTML 之外的显式数据描述,而语义化标签是 HTML 内部自带的信息层级。两者可以配合使用,但职责不同。
7.2 维护效率:语义化是团队协作的“隐性文档”
抛开 SEO 和无障碍,我最看重语义化的实际理由之一是团队协作效率。想象你接手一个别人写的页面,如果结构是这样的:
<div class="wrap"> <div class="inner"> <div class="content"> <div class="post-body">你基本只能靠 CSS 选择器和文字内容反推每一层的作用。而如果结构是这样的:
<main> <section> <article> <p>正文</p> </article> </section> </main>哪怕样式类名再乱,你能第一时间看懂层级逻辑:这是主内容区里的一个分区,分区里有一篇独立文章。语义化标签就像代码注释,而且是“永远和内容同步更新、不会过期”的注释。这层收益在项目规模变大后尤其明显。
8. 多标签复用规则:hgroup、details、dialog 这些“进阶选手”值得用吗
前面聊的基础标签,是每个项目里最常出场的。HTML5 其实还有一些进阶语义标签,用得好能提升结构化程度,用不好就徒增复杂度。
<hgroup>用于组合标题和副标题,比如主标题“产品新版发布”,副标题“v2.0 重点变化一览”。但它的浏览器支持和文档大纲算法在历史上有些波折,现行标准里它被重新定义为“一组标题的组合”,实践中如果有“主标题+副标题”的场景,可以用它包起来。不过说实话,如果副标题本质是对主标题的说明,用普通<p>再加适当的样式,语义上也没有大问题。
<details>配合<summary>可以实现“手风琴/折叠面板”,而且不需要自己写 JS 状态管理。它的语义优势很明显:用户代理可以原生管理展开/收起状态,移动端体验更一致。但是要注意,details 的默认样式在各浏览器间略有差异,通常需要自己覆盖 reset。
<dialog>是另一个被低估的标签,原生支持模态框语义,配合.showModal()可以在不使用复杂 ARIA 的情况下建立焦点陷阱和背景遮罩。兼容性在现代浏览器里已经可以接受,如果你的项目不需要兼容特别老的浏览器,可以考虑用它替代自造的 div 弹窗。
我的建议是分两层看:基础语义标签(header、nav、main、article、section、aside、footer)属于“必会技能”,每个前端开发者都该熟练掌握;进阶标签(hgroup、details、dialog)属于“按需使用”,遇到匹配场景时用,不要为了秀技术硬套。
9. 一次真实改造:某内容站语义化重构复盘
前阵子帮某内容站(一个技术博客类项目)做结构重构,那个项目的首页大概有几百个 div 嵌套,页面权重集中在正文和列表上,但爬虫抓取出来的内容摘要经常跑偏,老是抓到侧栏的推荐阅读。
我们做的第一步是盘点页面区块,把所有可视区块按语义归类:哪些是页眉、哪些是主导航、哪些是主内容、哪些是辅助内容、哪些是页脚。这一步不需要写代码,纯靠“理解业务”来定边界。
第二步是逐区块替换标签,同时把原本依赖 class 的样式选择器跟着调整。这里有个实操建议:语义化改造尽量跟样式重构解耦。先把标签换对、样式名沿用,等结构稳定后再把 class 名规范一遍。否则一边改结构一边改样式,出现问题很难定位,而且改动范围不可控。
第三步是在结构稳定后跑一轮大纲检查,确认每个页面只有一个 main、h1 层级没有跳变、辅助内容都进了 aside。整个改造完成之后,线上跑了一段时间,最直观的变化是:搜索引擎展示的页面摘要开始更精准地抓取正文首段,读屏体验测试也顺畅了不少。
这个项目的复盘给我最大的感受是:语义化改造并不难,难的是“判断每个区块该归到哪个语义层级”这件事,而这件事恰恰是最需要跟产品经理、业务方沟通的部分。技术标签是死的,业务理解是活的。
10. 收尾:语义化是一种“写得少、想得多”的工程习惯
说句实在话,掌握语义化标签的拼写和嵌套规则,半天时间就够。真正拉开差距的,是你面对任何一个页面模块时,能不能迅速判断出它的内容性质和结构层级。这种判断力,只能靠一次次实际项目里“多想一步”来积累。
我现在写页面,默认的思维路径是这样的:先想清楚这个区块在文档大纲里的位置,再写标签;而不是先随手写个 div,等代码成型了再琢磨“这里要不要换语义化标签”。顺序反了,语义化就成了事后补救,效率低且容易遗漏。
如果你正在重构一个老项目,我建议不要试图一次把所有页面全部改造完。先挑一个典型页面(比如首页或详情页)做全部改造,同时把自己的选型判断沉淀成一份团队可用的“标签使用规范”文档,后续改造照着执行就行。按我的经验,页面之间的区块结构大同小异,跑通三五个典型页面之后,剩下都是复制判断逻辑而已。
最后补一个我踩过坑后总结的实操小技巧:在写完语义化结构后,关掉 CSS 看一遍纯 HTML 的可读性。如果这时你还能清晰说出“这个页面的标题是什么、主体分区有哪些、侧栏在哪”,语义化就做到位了。关掉 CSS 这个动作,就是检验语义化的照妖镜。