上一篇讲浏览器打开网页时,读到 HTML 才建出一棵 DOM 树。但那棵树具体是你用什么写出来的、为什么长成那样,上一篇只带了一句。这一篇就解决这一件事:HTML 靠什么把一个页面搭成有结构的骨架。

先记住一句总的前提:HTML 不是给人看的排版稿,而是给浏览器看的说明书。你写在文件里的字符,最终要变成内存里那棵树的节点。所以下面所有语法,都值得问一句“它在这棵树上留下了什么”。

最小可用的一份 HTML

1<!doctype html>
2<html lang="zh-CN">
3  <head>
4    <meta charset="utf-8" />
5    <title>我的第一个页面</title>
6  </head>
7  <body>
8    <h1>你好</h1>
9    <p>这是我的第一个页面。</p>
10  </body>
11</html>

这十几个字符里,一共只有四层东西:

<!doctype html> 不是元素,而是一条给解析器看的声明。它必须放在文件第一行:只有写了它,浏览器才会用标准模式解析页面,其余部分才能按预期工作 1。

<html> 是根元素,页面里所有东西都在它里面。它上面带的 lang="zh-CN" 声明这份文档的主要语言,屏幕阅读器靠这个信息决定用哪套发音规则,翻译工具也靠它判断 2。

<head> 装的是“关于这个页面的信息”,而不是页面内容本身:编码、标题、样式表、脚本。上一篇说浏览器读 HTML 时发现 <link>、<script> 就再去发请求,那些标签通常就写在这里。注意它里面的东西用户基本看不到。

<body> 才装用户能看到的一切。上面那段里只有一个一级标题和一段文字。

需要留意的是:可见的文字必须写在 <body> 里。写在 <html> 下面、<body> 外面的文字,浏览器通常会顺手帮你塞进 body,但那是容错,不是你可以依赖的写法。

元素、标签,以及只有开始标签的那种

<p>这是我的第一个页面。</p> 这一整块叫一个 元素(element)。它由三部分拼成:开始标签 <p>、内容 这是我的第一个页面。、结束标签 </p>。

区分“标签”和“元素”很有用:标签只是尖括号里的那个记号,作用是标出边界;元素才是整块东西,也就是上一篇里树上的一个节点。你写一对标签,浏览器就在树上多一个节点。

标签名不区分大小写,<P> 和 <p> 效果一样,但惯例都写小写 1。

还有一类元素根本没有内容和结束标签,比如 <img src="cat.jpg" alt="一只猫" />。这叫 空元素(void element),img、br、meta、link、input 都属于这一类。它们的任务往往是往文档里插入或引用一个外部东西,本身不包裹任何内容,所以只有一个标签,也不能往里放东西 3。末尾那个 / 在 HTML 里可写可不写,在 XHTML 或 SVG 里才必须写。

属性:描述元素本身的那部分设置

属性写在开始标签里面,形式是 名字="值":

1<img src="cat.jpg" alt="一只猫" class="photo" id="hero" />

属性回答的是“这个元素本身是怎么回事”,而不是“元素里放什么文字”。所以凡是给元素用的参数——资源地址、链接目标、唯一标识——都放属性;可见的文字放内容。

几个你会反复遇到的:

src 和 href 分别给出图片、脚本这类资源的地址,以及链接指向哪里。alt 是图片的替代文字,图片加载失败时显示,屏幕阅读器也会读它。lang 声明这段内容的语言,可以写在单个元素上,也可以写在根元素上让所有后代继承 2。

class 和 id 是留给 CSS 和 JavaScript 的抓手。id 在整篇文档里必须唯一;class 是一组用空格隔开的标记,可以被很多元素共用 4。重点在于它们本身不改变任何外观——class="photo" 不会让图片变小。外观和交互分别由 CSS、JavaScript 按这些名字去选中元素再做。

还有一类 布尔属性,写了就是生效,不写就是不生效。比如 disabled,写成 disabled 和写成 disabled="disabled" 是一样的 1。

嵌套:结构真正的来源

HTML 的结构不来自缩进,而来自“谁在谁里面”:

1<ul>
2  <li>买牛奶</li>
3  <li>写代码</li>
4</ul>

上面两行缩进纯粹是给人看的,浏览器不看缩进。真正决定结构的是 <li> 处在 <ul> 里面这件事。嵌套的正确规则只有一条:先开的标签最后关。

1<p>这是<strong>很重要</strong>的一句话</p>   <!-- 对:strong 在 p 里面完整结束 -->
2<p>这是<strong>很重要</p></strong>           <!-- 错:两对标签交叉了 -->

第二行不会报错,页面可能还显示得好好的。因为 HTML 的解析器内置了容错规则,遇到交叉嵌套时它会去猜你的意图,而猜出来的结果未必是你想的那棵树。规范里有一个常被引用的例子:<p>1<b>2<i>3</b>4</i>5</p>。解析之后,<i> 被拆成了两个节点,4 落在新造出来的那个 <i> 里,而不是原来那个 5。

这带来一个很实际的判断方法:不要用“页面看起来正常”来确认结构写对了。确认的办法是打开浏览器的开发者工具,看 Elements 面板,它显示的不是你写的源文件,而是解析之后的 DOM 树。两者不一致时,以 DOM 树为准——毕竟 CSS 和 JavaScript 后面操作的也是它。

另有一条你可能已经见过的例外:有些元素的结束标签可以省略。因为解析器有“隐含结束”规则:<p> 遇到下一个块级元素会自己关掉;<li> 遇到下一个 <li>、表格单元格 <td> 遇到下一个 <td> 也一样 5。知道这条,是为了看懂别人写的(或自己漏写的)HTML 为什么结构跟想象的不一样,而不是为了省那几个字符:写全结束标签,最不容易出岔子。

结构不只是给浏览器“显示”用的

到这里,前面每一节的结论可以合成一句:标签名选哪个,描述的是“这段内容是什么”,而不是“它长什么样”。

<h1>你好</h1> 和在文字外面套个变大的样式,屏幕上可能看不出差别,但含义差得远。h1 表达的是“这是本页最主要的一级标题”。这个含义会被搜索引擎用来判断页面主题,被屏幕阅读器用来生成大纲、让人跳读,也被 CSS 用 h1 { } 直接选中 7。如果换成 <div class="big-text">,外观还在,意思丢了。

这就是所谓 语义。h1 到 h6 是标题层级,p 是段落,ul/ol 配 li 是列表,a 配 href 是链接——这些标签让“这段东西是什么”变成机器能读的信息。div 和 span 是没有语义的通用容器,只在确实找不到更合适的标签时才用。

一个稍完整的样子

1<body>
2  <h1>庞征的博客</h1>
3  <p>这里记录我学前端的过程。</p>
4  <ul>
5    <li><a href="/posts/1">第一篇</a></li>
6    <li><a href="/posts/2">第二篇</a></li>
7  </ul>
8</body>

这段对应的树(只看 body 部分)大致是:

1body
2├── h1
3│   └── "庞征的博客"
4├── p
5│   └── "这里记录我学前端的过程。"
6└── ul
7    ├── li
8    │   └── a  [href="/posts/1"]
9    │       └── "第一篇"
10    └── li
11        └── a  [href="/posts/2"]
12            └── "第二篇"

每个标签变成一个节点,嵌套变成父子关系,可见文字变成文本节点,属性挂在对应节点上。

这篇的边界

“哪些元素能放进哪些元素里”另有规定,叫内容模型。比如 ul 里只允许放 li,p 里不能放 div 这类块级元素 6。规则比“看起来合理”更严格,而且违反了同样不会报错。你以后遇到结构莫名其妙不对的情况,可以往这个方向查,但它不属于这篇的主线。

下一篇再讲怎么用这些标签把内容组织成真正的页面区块,以及让 CSS 接手的部分。现在的收获应该已经够用了:你能看懂一份 HTML 的骨架,知道元素、属性、嵌套各自负责什么,也知道结构写错时该去哪里核对。