跳到主要内容

一文总结 CSS 中的换行问题

· 阅读需 13 分钟

最近写业务,同时开发桌面端网页和 App webview 页面,经常被各种文字溢出和省略折磨不堪,每次都要翻一下 MDN 才明白。这里总结下关于换行和省略的所有 CSS 属性。

处理文字换行

先来说一下在通常元素里,CSS 默认对文字的换行方式:

  1. 连续的空格会合并,html中输入N个连续的空格都会变成一个,除非用   占位;
  2. 换行符会当作一个空格处理,文本的换行基于父元素宽度自动进行软换行;
  3. CJK(Chinese、Japanese、Korean)字符间可换行,非CJK字符(如英语),不在单词间断开,保持完整

可以看到默认的处理方式,不能兼顾到所有的使用场景:有时候英语单词可以用连接线来在单词间断行,有时候又需要换行符保持原文的格式。因此 CSS 有许多属性来支持这些文本格式的处理。

1. white-space

white-space属性用来控制上面的1、2条内容,即空白符和换行符如何处理,以及在填充行内元素时是否进行自适应的换行。

  • normal:使用默认的规则,文本间会进行自适应的软换行,来让文本不要超出父元素宽度。
  • nowrap: 使用默认的规则,但是文本不会软换行,这可能让文本超出父元素。
  • pre: 连续的空白符会合并,但是换行由文本内的换行符和<br>决定。
  • pre-wrap: 同 pre,但是也会进行软换行。
  • pre-line: 同 pre-line,但是连续的空白符不会合并。
  • break-space: 类似 pre-wrap,但是行尾空格会保留,是新加的属性,可能会有兼容问题。

MDN上总结了一个表格,可以具体查看:MDN上的white-space

2. overflow-wrap

这个属性仅用来控制一个长单词无法填充容器时,是否进行断行而防止溢出。word-wrap是和他不同名的相同属性,建议使用overflow-wrap

  • normal:默认设置,不考虑断行。
  • anywhere:如果行内没有多余空位容纳长单词,那么就让长单词断开并强制换行。在计算内容的最小大小时,会考虑单词中断引入的断行机会,可能会导致内容宽度变小。
  • break-wrod:类似 anywhere,但是不考虑单词中断引入的断行机会,有利于保持容器宽度。

值得注意的是,如果一个单词因断行被切开,并不会添加连字符,需要其他属性控制。

3. word-break

word-break 主要处理上面的问题3中,单词内部如何进行断行的问题。

  • normal: 默认规则,CJK字符间可换行。
  • keep-all:CJK字符不断行,非CJK字符表现同normal。
  • break-all:非CJK字符间也可以断行。
  • break-wrod: 相当于同时设置 word-break:normal 和 overflow-wrap: anywhere,仅当单词过长时考虑切开单词断行。

4. line-break

这个属性用来控制CJK字符的换行规则,这位更是重量级选手,因为MDN上都没有说清楚几个值具体的规则是什么,就给了几个demo让读者去看,想要搞懂这个属性可能还要补个语文课。

高考作文的纸是画好格子的,实际上我们平时写字不会在格子里写,一般都是横线上,如果只写汉字的话,其实也并没有太多规则,写不下就换行,但是有标点后,性质就不太一样了。

首先是常用的句尾标点,如逗号、句号、感叹号、问号等等,这些必须跟在汉字的后面,不能另起一行,称之为“避头标点”;其次左引号、左括号作为内容的开头,不能放在一行的结束,称之为“避尾标点”。 然后,“~”这样的连字符,一般也不会出现在一行开头。line-break这个属性就是控制这些情况下是否换行的问题。

在日语里也有类似的情况,如“日々”、“じゃ”这些内容是否可以在中间断开。我没有学习过韩语,但是韩语也应该会有类似的情况。下面来介绍具体的属性:

  • auto: 自动确定换行规则,比如可能对短行采用松散的换行规则,这是默认值。
  • loose:使用松散的换行规则,主要用于短行。
  • normal:使用常用的换行规则,如上文提到的中文换行规则,尽量保证标点的避头或避尾。
  • strict:使用最严格的换行规则。
  • anywhere:无视上面提到的换行规则,每个字符都可以进行换行。

可以看到上面的属性大概划分了四个分级:无视换行、松散、普通、严格。那么这个严格到底有多严呢?

  1. 对于日语中的小假名,比如“じゃ”,以及片假名中长音符,如“ケーキ”,禁止在此处断行。
  2. 禁止在连接符中换行:“~”和“=”,因为“=”在日语中常用作片假名外国人名中间的连接符。

可见这个规则对于中文来说不是非常敏感,通常使用默认值即可,或者使用 anywhere 来让文本自由换行。

5. hyphens

这个属性比较少见,因为要使用这个属性需要配合两个占位符 &hyphens;(U+2010) 和 &shy; (U+00AD),这两个占位符的作用是暗示浏览器单词在这里进行换行。

  • manaul:如果出现换行的占位符,才会进行换行。其中,&hyphens;会进行强制的换行,而 &shy;换行必须在浏览器发现行剩余空间不足时才会在此处换行。
  • auto:浏览器自动进行换行,这取决于换行元素的lang属性,如果换行元素没有那么就会查看 <html> 元素。
  • none:无视换行占位符。

这个属性通常不需要额外配置,默认值即可满足大部分场景。

总结

通常开发会遇到几个问题:

  1. 容器宽度固定,文本内容太长,这时候通常设置 workd-break: break-all 来让英文字符也可以换行(例如url)。
  2. 中文内容太长,可能需要标点内容也换行,这是可以设置 line-break: anywhere 来让排版更紧凑。
  3. 需要显示原文的排版格式(例如换行符等),可以设置 white-space: pre-wrap 等来让排版考虑到换行符。

2. 省略

有时候为了排版,我们不希望文本全部展示,例如卡片上的标题不超过一行,但是如果超出的话又需要显示省略号,这种情况下需要使用到text-overflow这个属性, 用来控制如何提示用户文本有换行或者截断。

  • clip:默认值,如果文本超出了容器宽度又不能换行,那么直接在容器边缘截断,无视字符的完整与否。
  • ellipsis:如果文本超出,那么用省略号来表示被截断的文本。
  • fade:使用一个淡出的效果来提示文本被截断。
  • 任意字符串,用这个字符串来代替超出的文本。这个仅在 Firefox 支持,所以不要用在生产环境或做好兼容。

如果我们想要显示省略号,那么需要这样做:

.ellipsis {
overflow: hidden; /* 溢出文本不显示 */
white-space: nowrap; /* 文本不要换行 */
text-overflow: ellipsis; /* 显示省略号 */
}

踩坑

对于大部分的使用场景,这样写足以满足使用。不过在具体业务里遇到了一个情况,溢出省略号没有生效。场景是卡片首行需要展示标题和tag,左边标题,右边是tag和一个checkbox,但是标题内容仅展示一行,tag内容由数据取出,可能没有。

很快啊,这个我就写出来了:

// tsx file
<div class="wrapper">
<div class="title">{data.title}</div>
<div class="right">
{data.tag ? (<div class="tag">{data.tag}</div> : null)}
<CheckBox />
</div>
</div>
.wrapper {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}

.title {
flex: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

.right {
flex: 0;
}

然后 QA 打回来了:在标题过长的情况下,文本没有省略,反而超出了宽度,导致checkbox在外边了,最要命的是,外层的容器还是 overflow: hidden,没法交互……

研究了一下,发现问题其实出现在 flex 属性,具体就是 flex-grow 上。让我们来看看 MDN 是怎么解释这个属性的:

这个属性规定了 flex-grow 项在 flex 容器中分配剩余空间的相对比例。 主尺寸是项的宽度或高度,这取决于flex-direction值。
This property specifies how much of the remaining space in the flex container should be assigned to the item (the flex grow factor).

问题就出在这个剩余空间上。通常我们使用 flex-grow 的场景,都是子元素的实际尺寸小于容器的,但是这个情况下,其实子元素的宽度要比容器宽度去掉右边tag的宽度要小的,这时候 flex-grow 已经无效了, 因为它只能让元素去占据剩余的空间。可是 flex-shirk 也不是被指定了吗?flex-shirk 其实在这个场景下失效了:容器的宽度是 100% 而不是一个具体值,因此在子元素宽度不够的情况下,容器宽度自动扩大了。

解决这个问题的办法,其实就是让 flex-grow 能自动地占据剩余宽度,但是title元素的宽度又不能大于剩余的宽度,那其实就只有一个思路了:设置title的宽度为0,这样在计算时,由于title的初始宽度为0, 肯定不够容器的剩余宽度,因此才会自动占据剩余宽度,而不是宽度大于剩余宽度导致 flex-grow 失效。

.title {
flex: 1;
width: 0;
min-width: 0; /* 实际上基于规范,flex子项宽度计算的是min-width,大部分浏览器写 width 也能控制 */
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}

这样就解决了文本“超出”剩余空间时没有省略的问题。