正确处理 HTML 5 标签的关闭
在将公司一款使用 jQuery 的产品升级到最新的 jQuery 3.6 之后发现部分页面出现了排版异常的问题,查阅了通过 jQuery 生成的 HTML 和 jQuery 3.5 升级文档(https://jquery.com/upgrade-guide/3.5/) 后发现是因为 jQuery 从 3.5 版本开始区分 HTML 模式(HTML mode)和 XHTML 模式(XHTML mode) 来解析 HTML 了。
假设有这样一段 js 代码用于创建 jQuery 对象:
jQuery("<div/><span/>")
我们期望生成的 HTML 是这样的:
<div></div>
<span></span>
在 jQuery 3.5 以前生成的确实也是上述内容,但 3.5 版本开始,除非当前为 XHTML 模式,否则 jQuery 会生成这样的 HTML:
<div>
<span></span>
</div>
由于生成的 HTML 发生了变化,从而导致页面排版出现问题。
那么 jQuery 为什么要做这样的变化呢,jQuery 3.5 升级文档中也有解释:
Modern browsers usually parse and render HTML strings in HTML mode. Elements such as
<p>
and<span>
are never self-closing in HTML, so the browser interprets a string like<p/><span/>
as<p><span>
and leaves the tags open. The browser closes the tags at the end of the string or at an element that cannot be contained in the element, so another<p>
tag closes an existing open<p>
tag.现代浏览器通常以 HTML 模式解析和呈现 HTML 字符串。诸如
<p>
和<span>
之类的元素在 HTML 中永远不会自动关闭,因此浏览器将诸如<p/><span/>
之类的字符串解析为<p><span>
并使标记保持打开状态。浏览器将关闭字符串末尾或元素中不能包含其它元素的标签,因此另一个<p>
标签将关闭现有的打开的<p>
标签。
虽然通过这个解释已经基本明白是什么原因了,但也想正好借此机会来看看 HTML 规范中有关标签关闭(闭合)是怎么规定的。
记得我第一次系统性的学习 HTML 是从学校图书馆借到了一本名为《HTML与CSS网站设计实践之旅》(https://book.douban.com/subject/3121016/)的书开始的,这是一本非常好的入门书,结构清晰上手简单,感谢这本书奠定了我的 HTML 基础。不过书名虽然是《HTML与CSS网站设计实践之旅》,但实际上书中介绍的其实是 XHTML 的知识。
我们知道,现在最新的 HTML 规范是 HTML 5,在此之前还有 HTML 4.01、XHTML 1.1、XHTML 1.0 等规范。现在的 HTML 5 一看就和 HTML 4.01 一脉相承,都是 HTML 开头的规范,而 XHTML 1.1 则在 HTML 前多了个 X,好像有点区别。
XHTML 是 eXtensible HyperText Markup Language(可扩展超文本标记语言)的缩写。由于 HTML 相当的宽松,导致容易编写出格式错误的代码,不同浏览器也可能解析出不同的结果,因此 W3C 在 HTML 4 的基础上制定了 XHTML 1.0 规范,目的是使用格式良好的 XML 来改进代码的编写和浏览器解析的问题。
因为 XHTML 必须是格式良好(Well-formed)(https://www.w3.org/TR/xhtml1/#wellformed)的,所以书写规范和 XML 一样,例如标签必须成对出现,不能只有开始标签而没有结束标签。
这是一段来自 HTML 4.01 规范中介绍元素结束标记可以忽略(https://www.w3.org/TR/html401/intro/sgmltut.html#h-3.2.1)的代码,这样的代码是合法的:
<DIV>
<P>This is the paragraph.
</DIV>
但在 XHTML 中指出:
For non-empty elements, end tags are required.
对于非空元素而言,结束标签也是必须的。
因此在 XHTML 中的正确的写法必须是:
<p>here is a paragraph.</p>
<p>here is another paragraph.</p>
可以看到,HTML 4.01 允许的这种写法用在 XHTML 中,浏览器会显示错误提示。
不过这个“空元素(Empty Elements)”是什么呢?
在 XHTML 规范中没找到相关的定义,但因为 XHTML 是在 HTML 4.01 的基础上制定的,于是在 HTML 4.01 中找到了它:
Some HTML element types have no content. For example, the line break element BR has no content; its only role is to terminate a line of text. Such empty elements never have end tags.
一些 HTML 元素类型没有内容。例如,换行元素 BR 不包含任何内容;它的唯一作用是终止一行文本。这样的空元素永远不会带有结束标签。
也就是说,空元素(Empty Elements)指的是诸如 <br />
、<hr />
、<img />
这样不包含任何内容的元素。
在 HTML 4 中,这些空元素不但可以写成不带结束标签的形式,甚至都不用关闭标签,也就是说写成 <br>
、<hr>
、<img>
都是合法的。
但在 XHTML 又因为前面提到的格式良好的要求,规定了:
Empty elements must either have an end tag or the start tag must end with
/>
. For instance,<br/>
or<hr></hr>
.空元素必须带有结束标签,或者开始标签必须以
/>
结尾。例如,<br/>
或<hr></hr>
。
也就是说 XHTML 因为承袭了 XML 格式规范的优良传统,标签要么成对出现 <hr></hr>
,要么写成 <br/>
直接闭合的形式。
当然,XHTML 的制定者也考虑到了,非空元素不应该写为最小化(minimized)的形式:
Given an empty instance of an element whose content model is not EMPTY (for example, an empty title or paragraph) do not use the minimized form (e.g. use
<p> </p>
and not<p />
).当给定的元素内容为空,但其不是一个可空元素时(例如:空的标题或段落),请不要使用最小化形式(例如,使用
<p> </p>
而不是<p />
)。
但我认为这应该是语义上的,因为 <p />
实际上是符合 XML 的规范的,即使这么写浏览器也不会报错,浏览器还是会将它解析为 <p></p>
。
总之,因为 XHTML 为了规范代码的书写,避免不同浏览器解析出不同内容,引入了 XML 的编写习惯,空元素必须关闭。
很多资料上在介绍 HTML 5 时都会提及一句,与 HTML 5 竞争的 XHTML 2 规范止于草案,未能成为正式规范的故事,一则漫画很好的解释了 XHTML、XHTML 2 和 HTML5 的关系:混乱的标记语言XHTML2/HTML5(Misunderstanding Markup: XHTML 2/HTML 5 Comic Strip)中文翻译版(https://www.cnblogs.com/JustinYoung/archive/2009/07/30/misunderstanding-markup-xhtml-2-comic-strip.html) | 英文原版(http://www.smashingmagazine.com/2009/07/29/misunderstanding-markup-xhtml-2-comic-strip/)。
话题回到标签闭合上,HTML 5 是这样规定开始标签的:
if the element is one of the void elements, or if the element is a foreign element, then there may be a single U+002F SOLIDUS character (/). This character has no effect on void elements, but on foreign elements it marks the start tag as self-closing.
如果该元素是void 元素或foreign 元素,则可能存在单个 U+002F SOLIDUS 字符(/)。该字符对void 元素没有影响,但会将foreign 元素的开始标签设为自动关闭。
也就是说将开始标签写成自闭合的形式,对 void 元素没有影响,会当成没有 /
来解析。那 void 元素是什么呢?
HTML 5 规范将 HTML 元素(https://www.w3.org/TR/html52/syntax.html#writing-html-documents-elements)分为6大类,其中就规定了哪些元素属于void 元素(https://www.w3.org/TR/html52/syntax.html#void-elements):
<area>, <base>, <br>, <col>,
<embed>, <hr>, <img>, <input>,
<link>, <meta>, <param>,
<source>, <track>, <wbr>
这不就是 XHTML 1 和 HTML 4 的空元素嘛。
可以对比一下 void 元素之一的 br 元素(https://www.w3.org/TR/html52/textlevel-semantics.html#elementdef-br) 与常规元素之一的 p 元素(https://www.w3.org/TR/html52/grouping-content.html#the-p-element) 在 HTML 5 规范中“省略标签”(tag omission)部分的描述:
The
br
elementNo end tag
(没有结束标签)
The
p
elementA
<p>
element’s end tag may be omitted if the<p>
element is immediately followed by an<address>
,<article>
,<aside>
,<blockquote>
,<details>
,<div>
,<dl>
,<fieldset>
,<figcaption>
,<figure>
,<footer>
,<form>
,<h1>
,<h2>
,<h3>
,<h4>
,<h5>
,<h6>
,<header>
,<hr>
,<main>
,<nav>
,<ol>
,<p>
,<pre>
,<section>
,<table>
, or<ul>
, element, or if there is no more content in the parent element and the parent element is an HTML element that is not an<a>
,<audio>
,<del>
,<ins>
,<map>
,<noscript>
, or<video>
element.(如果
<p>
元素后紧跟<address>
、<article>
、<aside>
、<blockquote>
、<details>
、<div>
、<dl>
、<fieldset>
、<figcaption>
、<figure>
、<footer>
、<form>
、<h1>
、<h2>
、<h3>
、<h4>
、<h5>
、<h6>
、<header>
、<hr>
、<main>
、<nav>
、<ol>
、<p>
、<pre>
、<section>
、<table>
或<ul>
元素;或者父元素中没有更多的内容,并且父元素是<a>
、<audio>
、<del>
、<ins>
、<map>
、<noscript>
和<video>
以外的元素。)
可见 HTML 5 中,<br>
只需要写成 <br>
不需要结束标签,而增加了 /
写为 <br />
自闭合的形式也没有任何影响,仍然按 <br>
解析;而 <p>
的结束标签在一些情况下是可以省略的。
这点在现代浏览器中也可以得到印证:
在段落
p1
中,本来没有关闭该段落,但因为紧跟了一个<div>
标签,所以浏览器自动生成了一个</p>
来关闭该段落在段落
p2
中,自闭合形式书写的<br />
被解析为了<br>
那么再回到本文最开始的例子:
<div/><span/>
现代浏览器会直接忽略 HTML 5 中的自闭合写法,将其解析为 <div><span>
,但这两个元素都不是 void 元素。
并且在 HTML 5 规范中,div(https://www.w3.org/TR/html52/grouping-content.html#the-div-element) 和 span(https://www.w3.org/TR/html52/textlevel-semantics.html#the-span-element) 元素的“省略标签”部分标注的都是:开始和结束标签都不能省略(Neither tag is omissible)。
因此浏览器在最后补上了对应的结束标签 </span></div>
。所以我们看到的现象就是 span
元素被 div
元素包裹起来了:<div><span></span></div>
。
那有没有办法在 HTML 5 中也保持原来 XHTML 1 相对规范的写法风格呢,答案是肯定的。HTML 5 规范中规定了 XML 语法(XML syntax)(https://www.w3.org/TR/html52/xhtml.html#xhtml)(或称为 XHTML 语法 XHTML syntax,很多地方提及的 XHTML5
指的就是 HTML 5 的 XML 语法,实际上目前并没有 XHTML5
这种东西)。
简单的说,通常情况下,我们的 HTML 文件使用的是 text/html
的 MIME Type,这将告诉接收方(如浏览器)HTML 文档使用的是 HTML 语法(https://www.w3.org/TR/html52/syntax.html#syntax);而当 MIME Type 为 application/xhtml+xml
或 application/xml
,并在 HTML 中声明对应的 XML 命名空间时:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
<title>HTML5 Document with XML Syntax</title>
</head>
<body>
<div/><span/>
</body>
</html>
浏览器按 XML 语法来解析 HTML,自闭合标签解析成为成对的标签了。在 XML 语法中,可以像 XHTML 1 规范的那样按 XML 风格编写 HTML 5,并且可以使用 HTML 5 新增的诸多标签等。而且当 HTML 中出现不规范的地方时,浏览器仍然会停止解析并显示错误信息。
在 XHTML 中,我们必须严格遵守 XML 的写法,所有标签都必须关闭,通过自闭合(<br />
)的方式或者书写结束标签(<span></span>
)都可以。但如果不闭合标签,则会收到相关错误。
在 HTML 5 中,分为 HTML 语法和 XML 语法。HTML 语法宽松,可以像 HTML 4 一样编写 HTML,浏览器会忽略自闭合的标签(<br />
→ <br>
),或者补全一些未闭合的标签(<p>p1<div></div>
→ <p>p1</p><div></div>
),但需要注意开始结束标签不可省略的元素一定要写全,否则可能会解析出预期外的 HTML(<div /><span />
→ <div><span></span></div>
)。而在 XML 语法下,可以像 XHTML 一样遵守 XML 写法。
借由 jQuery 一个中断性变更,我们通过阅读 HTML 规范文档,加深了对标签闭合部分规定的了解。但更重要的一点是,虽然 HTML 5 的 HTML 语法宽松,但作为开发者,我们还是应该严格要求自己,区分好 HTML 5 和 XHTML 的差异,写出符合规范的代码,避免浏览器解析出不符合预期内容的情况。
jQuery Core 3.5 Upgrade Guide:https://jquery.com/upgrade-guide/3.5/
XHTML™ 1.0 The Extensible HyperText Markup Language (Second Edition):https://www.w3.org/TR/xhtml1
空元素:https://developer.mozilla.org/zh-CN/docs/Glossary/empty_element
HTML 4.01 Specification:https://www.w3.org/TR/html401/intro/sgmltut.html#h-3.2.1
HTML 4.01 Specification (19991224):https://www.w3.org/TR/1999/REC-html401-19991224/intro/sgmltut.html#didx-element-4
漫画:混乱的标记语言XHTML2/HTML5(附中文版翻译):https://www.cnblogs.com/JustinYoung/archive/2009/07/30/misunderstanding-markup-xhtml-2-comic-strip.html
HTML 5.2:https://www.w3.org/TR/html52/xhtml.html#xhtml
HTML5学习笔记简明版(1):HTML5介绍与语法:https://www.cnblogs.com/TomXu/archive/2011/12/06/2269008.html