学习前提

不需要编程基础。准备能保存纯文本的编辑器和浏览器,先显示文件扩展名。

本课完成后你能做什么

独立建立 HTML 文件,区分源代码、浏览器生成的 DOM 和最终页面,找到改动没有生效的原因。

先理解机制

HTML 不是一组让文字变大的指令,而是在声明内容是什么。浏览器读取字符后建立 DOM 树,再把样式和布局应用到节点。因此同一段 HTML 可以呈现为不同外观;反过来,外观相同也不代表结构相同。先把结构写清楚,再考虑装饰,会减少后续修改的成本。

doctype 用来请求标准模式,它不是显示在页面上的内容。html 的 lang 告诉辅助技术主要语言;head 保存编码、视口和标题,body 承载读者看到的内容。浏览器会自动修复某些错误结构,所以“看起来能显示”并不足以证明源代码正确。需要同时检查文件和开发者工具中的元素树。

可运行示例

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>My first page</title>
</head>
<body>
  <h1>Hello, world!</h1>
  <p>I am learning to build websites.</p>
</body>
</html>

推演与验证

保存为 index.html 后打开文件,页面应出现一个主标题和一个段落,标签页显示 My first page。先只改 title,再刷新;随后只改 h1,再刷新。两次变化位置不同,说明元信息和正文有不同职责。最后在开发者工具中临时改段落,刷新后改动消失,因为你修改的是内存中的 DOM,而不是磁盘上的文件。

常见错误与边界

出现代码原样显示时,先检查是否保存成了 index.html.txt;中文乱码先检查文件实际编码与 meta 是否都是 UTF-8。不要把整份文档放进已有页面的 body。开发者工具里看到额外节点不一定是文件被篡改,也可能是浏览器修复了遗漏的标签。

动手练习

创建一个中文自我介绍页:标签页显示你的名字,正文只有一个主标题、两个段落,并把文档语言设置正确。再用一句话解释刷新为什么会撤销开发者工具中的修改。

参考解法与理由

将 html 的 lang 改为 zh-CN,title 和 h1 分别填写标签页标题与正文标题。两个 p 是并列节点,不能为了空行而重复插入 br。刷新会重新读取源文件,因此没有保存到文件的 DOM 改动不会保留。

深入实验:源码和DOM为什么不一样

一个常见误判是“页面看起来正确,所以标签结构一定正确”。浏览器会修复某些非法嵌套,修复后的DOM才是CSS和JavaScript操作的对象。下面用离线解析器复现,不修改你正在阅读的页面。在独立练习页的开发者工具Console运行。

在现代浏览器Console中独立运行这个实验;它不替换上方主示例。

const source = '<p>Before<div>Inside</div>After</p>';
const parsed = new DOMParser().parseFromString(source, 'text/html');
console.log(parsed.body.textContent);
console.log([...parsed.body.children].map(node => node.tagName).join(','));
console.assert(parsed.body.children.length === 3);
console.assert(parsed.body.childNodes[2].textContent === 'After');

预期观察

第一行是BeforeInsideAfter;第二行是P,DIV,P。实际结构等价于<p>Before</p><div>Inside</div>After<p></p>。注意After是body的文本子节点,不在第一个p里。

原因、取舍与适用边界

遇到div开始标签时,HTML解析规则结束当前p;后面的孤立p结束标签又产生一个空段落。不是div天生无法显示,也不是CSS失效。若写p div选择器,它不会命中,因为树里没有这层父子关系。排错时先看Elements中的实际父节点,再看样式计算;View Source用于检查服务器发来的文本。这也解释了为什么复制一张截图不能证明语义结构正确。

进阶练习

保持“前文、独立内容块、后文”的顺序,改写成合法结构。再解释为什么children和childNodes的数量可能不同。

解法与推理

使用<p>Before</p><div>Inside</div><p>After</p>。children只包含元素,childNodes也包含文本和注释;源码中的换行空白也可能成为文本节点。验证应比较目标元素关系,不要把格式化产生的空白节点数量写成业务假设。

进一步阅读