一、背景
对于正文页的对齐方式和排版,主流 app 为兼容长字符的英文单词和数字在两端对齐情况下可能产生的间隙过大的问题, 采用了左对齐 + 单词不截断的排版方式。
应用 | 对齐方式 | 截断方式 | 英文截断方式 | 链接/标的/话题等 tag 标签截断方式 |
---|---|---|---|---|
雪球 | 两端对齐 | break word | 不截断 | 可以截断 |
豆瓣 | 左对齐 | break word | 不截断 | 可以截断 |
微博 | 左对齐 | break word | 不截断 | 可以截断 |
知乎 | 左对齐 | break word | 不截断 | 可以截断 |
ZAKER | 左对齐 | break word | 不截断 | 可以截断 |
雪球最早也采用了这种左对齐的排版方式,但这带来了一个问题“右侧显示层次不齐,左侧视觉偏重”。在中文语境下,两端对齐左右对仗工整,两侧不会有多余空隙,可以保证段落的整齐划一,成规整的块状,相比左对齐阅读体验更佳,更舒适,为此雪球演进为两端对齐。
采用两端对齐后,由于单词仍是不截断完整展示,对于较长字符的英文单词/数字,就会出现字符空隙过大的情况。这就产生了一个技术上的取舍问题:单词不截断 还是 两端对齐 ?
大家可能会想到,如果像微信公众号把排版的编辑权限完全开放给用户(用户主导文章的对齐方式、单词的截断规则、页面的排版细节),文章完全交由用户控制,就不会出现细节上不可控的情况。伴随着文章排版控制权的转移,原本属于开发侧的工作也转移给了用户。这种实现方式并非一劳永逸的按钮式设置操作,由于每篇文章内容全然不同,必须因文制宜,用户需要对每篇即将发表的文章进行排版的设置。这看似完美的解决了排版问题,但从大多数雪球非专职运营的创作者角度来看,这无形中从“为”用户配置转变成了“让”用户配置,提高了创作门槛,增加了创作成本。
那有没有既保证了符合视觉审美的阅读体验,又能使用户无感知无操作完全从开发层面解决排版问题,又可以兼容长字符英文和数字的最优方案呢?
二、优化方案:
本次优化的预期是在保证两端对齐的基础上,最大限度的兼容外文及数字的展示。
备选截断方案
- hyphens: auto:使用连字符,浏览器根据算法来决定在哪处(适合的断字点)截断单词
- hyphens: manual:使用连字符,配合在 ­ / <wbr> 出现连字符或断字换行
- word-break: break-all:英文单词/数字截断,对于 non-CJK ( CJK 指中文/日文/韩文) 文本,可在任意字符间断行。
- word-break: break-word:英文单词/数字完整展示不截断。在行尾处遇到长单词,浏览器会新起一行来放置长单词,新的行还是放不下这个长单词则会对长单词进行强制断句。
备选方案选择
方案一和方案二(使用 hyphens)通过增加连字符的方式进行单词截断,阅读体验更佳,也能最大程度减少由于长单词/数字出现导致的字符间距过大的情况出现。
方案三(word-break: break-all)直接截断单词,过于简单粗暴,如果遇到帖子内大篇幅内容为英文时,阅读起来会不连贯,但是可作为兜底选项。(目前社区类 app 基本没有单词直接截断的例子)。
方案四(word-break: break-word)可以在左右对齐的基础上实现单词换行,但是会出现文字间缝隙过大的副作用(也是本次优化主要解决的问题),所以排除方案四。
由于 hyphens: auto 的实现成本较低,所以初步选择方案一来进行本次优化。
关于 hyphens
hyphens 属性告知浏览器在换行时如何使用连字符连接单词。也可以控制浏览器什么时候使用(manual),或者让浏览器决定什么时候使用(auto)。
其断字规则由语言决定,因此需要通过在 HTML 中指定lang
属性,来告诉浏览器使用哪种语言。设置文本语言有益于自动翻译工具、屏幕阅读器和其他辅助软件,无论是否使用断字,这种方式都是所有 Web 页面的最佳实践。
<html lang="en">
在 css 中设置 hyphens: auto,就打开了自动断字的功能。目前 Safari 和 IE/Edge 都需要前缀,所以需要在属性前面加对应的前缀。
如果使用连字符连接短单词,连字符之前的行上留下太少的字符或连字符之后仅有一个字符,这样的阅读体验很糟糕,我们可以使用断字 hyphenate-limit-chars
属性设置这些限制。为了更好的阅读体验,我们也可以使用hyphenate-limit-lines
属性限制连字符连续出现的行数。
.content {
-webkit-hyphens: auto;
-webkit-hyphenate-limit-before: 3;
-webkit-hyphenate-limit-after: 2;
-webkit-hyphenate-limit-chars: 6 3 2;
-webkit-hyphenate-limit-lines: 2;
-moz-hyphens: auto;
-moz-hyphenate-limit-chars: 6 3 2;
-moz-hyphenate-limit-lines: 2;
-ms-hyphens: auto;
-ms-hyphenate-limit-chars: 6 3 2;
-ms-hyphenate-limit-lines: 2;
hyphens: auto;
hyphenate-limit-chars: 6 3 2;
hyphenate-limit-lines: 2;
line-break: normal;
}
// hyphenate-limit-chars: 6 3 2;
// 只允许至少有 6 个字母长的单词用连字符连接,
// 在单词断开之前至少留下 3 个字符,并在下一行至少保留 2 个字符。
// hyphenate-limit-lines: 2;
// 限制行中连字符的行数,限制连字符不能连续出现两行以上。
方案一:hyphens: auto
在使用 hyphens: auto 优化后发现有两处明显问题,如下:
问题一:
对于文章内包含字符较长的数字,浏览器会按照不折断单词的原则来处理,但是由于数字不是单词,浏览器也不会在长串的数字内插入连字符,如下图(左)
问题二:
在段落的末尾倒数第一行和倒数第二行如果有长单词需要折行,是否展示连字符由浏览器计算决定。无法手动干预(hyphenate-limit-last: always; 该方法只针对 IE/Edge 生效),如下图(右)
解决方案
从后端拿到 htmlString 后,将 html 结构循环遍历至文本节点,用文本节点匹配出符合规则的数字结构(过滤规则:包含一个及以上的数字和非中文字符组成的无空格的文本),将匹配到的数字结构创建一个文本节点并且设置 class 类名为 numBreak (样式:word-break: break-all) ,允许数字结构的文本节点截断展示,其他文本节点遵循 hyphens:auto 的规则展示。
方案优势:
- 浏览器可以自由地在适当的断字点自动断词,断词处由浏览器手动控制。
- 可以在保证两端对齐的基础使用 hyphenate 自定义一些属性,控制连字符出现频率。
方案副作用及局限:
- 由于在一些文本节点中使用了 word-break: break-all,若该文本节点处于行末,那该行的 line-break:normal(标点回行策略)设置将会失效。
- 问题二无法解决,因为 hyphens: auto 是浏览器控制连字符出现位置,无法人为改动。
- 由于标的展示较为特殊,为达到预期效果的完全截断展示,需要在标的设置 word-break: break-all,作用于标的的 line-break:normal(标点回行策略)设置将会失效。
由于方案一的兼容方式会导致出现 line-break 失效的问题,而且不能彻底解决问题二,所以我们尝试采取方案二进行优化,以下着重分析方案二。
方案二:hyphens: manual
使用软连字符和换行符进行手动插入及换行(hyphens: manual; 配合 ­ <wbr> )
- 从后端拿到 htmlString 后,将 html 结构循环遍历至文本节点。
- 英文处理:用文本节点匹配出符合规则的英文结构(过滤规则:包含三个及以上的纯英文字母的组合),将匹配到的英文组合,除最后一位字母,每个字母后插入 ­ 软连字符,因为屏幕宽度不同必须保证在任何屏幕宽度任意字母在行末截断的情况下展示出连字符。手动实现了 hyphenate-limit-chars,规则是必须4个字母及以上,连字符前不可以少于两个字母,连字符后不能少于两个字母。
- 数字处理:用文本节点匹配出符合规则的数字结构(过滤规则:包含一个及以上的数字和非中文字符组成的无空格的文本)设置 hyphens:none(避免数字过长被强制赋上连字符),将匹配到的数字结构首个字符后加
,剩余字符每六个字符为一组,创建一个文本节点并且插入 换行符,允许数字结构的文本节点截断回行展示。
方案优势:
- 连字符位置手动控制,可以灵活的使用换行符,在任意位置插入换行符,突破了浏览器被动计算给出位置的局限性
- 可以用自定义的换行截断算法代替 word-break:break-all,灵活度更高,更好的兼容了标点符号回行的策略,日后可以不断完善算法,更易拓展。
- 由于连字符由代码手动控制,方案一中可能出现的问题一和问题二都得以解决。
方案副作用:
- 由于标的展示较为特殊,为达到预期效果的完全截断展示,需要在标的设置 word-break: break-all,作用于标的的 line-break:normal(标点回行策略)设置将会失效。标的内标点较少,且不具备传达完整语句的功能,影响可控。
方案性能统计
场景 | ios 平均耗时(iphone xs max 取三次平均时间) | android(版本:12)平均时长(oppo realme 取三次平均时间) | android 低端机型(版本:8.1)平均时长(Redmi 5 Plus 取三次平均时间) |
---|---|---|---|
数字较多长贴 | 7.6ms | 18ms | 45ms |
英文较多长贴 | 6ms | 9.6ms | 20.3ms |
英文数字混合长贴 | 11ms | 9.6ms | 19.3ms |
篇幅较长长贴 | 8.6ms | 15.3ms | 40ms |
篇幅较短长贴 | 5.6ms | 4.6ms | 10.3ms |
普通长贴 | 7ms | 3ms | 9.6ms |
综合平均耗时 | 7.6ms | 10ms | 24ms |
综合来看,在 IOS 和 Android 中等及以上机型替换文本耗时均能控制在 10ms 左右,在安卓低端机型替换文本耗时平均在 20ms 左右,没有性能问题。
三、最终效果对比
以下是对同一篇帖子进行优化前(左图)与优化后(右图)的效果对比展示。