HTML中空白符号和多余空格的处理

HTML 的空白字符处理规则,连续的空白字符(换行+空格)会被折叠成 1 个空格。所以有时渲染的页面里会有一个多余的空格,你要是不介意也没事。

HTML 中的空白

比如下面这两个代码片段是等效的:

1
2
3
4
5
<p id="noWhitespace">狗狗很 呆 萌。</p>

<p id="whitespace">狗狗很

萌。</p>

无论你在 HTML 元素内容中使用了多少空白(可以包括一个或多个空格字符,也包括换行),HTML 解析器在渲染代码时都会将每个空白序列减少为单个空格

那么为什么要使用这么多空白呢?答案是可读性。这里主要指的是代码缩进,一般每个嵌套的元素都比它所在的元素多缩进两个空格,这种结构易于人类阅读。

但是你在代码编辑器里为了“保持格式美观”而敲下的缩进空格(以及换行符),和你在文字内容里打出的空格,是没有任何区别的。它们都是实实在在的空白字符(U+0020),都会被纳入“空白序列”的解析过程中。

容易产生的问题

最常见的情况如下,就是一段话可能很长,可能就人为分成了多行

1
2
3
4
<p>
表 1.1 所有性状显著位点和候选基因数目统计表
Table 1.1 Summary statistics of significant loci and candidate genes across all traits
</p>

在谷歌浏览器显示的效果如下,这两行还是会变成一行,其中的换行符就变成一个空格。(如果是一段连续的内容看着就有点间断了)

1
表 1.1 所有性状显著位点和候选基因数目统计表 Table 1.1 Summary statistics of significant loci and candidate genes across all traits 

这里其实还要分情况讨论:

  1. 英文内容:没有关系,因为英文本身就是靠空格分隔的,只有在一个需要空格的地方换行就行
  2. 中文内容:有问题,因为中文内容本身是没有空格的。多出一个空格其实是不规范的。

解决办法

  1. 不要换行(一行要是太长了,可读性变差,查看源码要一直滑动水平滚动条,额,不过可以用编辑器的自动换行功能,但是总体来说还是不推荐)

  2. 用 HTML 注释吃掉换行,举例如下。原理是换行符号被包在注释里了,而注释是不会被解析的(这种写法也影响了可读性)。

    1
    2
    <p class='paragraph'>提取合格的gDNA... 20-24 小时,<!--
    -->接着对 gDNA 进行片段化...</p>
  3. 用火狐浏览器(中文会移除换行产生的空格,这样不用做任何改变)。不过要是发给客户就不行了,因为不太能要求客户更换或安装浏览器,毕竟谷歌系浏览器是主流。

  4. 不管它,多一个空格就多一个空格(强迫症做不到)

总体来说,只能无奈使用 HTML 注释吃掉换行。这纯粹是一种妥协性的做法,这是谷歌/edge这些浏览器没有考虑中文造成的问题(火狐不错,给火狐点赞)。

第二种做法其实是利用了标签内部的空白符号是被HTML忽视的,比如

1
2
3
4
5
6
7
<!-- 这四个完全等价,解析结果一模一样 -->
<caption class="x">
<caption
class="x"
>
<caption class="x" >
<caption class = "x" >

所以不是只有注释标签<!-- --> ,其他也可以这么做(就可以用本身的标签这么做,拆分到两行中)。比如下面将 <br> 拆分到两行中(将换行符 \n 包在 <br> 中)。

1
2
<caption>表 1.1 所有性状显著位点和候选基因数目统计表<br
>Table 1.1 Summary statistics of significant loci and candidate genes across all traits</caption>

原理

HTML中元素中的空白字符最终的渲染行为,由 CSS 的 white-space 属性决定。

但是”标签定义内部”(<>之间,不含属性值里面)中的空白字符(换行、缩进、多余空格)会被忽略,且不进 DOM

HTML中的空白字符

空白字符在不同编程语言环境中具有不同含义。对文档空白字符而言,仅包含空格(U+0020)、制表符(U+0009)、换行符(LF,U+000A)和回车符(CR,U+000D),其中回车符在所有方面都等同于空格。这些字符可用于格式化代码以提升可读性。我们的源代码中充斥着大量此类空白字符,通常仅在生产构建阶段为缩减文件大小而将其移除。

请注意,此列表不包含不可分割空格(U+00A0,HTML 中的 &nbsp;)。因此这些字符不会触发任何折叠 (Collapsing,),这也是它们常被用于在 HTML 中创建更长空格的原因。

CSS 还定义了分段符的概念,在 HTML 语境中其功能等同于换行符。

HTML如何处理空白字符

“HTML 会忽略空白字符”是一个普遍存在的误解,事实并非如此:HTML 会完整保留源代码中所有空白文本内容。作为标记语言,HTML 会生成一个 DOM(文档对象模型,Document Object Model),其中文本内容的所有空白字符均被完整保留,可通过 DOM API 进行检索和操作。若 HTML 从 DOM 中移除空白字符,那么作为基于 DOM 工作的下游渲染引擎,CSS 将无法通过white-space属性保留这些空白。

简单的说,原封不动进入 DOM。

备注:需要明确的是,我们讨论的是 HTML 标签之间的空白字符,这些空白字符在 DOM 中会转化为文本节点。

CSS如何处理空白字符

当 DOM 被传递给 CSS 进行渲染时,默认情况下空白字符会被大量移除。这意味着代码的格式化方式对最终用户不可见——在元素周围和内部创建空间是 CSS 的职责。

1
2
3
<!doctype html>

<h1> Hello World! </h1>

该源代码在 doctype 标签后包含若干换行符,并在 <h1> 标签前后及内部存在大量空格字符。但浏览器会忽略这些空白字符,仅显示“Hello World!”字样,仿佛这些字符根本不存在。

CSS 会忽略大部分(但并非全部)空白字符。在此示例中,当页面在浏览器中渲染时,“Hello”与“World!”之间的一个空格仍然存在。CSS 采用特定算法来判定哪些空白字符对用户无关紧要,并决定如何移除或转换这些字符。我们将在接下来的章节中详细说明该处理机制的工作原理。

折叠与转换 (Collapsing and Transformation)

让我们看一个例子。为了使空白字符更突出,我们还添加了注释,将所有空格显示为◦,所有制表符显示为⇥,所有换行符显示为⏎:

1
2
3
4
5
6
7
<h1>   Hello
<span> World!</span> </h1>

<!--
<h1>◦◦◦Hello◦⏎
⇥⇥⇥⇥<span>◦World!</span>⇥◦◦</h1>
-->

还是渲染成 “Hello World!”字样

<h1> 元素包含:

  • 一个文本节点(包含一些空格、单词“Hello”、一个换行符和一些制表符)。
  • 一个行内元素(<span>,包含一个空格和单词“World!”)。
  • 另外一个文本节点(只包含制表符和空格)。

由于 <h1> 只包含行级元素,它建立了行内格式化上下文。这是浏览器引擎可能使用的布局渲染上下文之一。

在这个上下文中,空白字符的处理可以总结如下:

备注:该算法可通过white-space-collapse属性(或其简写属性 white-space)进行配置。我们将首先假设其默认值(white-space-collapse: collapse),然后探讨不同属性值如何影响该算法。

  1. 首先,换行符前后紧邻的所有空格和制表符都会被忽略。因此,如果我们参考之前的示例标记:

    1
    2
    <h1>◦◦◦Hello◦⏎
    ⇥⇥⇥⇥<span>◦World!</span>⇥◦◦</h1>

    应用这条规则,会得到:

    1
    2
    <h1>◦◦◦Hello⏎
    <span>◦World!</span>⇥◦◦</h1>
  2. 然后,连续的换行符被折叠为单个换行符。此示例中不存在。

  3. 接下来,源代码中的行通过移除所有剩余换行符合并为单行。具体处理方式取决于换行符前后的上下文:换行符要么转换为空格(U+0020),要么直接删除。具体采用哪种方式取决于浏览器和语言环境。在本例的英文文本中(单词间以空格分隔),所有换行符都将被“转换”为空格。最终结果如下:

    1
    <h1>◦◦◦Hello◦<span>◦World!</span>⇥◦◦</h1>

    值得注意的是,在没有词间分隔符的语言(如中文)中,行与行之间不留空格。因此:

    1
    2
    <div>你好
    世界</div>

    根据浏览器的启发式算法,可能会渲染为“你好世界”,中间不带空格(火狐浏览器会这么做,中文会移除换行产生的空格。但是谷歌不会)。

    Firefox的做法(即折叠或移除换行产生的空格)更接近CSS规范的最新建议。

    • 规范依据:CSS Text Module Level 4 中有一个“line-break-transform”的示例(Example 19),它明确指出:在中日韩文字之间,换行符应当被移除,而不是替换为一个空格。Firefox的行为正是遵循了这一原则。
    • 核心逻辑:对于中文、日文等不使用空格分隔词汇的书写系统,换行符本身不代表任何语义,因此不应产生一个可见的空格。

    Chrome的做法(即保留折叠后的空格)则遵循了另一种规范解读或更旧的实现。

    • 规范依据:这更符合CSS Text Module Level 3 中对空白字符处理的一般规则,即连续的空白字符(包括换行符)会被折叠成一个可见的空格字符。
    • 其他浏览器:Safari的行为与Chrome类似。
  4. 然后,所有的制表符都会转换为空格字符,所以示例将变为:

    1
    <h1>◦◦◦Hello◦<span>◦World!</span>◦◦◦</h1>
  5. 之后,任何紧接在另一个空格之后的空格(即使跨越两个独立的行级元素)都会被忽略,因此最终结果为:

    1
    <h1>◦Hello◦<span>World!</span>◦</h1>

这就是为什么人们在访问网页时,会看到“Hello World!”这句话很好地写在页面的顶部,而不是一个奇怪的缩进的“Hello”,但在下面一行有一个更奇怪的缩进的“World!”。

完成上述步骤后,浏览器会处理换行和双向文本,此处我们不予考虑。请注意,在 <h1> 标签开头与 </h1> 标签闭合之间仍存在空白字符,但这些空白字符不会在浏览器中显示。接下来我们将处理此问题,具体将在每行布局时进行。

不同的white-space-collapse值会跳过此算法的不同部分:

  • preservebreak-spaces:跳过整个算法,不发生任何空白字符折叠或转换。
  • preserve-breaks:跳过第二步和第三步,保留换行符。
  • preserve-spaces:跳过整个算法,算法变为仅将制表符和换行符转换为空格。

简而言之,不同类型的空白字符将按以下方式进行折叠和转换(下面内容没看懂):

  • 制表符通常转换为空格。
  • 若需折叠分段符:
    • 连续的分段符序列将折叠为单个分段符。
    • 在使用空白字符分隔单词的语言(如英语)中,它们将转换为空白字符;而在不使用空白字符分隔单词的语言(如中文)中,则完全移除。
  • 若需折叠空白字符:
    • 断行符前后空白字符或制表符将被移除。
    • 连续空白字符序列折叠为单个空白字符。
  • 保留空白字符时,连续空白字符视为不可断行,但会在每组空白字符末尾进行软换行——即下一行始终从下一个非空白字符开始。但当采用 break-spaces 值时,每个空白字符后都可能发生软换行,因此下一行可能以一个或多个空白字符开头。

修剪与定位 (Trimming and Positioning)

在行内和区块格式化上下文中,元素均按进行排版。行内格式化上下文中,行通过文本换行生成。而在块级格式化上下文中,每个块级元素各自形成独立行。每行布局时,空白字符会继续被处理。让我们通过示例说明其工作原理。

本例中,我们仍像之前那样在注释中标记了空白字符。共有三个仅含空白字符的文本节点:第一个位于首个 <div> 之前,第二个位于两个 <div> 之间,第三个位于第二个 <div> 之后。

1
2
3
4
5
6
7
8
9
10
11
12
13
<body>
<div> Hello </div>

<div> World! </div>
</body>

<!--
<body>⏎
⇥<div>⇥Hello⇥</div>⏎

◦◦◦<div>◦◦World!◦◦</div>◦◦⏎
</body>
-->

渲染结果如下(没有任何多余的空白字符)

1
2
Hello
World!

示例中的空白字符处理方式总结如下:(这里假设 white-space-collapse: collapse

  1. 首先,空白字符会像上一节所示那样被折叠,转换为

    1
    <body>◦<div>◦Hello◦</div>◦<div>◦World!◦</div>◦</body>

    随后,行布局将根据 <body> 建立的块级格式化上下文进行排版。在此示例中,<body> 的五个子节点各自作为独立行进行布局。(此代码块中的每行代表渲染布局中的独立行,而非原始 HTML 代码中的行):

    (这里应该是包括了2个<div>和3个空格,才说的5个子节点)

    1
    2
    3
    4
    5
    6
    7
    <body>

    <div>◦Hello◦</div>

    <div>◦World!◦</div>

    </body>

    请注意,如果行过长,每行可能会换行并生成更多行。实际上,浏览器在布局过程中会确定每行的内容。关于文本换行机制的具体原理,我们在此不作赘述。

  2. 行首的空白字符序列将被移除(彻底删除,这就是修剪,移除行首行尾空白字符),因此示例变为:

    1
    2
    3
    4
    5
    6
    7
    <body>

    <div>Hello◦</div>

    <div>World!◦</div>

    </body>
  3. 此时保留的每个制表符均按 tab-size(默认是8,也就是一个制表符的宽度是8个空格的宽度) 进行渲染。这仅当 white-space-collapse 设置为 preservebreak-spaces 时生效,因为其他所有设置都会将制表符转换为空格符。

  4. 行尾的空白字符序列将被移除,因此上文变为:

    1
    2
    3
    4
    5
    6
    7
    <body>

    <div>Hello</div>

    <div>World!</div>

    </body>

    当前存在的三个空行在最终布局中不会占用任何空间,因为它们不包含任何可见内容。因此页面最终仅有两行会占据空间。浏览网页的用户会看到“Hello”和“World!”分别显示在两行上,这完全符合预期中两个 <div> 元素的布局效果。浏览器本质上会忽略 HTML 代码中包含的所有空白字符。

不同的 white-space-collapse 值会跳过此算法的不同部分:

  • preservebreak-spaces:跳过整个算法(除了第三步),不发生任何空白字符折叠或转换。
  • preserve-breaks:跳过整个算法,因此行首行尾的空白字符得以保留。
  • preserve-spaces:应用与 collapse 值相同的算法。

这里都只是说了修剪,没有说到定位,定位就是下面表格中的挂起(不显示行尾的空白字符,但是实际存在)。

white-space 属性

这个属性指定了两件事:

  • 空白字符是否合并,以及如何合并。
  • 是否换行,以及如何换行。

white-space 属性可以被指定为从下面的值列表中选择的单个关键字,或者是表示 white-space-collapsetext-wrap 属性的简写的两个值。

下面的表格总结了各种 white-space 关键字值的行为:

换行符 空格和制表符 文本换行 行末空格 行末的其他空白分隔符
normal 合并 合并 换行 移除 挂起
nowrap 合并 合并 不换行 移除 挂起
pre 保留 保留 不换行 保留 不换行
pre-wrap 保留 保留 换行 挂起 挂起
pre-line 保留 合并 换行 移除 挂起
break-spaces 保留 保留 换行 换行 换行

解释如下:

  • 表格中的文本换行是说当一行文本的长度超过其容器的宽度时,浏览器是否允许在不借助 <br> 标签的情况下,自动把多余的文字折到下一行。

  • “行末” 指的是:在浏览器自动换行(或 <br> 强制换行)之后,每一行文字末尾的那些空白字符。

    • “行末空格”:特指普通的空格字符(U+0020)。例如 "Hello "(Hello后面有个空格),如果这个空格正好在换行前,它就是行末空格。
    • “行末的其他空白分隔符”:通常指制表符(Tab,U+0009),以及其他极罕见的 Unicode 空白分隔符(但网页中 99.9% 的情况就是指 Tab)。
  • “挂起” (Positioning / Hanging):这个空白字符还在 DOM 里,但在生成行框时,浏览器保留这个空白字符,但把它“挂”在行框的右边界之外。

    • 不占行框内部的水平空间(因此不会导致溢出的宽度,也不会把后面的文字挤到下一行)。

    • 但它存在于 DOM 中,如果你用鼠标选中该行文字并复制,这个空格会被复制出来

      你可以把它想象成:这行文字末尾有一个透明的“小尾巴”

      • 存在(如果你选中这行文字复制,末尾的这个空格会被复制)。

      • 但它不占位(它不会把后面的文字挤到下一行,也不会让容器出现横向滚动条)。

      用“行末空格”举例,对比不同值的表现:

      假设一行文字是 "Apple "(Apple后面有个空格),且这行刚好顶到容器右边界:

    white-space 值 行为 渲染结果(视觉上) 复制时带空格吗?
    normal / pre-line 移除 空格彻底消失,相当于删掉了 ❌ 不带
    pre-wrap 挂起 空格看不见也占不到宽度,但它在源码里 (粘贴出来会有空格)
    break-spaces 换行 空格强制占宽,如果空间不够,空格自己会折到下一行开头 ✅ 带

    为什么会有“挂起”这种奇怪操作?

    这是 CSS 规范为了解决一个经典矛盾而设计的:

    • 矛盾:在 <pre>pre-wrap 模式下,代码里的所有空格必须“保留原样”(为了数据准确)。
    • 问题:行尾空格通常是程序员无意中敲出来的垃圾字符。如果程序员在代码行末不小心敲了个空格,比如 const a = 1;(分号后面有个空格)。如果按照“保留”逻辑,这个空格会强行撑宽容器,导致出现横向滚动条,非常难看。
    • 解法(挂起):浏览器就把这个行末空格“挂”在边界外面。这样,程序员复制代码时,空格还在(保证代码执行不出错);但页面排版时,容器没有被撑开(保证界面美观)
  • 这里没有说行首空格怎么操作,但是实际行首空格要么是保留,要么是移除。根据 CSS 规范,行首空格的处理规则其实隐藏在 “空格和制表符” 这一列的逻辑里。 “空格和制表符” 如果是合并行首空格就是移除, 如果是保留行首空格就是原样保留占宽

例子

先看下面的例子,用 <br> 换行,写成两行

1
2
3
4
<p>
表 1.1 所有性状显著位点和候选基因数目统计表<br>
Table 1.1 Summary statistics of significant loci and candidate genes across all traits
</p>

解析一下,具体哪里会产生额外的空格?

首先,按照折叠与转换的规则

  1. <p> 和 表 之间:换行符 + 缩进(空格)会变成一个前导空格,渲染为“ 表 1.1…” (表前面有个空格)。
  2. <br>和 Table 之间:换行符 + 缩进会变成一个前导空格,渲染为第二行开头有个空格,即“ Table 1.1…”(这是最明显、最不好看的地方)。
  3. traits 和 </p> 之间:换行符会变成一个尾随空格,虽然肉眼看不见,但复制文本时末尾会多一个空格。

之后进行修剪,会移除这两行开头和结尾的空白字符,因此实际效果没有额外的空格。

1
2
表 1.1 所有性状显著位点和候选基因数目统计表
Table 1.1 Summary statistics of significant loci and candidate genes across all traits

参考

  1. https://developer.mozilla.org/zh-CN/docs/Web/CSS/Guides/Text/Whitespace
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2019-2026 Vincere Zhou
  • 访问人数: | 浏览次数:

请我喝杯茶吧~

支付宝
微信