一文总结 CSS 中的换行问题
最近写业务,同时开发桌面端网页和 App webview 页面,经常被各种文字溢出和省略折磨不堪,每次都要翻一下 MDN 才明白。这里总结下关于换行和省略的所有 CSS 属性。
处理文字换行
先来说一下在通常元素里,CSS 默认对文字的换行方式:
- 连续的空格会合并,html中输入N个连续的空格都会变成一个,除非用
占位;- 换行符会当作一个空格处理,文本的换行基于父元素宽度自动进行软换行;
- 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
:无视上面提到的换行规则,每个字符都可以进行换行。
可以看到上面的属性大概划分了四个分级:无视换行、松散、普通、严格。那么这个严格到底有多严呢?
- 对于日语中的小假名,比如“じゃ”,以及片假名中长音符,如“ケーキ”,禁止在此处断行。
- 禁止在连接符中换行:“~”和“=”,因为“=”在日语中常用作片假名外国人名中间的连接符。
可见这个规则对于中文来说不是非常敏感,通常使用默认值即可,或者使用 anywhere
来让文本自由换行。
5. hyphens
这个属性比较少见,因为要使用这个属性需要配合两个占位符 &hyphens;
(U+2010) 和 ­
(U+00AD),这两个占位符的作用是暗示浏览器单词在这里进行换行。
manaul
:如果出现换行的占位符,才会进行换行。其中,&hyphens;
会进行强制的换行,而­
换行必须在浏览器发现行剩余空间不足时才会在此处换行。auto
:浏览器自动进行换行,这取决于换行元素的lang
属性,如果换行元素没有那么就会查看<html>
元素。none
:无视换行占位符。
这个属性通常不需要额外配置,默认值即可满足大部分场景。
总结
通常开发会遇到几个问题:
- 容器宽度固定,文本内容太长,这时候通常设置
workd-break: break-all
来让英文字符也可以换行(例如url)。 - 中文内容太长,可能需要标点内容也换行,这是可以设置
line-break: anywhere
来让排版更紧凑。 - 需要显示原文的排版格式(例如换行符等),可以设置
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;
}
这样就解决了文本“超出”剩余空间时没有省略的问题。