随着计算机快速发展,现代的应用程序也足够聪明,除了实现打字机那些基本功能之外,还提供了一些具有对其属性的制表位(比如 Microsoft Word 等),动态制表位、补全(Tab completion)等能力。比如,代码编辑器中 Code Snippet 的占位符可通过 Tab 键快速切换,就是利用了动态制表位实现的。
这三个空白符可称为 document white space characters,本文称作「文档空白符」。
但在 HTML 中表示 Newlines(换行)的字符包含换行符(LF, U+000A)、回车符(CR, U+000D)以及成对的 CR+LF。
那么,为什么 CSS 不处理回车符(CR, U+000D)呢?
首先,浏览器渲染 HTML 的流程大致包括:HTML 解析得到 DOM Tree,CSS 解析得到 CSSOM Tree,然后 DOM Tree 和 CSSOM Tree 结合生成 Render Tree,接着 Layout 阶段将 Render Tree 的所有节点分配空间确定坐标,最后 Painting 阶段再将所有阶段绘制出来。而 CSS 起作用发生在解析之后,因此 CSS 并不直接作用于 HTML,而是 DOM Tree。
其次,在 HTML 解析为 DOM Tree 的过程中,每个换行序列字符(newline sequence)都会做规范化处理为 Line Feed(U+000A),此时 Line Feed 被称为 segment break。
To normalize newlines in a string, replace every U+000D CR U+000A LF code point pair with a single U+000A LF code point, and then replace every remaining U+000D CR code point with a U+000A LF code point.
若 white-space 设为 normal、nowrap 或 pre-line,那么文档空白符被认为是可折叠(collapsible)的。这种折叠现象可以称为空白符折叠(white space collapsing)。在空白处理过程中,没有被移除或折叠而保留下来的空白符,称为保留空白符(preserved white space)。
white-space: normal 就像循规蹈矩的倔牛,它不管你是否有显示指定换行符(Line Feed),它只会在该换行的时候(有软换行机会)才进行换行,比如块容器空间不够了,而且有软换行机会的时候才进行换行。同时为了排版更美观,它会将连续的空白符折叠成一个。但为了满足各种各样的排版需求,才有了其他值以方便换行、或者保留多个空格等。
在断行中允许在单词内进行断行。具体而言,除了 normal 的软换行机会外,任何排版字母单元(以及解析为 NU(Numeric)、AL(Alphabetic)或 SA(South East Asian)断行类别的任何排版字符单元)都被视为表意字符(ideographic characters, ID)用于断行的目的。不会应用连字符化。
keep-all
在单词内禁止断行。除此之外,该选项与 normal 情况相同。在这种样式中,连续的 CJK(中文、日文和韩文的统称)字符序列不会断行。比如:这是一些汉字 and some Latin 的换行机会在 这是一些汉字·and·some·Latin(用 · 表示)
<a href="https://prettier.io/">Prettier is an opinionated code formatter.</a>
由于 printWidth 的存在,它有可能会被格式化为:
<a href="https://prettier.io/">
Prettier is an opinionated code formatter.
</a>
这样页面呈现的链接左右可能会出现空格。
htmlWhitespaceSensitivity 提供了三个可选值:
css:遵循 CSS 中 display 属性的默认值。
strict:认为所有空白符都是重要的。
ignore:认为所有空白符都不重要。
<!-- <span> is an inline element, <div> is a block element -->
<!-- input -->
<span class="dolorum atque aspernatur">Est molestiae sunt facilis qui rem.</span>
<div class="voluptatem architecto at">Architecto rerum architecto incidunt sint.</div>
<!-- output -->
<span class="dolorum atque aspernatur">
Est molestiae sunt facilis qui rem.
</span>
<div class="voluptatem architecto at">
Architecto rerum architecto incidunt sint.
</div>
JSX removes whitespace at the beginning and ending of a line. It also removes blank lines. New lines adjacent to tags are removed; new lines that occur in the middle of string literals are condensed into a single space.
前言
作为 Web 前端开发,你一定听到过类似「HTML 会将多个空格合并为一个」的说法,你有深入了解过它是如何折叠(合并)的吗?
我们平常编写的源码文件,通常会包含与最终呈现无关的格式。比如,项目采用 Space 或 Tab 的缩进风格,里面可能包含了空格、制表符、换行符等。浏览器在渲染源文件的时候,会根据一定的规则处理(保留或折叠)这些字符,开发者也可以通过 CSS 属性(
white-space
)控制其渲染规则。在此之前,我们要充分了解空白符是什么,有哪些?
空白符
比如空格、制表符、换行符等。空白符在各类编程语言中可作为分割 Token 的标识。
 
\t
	
\n
\v

\f

\r
其中垂直制表符、分页符可能少见一些。在 ASCII 编码也没有专门的字符来表示垂直制表符。
在正则表达式中,可通过
\s
来匹配一个除普通空格之外的空白符(也就是\n
、\r
、\f
、\t
、\v
),相当于/[\n\r\f\t\v]/
。空格
通常,我们会使用键盘上的 Space 键输入空格(也就是普通空格)。
除此之外,表示空格的字符还有很多种。
&32;
&npsp;
 
 
 
​
‍
‌
在 HTML 中可发生折叠的空格,也只有普通空格才会。
回车符与换行符
可能还有一部分同学还分不清「回车」和「换行」区别的。
随着计算机的快速发展和普及,大家常说的「换行」或「回车」操作所表示的意思,相比之前,其实早就发生了变化。
关于「回车」和「换行」的词源,它们源起自打字机,后应用于计算机。在打字机中概念如下:
在早期,机械打字机处理字符的效率很低,为了避免丢失字符,须先执行
CR
操作,再执行LF
操作。现代意义上的换行相当于打字机中「回车 + 换行」的组合。计算机中的两种换行符
很多同学都知道,目前主流操作系统中表示换行的字符,有
\r\n
和\n
两种。原因是当时存储非常昂贵,有些人认为用CR+LF
两个字符表示换行过于浪费,因此产生了分歧。\r\n
\n
\r
既然存在两种换行符,对我们会产生什么影响?
如果软件层面没做好适配,Windows 文件在 Unix/Unix-like 操作系统上打开可能会变成一行。而 Unix/Unix-like 文件在 Windows 操作系统上打开,每行结尾可能会多一个
^M
符号。但基本不用担心,已经 2023 年了,大多数软件都可以很好地兼容两种换行符。对于开发者来说,很多 IDE/Editor 都可以设置默认换行符的,也有各种 Linter 工具(比如 EditorConfig)自动处理。通常,更多人选择将默认换行符设为
LF
,也就是\n
。制表符
制表符就是键盘上 Tab 键输入的字符,用于将光标前进到下一个制表位(Tab stop)。
它同样源自打字机,在当时如果要打出一个表格等内容时,需要使用大量的空格键等,为此人们发明了一个类似现代的 Tab 键的东西,按下该键可以前进到下一个制表位。
随着计算机快速发展,现代的应用程序也足够聪明,除了实现打字机那些基本功能之外,还提供了一些具有对其属性的制表位(比如 Microsoft Word 等),动态制表位、补全(Tab completion)等能力。比如,代码编辑器中 Code Snippet 的占位符可通过 Tab 键快速切换,就是利用了动态制表位实现的。
平常写代码过程中,按下回车键快速切换至下一行指定位置,相当于「CR + LF + Tab」。如果项目中指定了类似
useTab=false, tabSize=2
的风格要求,在按下 Tab 键时,应用程序会根据配置替换为指定数量的空格实现快速对齐效果(取决于应用程序的实现)。通常 Tab 键产生的字符为水平制表符(Horizontal tabulation, HT),除此之外,还有垂直制表符(Vertical tabulation, VT),用于打印或者 Word 排版等场景。
\t
	
\v

有些标准中垂直制表符也称为 Line tabbulation。
HTML 实体
在 HTML 中有些字符是预留的。比如小于号
<
和大于号>
,如果在源码中直接键入,会被浏览器误认为它们是标签。为了正确地显示保留字符、难以用键盘输入的特殊字符,需要用 HTML 实体表示。比如,小于号
<
,其实体名称是lt
,实体数字为60
(对应的 ASCII 编码值),所以它有<
、<
、<
三种表示方法。以下列出对应的 HTML 实体:
 
 
 
 
 
 
 
 
​
​
‌
‌
‍
‍
	
	


HTML 实体编码在页面上展示取决于页面中所设字符集。常见的页面乱码,就是因为字符集不一致导致的。
相关链接:
CSS 空白符处理规则
写在前面
在 CSS 中,规则指出,CSS 的空白处理仅影响以下三种空白符:
这三个空白符可称为 document white space characters,本文称作「文档空白符」。
但在 HTML 中表示 Newlines(换行)的字符包含换行符(LF, U+000A)、回车符(CR, U+000D)以及成对的 CR+LF。
那么,为什么 CSS 不处理回车符(CR, U+000D)呢?
首先,浏览器渲染 HTML 的流程大致包括:HTML 解析得到 DOM Tree,CSS 解析得到 CSSOM Tree,然后 DOM Tree 和 CSSOM Tree 结合生成 Render Tree,接着 Layout 阶段将 Render Tree 的所有节点分配空间确定坐标,最后 Painting 阶段再将所有阶段绘制出来。而 CSS 起作用发生在解析之后,因此 CSS 并不直接作用于 HTML,而是 DOM Tree。
其次,在 HTML 解析为 DOM Tree 的过程中,每个换行序列字符(newline sequence)都会做规范化处理为 Line Feed(U+000A),此时 Line Feed 被称为 segment break。
也就是在规划化处理时,先将成对的 U+000D CR U+000A LF 替换为一个 U+000A LF,然后将剩余的 U+000D CR 也替换为一个 U+000A LF。因此对 CSS 而言,它压根感受不到 U+000D 的存在。
相关链接:
white-space 属性
用于控制空白处理的 CSS 属性是
white-space
,其默认值是normal
。该属性指定了两件事:
若
white-space
设为normal
、nowrap
或pre-line
,那么文档空白符被认为是可折叠(collapsible)的。这种折叠现象可以称为空白符折叠(white space collapsing)。在空白处理过程中,没有被移除或折叠而保留下来的空白符,称为保留空白符(preserved white space)。normal
默认值。该值指示浏览器将(多个)空白序列折叠为一个单个字符(有些情况下,没有字符)。允许存在软换行机会的位置换行。
pre
意为保留(preserved)。该值防止浏览器折叠空白序列。Line Feed(U+000A)被保留为 forced line breaks,当且仅当为强制换行符时,才发生换行。当容器的宽度无法满足内容时,内容会发生溢出。
nowrap
意为不换行。该值像
normal
一样会折叠空白序列,但又像pre
那样,不允许换行。pre-wrap
意为保留换行。像
pre
那样保留空白序列,又像normal
那样允许换行。break-spaces
该值与
pre-wrap
行为一致,除了:pre-line
该值与
normal
一样,此值折叠连续的空白字符,并允许换行,但同时保留源码中的 Line Feed 作为 forced line breaks。空白处理规则
分为三个阶段:
以下三个阶段中提到的 Line Feed,在规范中时用 segment break 表示的。如果 Document language 未定义 segment break 或 newline sequence 的话,那么文本中的 Line Feed(U+000A)被视为 segment break。因此下面才直接称为 Line Feed。
折叠与转换(Collapsing and Transformation)
对于内联格式上下文(Inline formatting contexts, IFC)中的每个内联元素,空白序列在换行和双向重新排序(针对诸如阿拉伯语等从右到左书写的语言)之前,按如下方式进行处理:
如果
white-space
设为normal
、nowrap
或pre-line
,空白序列被认为是可折叠的,并通过一下步骤处理:如果
white-space
设为pre
、pre-wrap
或break-spaces
,任何 Space 被视为 Non-breaking space(U+00A0)。white-space: pre-wrap
,在一系列的 Space 或 Tab 的末尾存在软换行机会。white-space: break-spaces
,每个 Space 和每个 Tab 之后都存在软换行机会。如何理解内联格式上下文中的每个内联元素(包含匿名内联元素)?
<p>
是一个块级元素,里面包含了 5 个内联元素,其中 3 个是匿名的:Several
emphasized words
appear
in this
sentence, dear.
裁剪与定位(Trimming and Positioning)
接着,整个块(block)被渲染。内联元素(inlines)按照双向重排的规则进行布局,并根据
white-space
属性指定的方式进行换行。在逐行布局的过程中:tab-size
为0
,保留的 Tab 不进行渲染。否则,每个保留的 Tab 都呈现为水平移动,使得下一个字形的起始边缘与下一个制表位(Tab Stop)对齐。white-space
为normal
、nowrap
或pre-line
,那么也会将该字符移除。white-space
为normal
、nowrap
或pre-line
,浏览器必须(无条件地)挂起(hang)该字符。white-space
为pre-wrap
,浏览器必须挂起该字符,除非该字符后跟了一个 forced line break,这时浏览器必须(有条件地)挂起该字符。此外,浏览器还可以在该字符的宽度超出限制时,将其字符宽度进行视觉上的折叠。white-space
为break-spaces
,Space、Tab 和 other space separators 被视为与其他可见字符相同,它们不能被挂起,也不能折叠其前进宽度。换行转换规则(Segment Break Transformation Rules)
当
white-space
为pre
、pre-wrap
、break-spaces
或pre-line
时,Line Feed 不可折叠,而是转换为保留的 Line Feed(preserved line feed, U+000A)。若
white-space
为其他值,Line Feed 是可折叠的,并按如下方式进行折叠:换行与单词边界
当换行仅在允许的换行点处执行,称为 soft wrap opportunity(软换行机会)。对于大多数书写系统来说,在没有使用连字的情况下,软换行只会在单词之间发生。对于使用空格或标点符号分隔单词的书写系统,软换行的位置可以通过这些字符来确定。
尽管 CSS 没有用于软换行机会的属性,但可以通过诸如
line-break
、work-break
、hyphens
、overflow-wrap
/work-wrap
属性去指定改变换行点。换行细节
在确定 line break 时:
line breaking 和双向文本的交互由 CSS Writing Modes 4 和 Unicode Bidirectional Algorithm 定义。
保留的 Line Feed、具有 BK(U+000C、U+000B、U+2028、U+2029)、NL(U+0085)类别的 Unicode 字符,必须视为 forced line breaks,且不受
white-space
属性的影响。除非另有明确定义(例如
line-break: anywhere
或overflow-wrap: anywhere
),否则必须遵守 WJ(U+2060、U+FEFF)、ZW(U+200B)、GL(U+00A0、U+202F、U+180E)和 ZWJ(U+200D)类别的 Unicode 字符的换行行为。对于使用标点符号作为分隔符的书写系统,浏览器可以允许在除了词分隔符之外的标点符号处进行换行,但在确定换行位置时应优先考虑换行点(breakpoint)的设置。例如,如果在
/
后面的换行点(breakpoint)优先级低于空格,那么在序列check /etc
中,将不会在/
和字母e
之间发生换行。脱离标准文档流的元素(out-of-flow elements)和行内元素的边界不会引入 forced line break 或 soft wrap opportunity,它们不会中断文本流的连续性。
为了 Web 兼容性,在每个替换元素(replaced element)或其他原子内联(atomic inline)前后都有一个软换行机会,即使与通常会抑制它们的字符相邻时也是如此,例如 Non-breaking space。
对于由在换行处消失的字符(例如 U+0020 Space)创建的软换行机会,直接包含该字符的盒子上的属性控制该机会的换行。对于由两个字符之间的边界定义的软换行机会,两个字符最近的共同祖先上的
white-space
属性控制换行;哪些元素的line-break
、word-break
和overflow-wrap
属性控制在此类边界处软换行机会的确定在 CSS Level 3 中未定义。对于盒子的第一个字符之前或最后一个字符之后的软包装机会,换行发生在盒子之前/之后(在其外边距边缘),而不是在其内容边缘和内容之间发生换行。
在
/
周围的 Ruby 文本中的换行行为在 CSS Ruby Annotation Layout 1 中定义。为了避免意外的溢出,在浏览器无法执行所需的词法或正字法分析(orthographic analysis)来进行需要换行的任何语言(例如由于缺乏某些语言的字典)时,它必须在该书写系统中的排版字母单元对之间假设存在软换行机会。
work-break 属性
此属性指定字母之间的软换行机会,即它是“正常”且允许换行的位置。
normal(默认值)
默认断行规则,像上面提到的 line break 那样,单词按照它们自己的习惯进行断行。
break-all
在断行中允许在单词内进行断行。具体而言,除了
normal
的软换行机会外,任何排版字母单元(以及解析为 NU(Numeric)、AL(Alphabetic)或 SA(South East Asian)断行类别的任何排版字符单元)都被视为表意字符(ideographic characters, ID)用于断行的目的。不会应用连字符化。keep-all
在单词内禁止断行。除此之外,该选项与
normal
情况相同。在这种样式中,连续的 CJK(中文、日文和韩文的统称)字符序列不会断行。比如:这是一些汉字 and some Latin
的换行机会在这是一些汉字·and·some·Latin
(用·
表示)line-break 属性
此属性指定元素内应用的换行规则的严格性:尤其是换行如何与标点符号(punctuation)和符号(symbols)交互。
auto (默认值)
浏览器确定要使用的断行限制集,并且它可以根据行的长度变化限制。例如,对于较短的行使用一组较少限制的断行规则。
loose
使用最宽松的断行规则来断开文本。通常在短行文本中使用,例如报纸。
normal
使用最常见的一组换行规则来打断文本。
strict
使用最严格的一组换行规则来断开文本。
anywhere
每个印刷字符单元周围都有一个软换行机会,包括任何标点符号周围或保留的空白符,或在单词中间,无视任何禁止换行符的规定,即使是那些由具有 GL、WJ 或 ZWJ 换行符类别的字符引入的或由
work-break
属性强制要求。不能优先考虑不同的换行机会。不应用连字符。请注意:
line-break: anywhere
只允许在white-space
设为break-spaces
时,将行末的保留空格换行到下一行,因为在其他情况下:white-space: normal
和white-space: pre-line
下,行末/行首的保留空格会被丢弃。white-space: nowrap
和white-space: pre
下,禁止换行。white-space: pre-wrap
下,保留空格会保持悬挂状态。当
line-break: anywhere
对保留空格产生影响时(在white-space: break-spaces
下),它允许在连续空格序列的第一个空格之前进行换行,而独立使用white-space: break-spaces
则不具备这个功能。overflow-wrap 和 word-wrap 属性
此属性指定浏览器是否可以在一行内不允许的位置换行以防止溢出,当 otherwise-unbreakable 的字符串太长而无法放入行盒子时。它仅在
white-space
允许换行时有效。normal(默认值)
行只能在允许的换行点处换行。然而,由于
word-break: keep-all
引入的限制可以放宽以匹配word-break: normal
如果行中没有 otherwise-acceptable 换行点。anywhere
如果该行中没有其他方面可接受的换行点,则可以在任意位置中断一个 otherwise-unbreakable 字符。
break-word
除了由
overflow-wrap: break-word
产生的软换行机会之外,在计算元素的最小内容固有尺寸时,不考虑其他任何软换行机会。行内元素标签之间的空白符
两个块级元素之间、块级元素与行内元素之间的普通空白符会被忽略。但行内元素之间的普通空白符就要注意了。
举个例子,我们可能会经常遇到类似的排版:
给每个
item
设为display: inline-block
,使得它既能像块级元素那样设置边距宽高等,又能像行内元素那样排列在一行中,兼容性还好。但问题却是行内元素之间有间隙,如下:原因很简单,我们在源码中输入了空格、换行符(或是格式化工具产生的),可以打印一下
list
元素的 HTML 结构:前导和尾随的普通空白符被忽略了,但中间的则被解析为普通空格,而普通空格占据的宽度又跟字体大小有关系,因此就产生了缝隙。
诸如外部元素设为
font-size: 0
、改变布局方式等,都能一定程度上解决问题,更多可看张鑫旭大佬的文章《去除 inline-block 元素间间距的 N 种方法》。问题的本质是行内元素之间的空白符是会被解析出来的,比如
<a>link1</a> <a>link2</a>
两个<a>
标签之间的空格是无法忽略的。那么彻底解决问题的方式是在源码中干掉空格,比如:虽然这种写法是「丑」的,可读性不好,但它才是终极解决方法。
难道我们开发的过程中真的要这样写?
并不是,我们现在享受着各种框架所带来的便利,各种编译、压缩工具横行,它们帮我们做了很多「脏活」,在编译出 HTML 或者动态更新 DOM 之前已经把标签之间的空格、换行符都去掉了。不信你可以用 React 等框架还原上述示例。
这也是只注重框架使用,而忽视基础知识的一个反映,它掩盖了一些细节,容易让使用者忽略掉了事物的本质。
htmlWhitespaceSensitivity
我们知道在 Prettier 中有一个
htmlWhitespaceSensitivity
选项,就是用来指定如何格式化 HTML 文件的。通过前面的内容,我们可以知道
1<b> 2 </b>3
将会被解析为1 2 3
(中间是有空格的),但如果我们本地的格式化程序将源码格式化为1<b>2</b>3
(文本的前导和尾随空格被移除),那么解析结果就是123
(中间没有空格),这可能是非预期行为。举个例子,以下元素不能安全地格式化:
由于
printWidth
的存在,它有可能会被格式化为:这样页面呈现的链接左右可能会出现空格。
htmlWhitespaceSensitivity
提供了三个可选值:css
:遵循 CSS 中display
属性的默认值。strict
:认为所有空白符都是重要的。ignore
:认为所有空白符都不重要。如果是
"htmlWhitespaceSensitivity": "css"
,格式化结果如下。即块级元素的前导和尾随空白符可以忽略,因此换行显示。而行内元素前导或尾随空白符是影响显示的不应忽略。如果是
"htmlWhitespaceSensitivity": "strict"
,格式化结果如下。如果是
"htmlWhitespaceSensitivity": "ignore"
,格式化结果如下。在全局指定处理方式之后,还支持在局部添加
<!-- display: block -->
来覆盖全局配置。更多请看 Whitespace-sensitive formatting。<pre>
与<code>
如果要使得前导、尾随或中间的普通空格等如源码般呈现在页面中,可以使用
<pre>
元素。该元素中的文本通常按照原文件中的编排,以等宽字体的形式展现出来,文本中的空白符(比如空格和换行符)都会显示出来。但紧跟在
<pre>
开始标签后的换行符会被省略。以上示例有普通空格、换行符、半角空格和全角空格等。在 Markdown 文档常用的 Code Block 也是用
<pre>
进行渲染的。类似的,还有一个
<code>
元素,表示呈现一段计算机代码。默认情况下,它以浏览器的默认等宽字体显示。但它与<pre>
不同的是,连续多个空白符仅算作一个。同样地,在 Markdown 中单行代码块就是用
<code>
(右键检查元素试试?)元素进行渲染的,但通常网站会给该元素设置背景颜色使得更加突出。React 如何表示换行
由于 JSX 在解析的时候,会将文本中的换行符转换为空格(详见)。
因此,以下几种写法的表现是一样的,并不会表现出换行,即使给设置上
white-space: pre-wrap
。解法一:
解法二:
解法二:
解法三:
References