Skip to content
This page has been auto-translated and may contain errors.View in English

语义化 HTML ​

docs.scrimba.com

两个页面看起来可以完全一样,但内部的搭建方式却可能天差地别。一种用与内容名副其实的元素来搭建:头部就是头部,导航就是导航,主区域就是主区域。另一种把所有东西都塞进同样一个没有身份的容器里,靠类名来区分彼此。从屏幕上看你分不出区别,但除此之外,它们根本不是同一种页面。按元素的含义来选择元素,正是语义化 HTML 要讲的事。

语义指的是什么 ​

语义(Semantic) 说的就是"和含义有关"。语义元素指的是,它的名字本身就说明了它装的是什么内容。段落放进 <p> 里,因为它就是一段文字。导航菜单放进 <nav> 里,因为它就是导航。标签名本身就在描述这个东西。

你几乎可以用一个万能盒子搭出任何页面,再靠样式让它看起来对。但这样一来,页面上就没有任何东西能说明自己是什么了。选择能命名内容的元素,就像搬家时给箱子贴标签一样:不贴标签,东西照样能装进箱子,但贴上标签,别人(或者浏览器)才能不用打开每个箱子就知道里面装的是什么。

一个元素是**语义化**的,前提是它的名字描述的是内容的含义,而不是内容的样子。<nav>、<article> 和 <button> 都是语义元素:它们告诉你内容是干什么用的。<div> 和 <span> 不是:它们只是通用容器,自身没有任何含义。

这样做的好处是,含义写进了标记里,其他工具就能读出来。浏览器不需要你额外做任何事,就知道 <button> 是可点击的,也能用键盘聚焦。屏幕阅读器能把 <nav> 播报成一个导航区域。搜索引擎会把 <article> 当作一段独立完整的内容来处理。这些效果放在样式化过的 <div> 上统统失效,因为 <div> 本身不对自己装的内容做任何承诺。

语义,说到底就是"携带含义的元素"和"只携带像素的元素"之间的区别。当你写下 <nav> 时,你做的不只是画一个框,而是在陈述一个关于文档的事实:这个区域是导航区。这个事实会记录在**可访问性树(accessibility tree)**中(浏览器在 DOM 之外同步构建的一套平行结构,用来向辅助技术描述每个元素的角色和状态),而这正是除了用眼睛看鼠标点的用户之外,其他工具真正依赖的东西。

有一个实用的检验方法:替换测试。如果你把一个元素换成裸的 <div>,再用样式把它做得看起来一模一样,你会失去什么?对 <p> 或纯装饰性的包装容器来说,什么也不会失去。但对 <button> 来说,你会失去键盘焦点、回车键和空格键的处理,以及屏幕阅读器"按钮"的播报——而这些都得你自己手动重新实现。这个差距就是语义:因为元素的名字告诉了浏览器内容是什么,浏览器才免费给你这些行为和含义。选用有名字的元素不是风格偏好,而是获得正确行为成本最低的方式。

Juno语义指的是什么 语义化,就是标签的名字和它装的内容对得上:段落用 <p>,菜单用 <nav>。你可以把所有东西都塞进普通盒子里,用样式做得看起来一样,但这样谁也说不清里面是什么。就把它想成是给搬家的箱子贴标签,谁都能一眼看出里面装的是啥。
Juno语义指的是什么 语义元素能说出自己的含义,<div> 什么都说不出来。好处是浏览器、屏幕阅读器、搜索引擎都能直接从标记里读出含义。优先选能描述内容的元素,把 <div> 留给那些真的没什么可描述的地方。
Juno语义指的是什么 做一下替换测试:把元素换成样式化的 <div>,看看你失去了什么。如果答案是键盘焦点、可访问性树里的角色,或者屏幕阅读器的播报,那就是你原本白得的语义。手动重建这些,永远比直接用有名字的元素麻烦得多。

地标元素 ​

有几个元素专门用来划分页面上的大区域,也是你最先该用到的:

  • <header> 是页面或某个区块的顶部:logo、标题、简介。
  • <nav> 是一组导航链接,比如主菜单。
  • <main> 是主要内容,也就是这个页面存在的原因。
  • <section> 是一组相关内容的集合,通常带有自己的标题。
  • <article> 是一段独立完整的内容,单拿出来看依然说得通,比如一篇博客文章或一条新闻。
  • <aside> 是旁侧的内容:相关链接、备注、广告。
  • <footer> 是页面或区块的底部:版权信息、联系方式、小字说明。

规则每次都一样:如果一个区域是导航,就放进 <nav>,不要放进 <div>。如果是页脚,就放进 <footer>。用和区域相匹配的元素来命名它。

html
<header>
  <h1>Mara's Kitchen</h1>
  <nav>
    <a href="/recipes">Recipes</a>
    <a href="/about">About</a>
  </nav>
</header>
<main>
  <h2>Weeknight dinners</h2>
  <p>Quick meals you can make after work.</p>
</main>
<footer>
  <p>Made in Lisbon.</p>
</footer>

有七个元素把页面划分成主要区域,它们统称为**地标元素(landmark elements)**,因为每一个都标出了一个读者或工具可以直接导航到的区域:

  • <header> 引出它最近的容器:放在页面顶层时,它装的是站点品牌信息;放在 <article> 内部时,它装的是这篇文章的标题和作者信息。
  • <nav> 包裹一组导航链接。用它来标记重要的导航,比如主菜单或目录,不要用在页面上每一个零散的链接上。
  • <main> 装文档的主要内容,也就是这个页面独有的部分,而不是全站重复出现的部分。
  • <section> 把相关的内容归到一起,通常前面会有一个标题。
  • <article> 是一个完整、独立的单元:一篇帖子、一条评论、一张商品卡片。判断标准是:把它单独拿出来展示,依然说得通吗?
  • <aside> 装与周围内容相关但不是核心的内容:侧边栏、引言摘录、相关链接。
  • <footer> 用收尾性的内容关闭它最近的容器:作者信息、链接、法律声明。

这里的关键在于,其中大多数元素是"看上下文"的。<header> 和 <footer> 描述的是它们所处的容器,所以同一个页面上可以出现好几对,一对属于整个页面,其余每对属于各自的 <article>。<main> 是例外,会在下一节讲到。

html
<article>
  <header>
    <h2>Roasted tomato soup</h2>
    <p>By Kwame, 20 minutes</p>
  </header>
  <p>Halve the tomatoes and roast them until soft.</p>
  <footer>
    <p>Serves four.</p>
  </footer>
</article>

这七个区域元素就是 HTML 的**地标(landmarks)**:能在可访问性树中暴露出一个可导航区域的元素。每一个都对应一个角色,辅助技术可以列出这些角色并在它们之间跳转——这也正是它们除了让源码看起来整洁之外真正重要的原因。

  • <header> 在页面层级对应 banner 角色,但如果嵌套在 <article>、<aside>、<main>、<nav> 或 <section> 内部,它就只是一个没有地标角色的引导性分组。
  • <footer> 情况相同:在页面层级是 contentinfo,嵌套在上述分区元素内部时只是一个普通分组。
  • <nav> 对应 navigation 角色。一个页面通常会有不止一个,比如主菜单和页内目录,这就用到了后面讲的标签命名。
  • <main> 对应 main 角色,标出主要内容。每个页面只有一个。
  • <aside> 对应 complementary 角色:辅助主要内容、但又独立于主要内容之外的部分。
  • <section> 只有在拥有一个可访问名称时才会成为 region 地标,这个名称通常来自与它关联的标题;没有名字的 <section> 不会暴露任何地标,在可访问性树看来,它几乎等同于 <div>。
  • <article> 带有 article 角色,代表一段独立完整的内容组合,工具可以枚举它,但它并不像其他元素那样是一个导航用的地标。

这一整套设计的目标是:让非视觉用户浏览文档的速度,能和视觉用户扫视版面的速度一样快。屏幕阅读器可以列出页面上所有的地标,让用户直接跳到"main"或"navigation",而不必听完中间所有的内容。但这一切成立的前提是,区域必须用真正的地标元素来标记。满屏都是 <div> 的文档没有任何地标,唯一的浏览方式就是从头听到尾。

html
<body>
  <header>            <!-- banner:页面层级 -->
    <nav>...</nav>    <!-- navigation -->
  </header>
  <main>              <!-- main:唯一的主要区域 -->
    <article>...</article>
    <aside>...</aside> <!-- complementary -->
  </main>
  <footer>...</footer> <!-- contentinfo:页面层级 -->
</body>
Juno地标元素 值得记住的区域标签有七个:<header>、<nav>、<main>、<section>、<article>、<aside>、<footer>。每一个都命名了页面的某一部分,你要选和你搭建的区域相匹配的那个。今天不用死记硬背,用得多了自然就记住了。
Juno地标元素 七个元素把页面切成不同区域,其中大多数描述的是它们所处的容器,所以 <header> 和 <footer> 可以出现不止一次。有一个 <article> 判断法帮我省了不少纠结:单独拿出来能不能说得通?能,就是 article;不能,但是带标题的一组内容,那就是 section。
Juno地标元素 这些就是地标,每一个都对应一个屏幕阅读器可以跳转到的角色,这也是它们胜过一堆 <div> 的原因。有两个坑值得留意:<header> 和 <footer> 只有在页面顶层才会成为 banner 和 contentinfo 地标;而 <section> 只有拥有可访问名称之后才算地标。没有名字的 section,对辅助技术来说基本等同于 div。

div 和 span:没有语义时的退路 ​

页面上不是每个部分都值得起一个有含义的名字。有时候你只是需要一个盒子来把东西归到一起方便加样式,或者包住几个字改改颜色。为此有两个本身不带任何含义的普通元素。

<div> 是一个通用的块级容器:找不到更合适的元素时就用它当空容器。<span> 是同样的思路,用在一行文字里的一小段内容上。它们对自己的内容什么也不说,存在的唯一目的就是给样式和分组提供一个挂钩。

html
<p>Today's total is <span class="price">£42.00</span>.</p>

<div class="card">
  <h2>Special offer</h2>
  <p>Two for one on all pasta.</p>
</div>

这里的 <span> 包住一个价格,方便给它上色。<div> 把一张卡片归到一起,方便给它加边框。只有当找不到更有含义的元素时才用这两个,因为它们对自己装的内容什么都没说明。

<div> 和 <span> 是仅有的两个完全没有含义的元素,而这恰好就是它们的用处所在。<div> 是通用的块级容器,<span> 是通用的行内容器。你把它们当作**样式挂钩**来用:当你需要把内容分组或作为定位目标、但内容本身又没什么好说的含义时,就挂上一个 class 或 id。

html
<div class="grid">
  <article class="card">...</article>
  <article class="card">...</article>
</div>

这里的 <div class="grid"> 存在的唯一目的就是把卡片排成网格布局。这是纯展示性的工作,不带任何含义,所以用 <div> 是正确的选择,不是什么需要惭愧的退路。真正的错误是反过来:把有含义的内容也用 <div> 装。一个可点击的 <div> 应该是 <button>,一个装满导航链接的 <div> 应该是 <nav>,一个包住整篇文章的 <div> 应该是 <article>。只有当内容真的没有含义,纯粹是布局或样式上的需要时,<div> 和 <span> 才是正确答案。

<div> 和 <span> 是语义上完全空白的元素:分别是通用块级容器和通用行内容器,在可访问性树中没有任何角色,对任何工具都不传达任何含义。它们恰好适合做一件事,也就是充当**展示性挂钩**:这种元素存在的唯一目的是承载样式,或者把子元素归到一起方便布局,不携带任何屏幕阅读器或搜索引擎应该播报出来的信息。

拥有一个完全没有含义的元素的价值在于,它能让那些真正有含义的元素保持诚实。网格容器、flexbox 行、定位上下文——这些都是真实存在的需求,本身却没有语义内容,硬把一个有含义的元素套在上面就是在标记里说谎。一个纯粹为了背景颜色而用的 <section>,等于在声称自己是一个它其实不是的区域。所以这条规则是双向的:不要在有名字的元素能用的时候硬用 <div>,也不要在只需要一个盒子的时候硬用有名字的元素。

要留意的失败模式是"交互式 <div>":给一个元素挂上 JavaScript 的点击处理函数,让它假装成按钮或链接。它能渲染,鼠标点上去也有反应,看起来是完工了,但它没有角色,不在 Tab 键的焦点顺序里,对回车键和空格键没有反应,屏幕阅读器什么也不会播报。用 tabindex、role 和按键处理函数把这一切重新搭一遍,只是在脆弱地重新实现一个本来就存在的 <button>。如果一个东西表现得像一个控件,就用控件元素;把 <div> 和 <span> 留给那些只关乎像素的部分。

Junodiv 和 span:没有语义时的退路<div> 是一个普通盒子,<span> 是包住几个字的普通容器,两者本身都没有任何含义。当你只是需要归类或加样式、又找不到合适的有名字的元素时,就用它们。如果内容确实有含义,比如是个菜单或按钮,那就用对应的元素。
Junodiv 和 span:没有语义时的退路 当唯一的工作是布局或样式、没有含义要表达时,用 <div> 或 <span> 就是对的选择。不用为此感到愧疚,该愧疚的是反过来的情况:本该是 <button> 的可点击 <div>,或者本该是 <article> 的包装 <div>。没有含义就用空元素,有真实含义就用有名字的元素。
Junodiv 和 span:没有语义时的退路<div> 和 <span> 在可访问性树中没有任何角色,这正是它们适合纯样式挂钩、却不适合任何有含义的场合的原因。真正会咬你一口的是带点击处理函数的交互式 <div>:没有焦点、没有键盘响应、没有播报,还得用一堆 tabindex 和 role 的补丁去假装出 <button> 本来就白送的能力。如果它表现得像个控件,就把它做成控件。

挑对正确的元素 ​

不确定该用哪个元素时,问自己一个简单的问题:这东西是什么?不是它看起来像什么,而是它本质上是什么。答案通常就已经告诉你该用哪个元素了。顶部横排的一行链接是导航,那就是 <nav>。底部一块小字说明是页脚,那就是 <footer>。一个你想上色的价格本身没什么实质含义,那就是 <span>。

从含义出发,只有当真正的答案是"它只是个盒子"时,才退而用 <div> 或 <span>。早点养成这个习惯,你的页面就能自己说明自己是什么,等你或别人以后再来读这份标记时,这个习惯就会派上用场。

挑对元素其实是一次每次都能重复的简短判断。问自己内容是什么,让答案来决定用哪个元素:

  • 它是不是一个独立完整、单拿出来也说得通的东西?用 <article>。
  • 它是不是一组带标题的相关内容?用 <section>。
  • 它是导航吗?用 <nav>。是某个区域的引导性内容吗?用 <header>。是收尾内容吗?用 <footer>。
  • 它是辅助主要内容的旁侧内容吗?用 <aside>。
  • 没有含义,只是布局或样式上的需要?用 <div>,如果是在一行文字里就用 <span>。

最难判断的分岔点是 <section>、<article> 和 <div> 之间的取舍。按顺序想一遍:如果这块内容能独立存在,那是 <article>;如果不能,但是一组带自己标题的主题内容,那是 <section>;如果没有标题也没有含义,纯粹是为了加样式或做布局,那是 <div>。<section> 几乎总该有一个标题;如果你想不出该给它起什么标题,这就是一个强烈的信号,说明你其实想要的是 <div>。

html
<main>
  <section>
    <h2>Latest posts</h2>
    <article>
      <h3>Why I switched to plain HTML</h3>
      <p>...</p>
    </article>
    <article>
      <h3>The case against heavy frameworks</h3>
      <p>...</p>
    </article>
  </section>
</main>

这里的 <section> 是一个主题分组,"最新文章",里面每篇文章用 <article>,因为它们各自都能独立成立。这里的嵌套结构,恰好映射出了内容的含义。

要挑得好,关键在于始终先读懂含义,从来不看布局,但也有几条规则能让常见的判断变得机械化。

每个页面只能有一个 <main>。它标出主要内容,也就是这个页面独有的部分,而不是全站重复出现的头部、导航和页脚。如果可访问性树里出现了不止一个 <main>,屏幕阅读器用户就会看到两个"main"目标,却不知道哪个才是真的,所以要保持它的唯一性。所有全站通用的内容都应该放在它外面。

另一条反复用得上的规则是**给重复出现的地标打标签**。当一个页面上有两个同类型的地标元素,比如一个主 <nav> 和一个页内目录,两者都会被播报成"navigation",用户没法分辨。这时你可以用 aria-label 来消除歧义,这个属性能为一个区域提供一个简短的可访问名称,而不会在屏幕上显示出任何东西:

html
<nav aria-label="Primary">
  <a href="/">Home</a>
  <a href="/blog">Blog</a>
</nav>

<nav aria-label="On this page">
  <a href="#intro">Introduction</a>
  <a href="#setup">Setup</a>
</nav>

现在这两个区域分别播报成"Primary navigation"和"On this page navigation",地标列表就可以正常使用了。把同样的标签方式用到任何重复出现的地标上,<aside> 或 <section> 也包括在内。对 <section> 而言,记住它只有在拥有可访问名称之后才会成为地标,不管这个名称来自关联的标题还是来自 aria-label。

贯穿以上所有内容的元规则是:标记是一份供机器读取的文档描述,会被屏幕阅读器、搜索爬虫、阅读模式工具,以及未来的你自己读取。按含义来选择元素,正是让这份描述保持真实的关键。布局是 CSS 的事,任何时候一旦你发现自己在按"看起来像什么"来选元素,你就已经越界闯进了 CSS 的地盘,还用错了工具。

Juno挑对正确的元素 卡住的时候,问这东西是什么,而不是它看起来像什么。导航就是 <nav>,小字说明就是 <footer>,一个你只想加样式的普通盒子就是 <div>。每次都从含义出发,你的页面最终就会自己说明自己是什么。
Juno挑对正确的元素 按这个小清单走一遍:能独立存在的是 <article>,带标题的主题分组是 <section>,没有含义只是加样式的是 <div>。能解决大多数 section-vs-div 争论的判断法:如果你给它想不出一个标题,那你其实想要的是 <div>。让含义来挑标签,嵌套结构自然就理顺了。
Juno挑对正确的元素 两条规则撑起了大部分场景:每个页面只用一个 <main>,重复出现的地标要用 aria-label 打上标签,免得两个 nav 都播报成"navigation"。记住 <section> 只有拥有可访问名称之后才算地标。按含义来选元素,把"看起来像什么"交给 CSS,因为一旦你为了渲染效果去选标签,你就是在用错误的工具做 CSS 该干的事。