上一篇讲浏览器打开网页时,读到 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 的骨架,知道元素、属性、嵌套各自负责什么,也知道结构写错时该去哪里核对。