记一次前端文本对齐的问题

前段时间处理了一个在网页中文本对齐的问题,发现了一些之前关于字体未曾了解的知识点,颇有意思,总结一下。

<!--more-->

1. 背景

业务中需要在网页中展示pandas读取excel后的输出内容

import pandas as pd
import sys

pd.set_option('display.unicode.ambiguous_as_wide', True)
pd.set_option('display.unicode.east_asian_width', True)

data = pd.read_excel("./销售订单.xlsx", sheet_name="订单数据")
# sys.stdout = open("1.log", "w") # 测试输出重定向
print(data)

控制台打印的效果十分完美

在浏览器中使用pre标签展示输出内容时,却发现文本完全没有像控制台那样对齐

下面是原始输出内容

<pre>             订单号  商品ID            商品名      品牌  类别     规格  单价  数量  总价            下单时间
0    98232019040002  700009        芹菜味薯片  开口哭牌  零食    200克    20     2    40 2019-04-01 08:00:15
1    98232019040003  700006  没有一点味口香糖  开口哭牌  零食    100克    15     2    30 2019-04-01 08:45:10
2    98232019040004  700013        火龙果可乐  君再来牌  饮料  550毫升     5     5    25 2019-04-01 10:03:38
3    98232019040005  700003      芹菜味口香糖  开口哭牌  零食    100克    15     1    15 2019-04-01 10:58:03
4    98232019040006  700009        芹菜味薯片  开口哭牌  零食    200克    20     1    20 2019-04-01 11:35:19
..              ...     ...               ...       ...   ...      ...   ...   ...   ...                 ...
300  98232019040302  700011        蟹黄味薯片  开口哭牌  零食    200克    20     5   100 2019-04-30 13:29:20
301  98232019040303  700016        夏夜雨可乐  君再来牌  饮料  550毫升     5     1     5 2019-04-30 14:05:13
302  98232019040304  700015        青草味可乐  君再来牌  饮料  550毫升     5     1     5 2019-04-30 14:45:06
303  98232019040305  700015        青草味可乐  君再来牌  饮料  550毫升     5     1     5 2019-04-30 17:46:43
304  98232019040306  700005      蟹黄味口香糖  开口哭牌  零食    100克    15     2    30 2019-04-30 22:07:13

[305 rows x 10 columns]</pre>

最开始以为是在复制文本时导致空格被合并了,因此使用sys.stdout将输出重定向到文本中,然后使用VSCode打开,发现居然也是错乱的

2. 使用严格半角的字体

经过非常严格和认真的对比,我发现这些文本是通过填充不同的空格进行对齐的,换言之,如果需要对齐,字体需要满足下面的条件

  • 英文字体等宽,且与一个空格的宽度相等
  • 中文字体等宽
  • 一个中文字符等于两个空格的宽度

这里需要配置符合下面要求的严格半角字体,参考:

在第二个回答中找到了Mac系统自带的一种字体PCMyungjo,试了一下

pre {
    font-family: PCMyungjo;
}

尽管部分标签符号和汉字看起来有点奇怪,居然能满足要求!!而这也仅仅需要一行简单的CSS代码。

当然,随之而来的就是兼容性问题,并不能保证所以机器上都安装了该字体,且该字体并不能通过UI那关,因此尝试去寻找了一些其他符合条件的字体。

后来发现如SimHei等黑体也可以满足条件,且汉字展示要美观得多

@font-face {
  font-family: "SimHei";
  src: url("SimHei.ttf") format("truetype");
  font-weight: normal; 
  font-style: normal;
  font-display: swap;
}
pre {
    font-family: SimHei;
}

中文自定义字体的缺点在于体积往往会非常大

因此还需要寻找其他不依赖于特定字体的其他方案。

这里补充一下关于字体的一些知识

3. 等宽字体

参考:等宽字体 - 维基百科

等宽字体(英语:Monospaced Font)是指字符宽度相同的电脑字体。与此相对,字符宽度不尽相同的电脑字体称为比例字体。

由于早期打字机和显示器等技术局限,字符一般也是等宽的。在传统西文印刷中,比例字体可以提高单词的可读性。目前由于技术突破,比例字体的使用也比较普及

大部分程序员选择的代码字体一般都是等宽的,等宽字体在处理缩进对齐、统一字符间距等方面更占优势;此外,东亚字体中的方块字基本上都作为等宽字体处理。

4. 全角半角字体

参考:

主要原因是符号冲突

比如英文逗号","与中文逗号",",用眼睛就可以看出长度与大小是不一样的。当在键盘上输入逗号时,中文输入法不确定你想要的是哪种逗号(中/英),所以就提供了全角半角模式,英文半角输出英文逗号,其它模式就是中文逗号,这样,我们用一种输入法就能打出两种符号,而不用切换成其它输入法

5. 控制每个中文字符的宽度

由于VSCode编辑框与终端默认配置的是相同的字体,因此编辑框和终端展示结果不一致应该不是字体的问题。那为啥终端会展示完全对齐的效果呢?

后来发现了一个类似的issue:Print data.frame with Chinese strings column aligned

其中提到了一个解决办法是手动控制设置每个中文字符的宽度~咋一看貌似挺不靠谱,转念一想:咦?貌似值得一试

function getTextSize(parent, ch = 'a') {
  const span = document.createElement('span')
  const result = {}
  result.width = span.offsetWidth
  result.height = span.offsetHeight
  span.style.visibility = 'hidden'
  // span.style.fontSize = fontSize
  // span.style.fontFamily = fontFamily
  span.style.display = 'inline-block'
  parent.appendChild(span) // 使用继承自父元素的字体样式
  if (typeof span.textContent !== 'undefined') {
    span.textContent = ch
  } else {
    span.innerText = ch
  }
  result.width = parseFloat(window.getComputedStyle(span).width) - result.width
  result.height = parseFloat(window.getComputedStyle(span).height) - result.height
  parent.removeChild(span)
  return result
}

然后控制每个字符的宽度,为了减少内联style导致HTML内容过于复杂,可以使用CSS变量

let preDom = document.querySelector('#pre')
let preTextSize = getTextSize(preDom)

preDom.setAttribute("style", `--char-width:${preTextSize.width}px`);

然后将所有的中文字符添加一个特定的样式类char-cn

.char-cn {
    font-family: monospace;
    display: inline-block;
    text-align: center;
    width: calc(var(--char-width) * 2);
}

最后将pre标签内容中的所有中文字符添加char-cn类,同时替换内容

function replaceChineseChar(str) {
    const re = /[\u4e00-\u9fa5]/gi
    return str.replace(re, function(match) {
        return `<span class="char-cn">${match}</span>`
    })
}

preDom.innerHTML = replaceChineseChar(preDom.innerHTML)

最后就可以得到与控制台基本一样的输出结果

大功告成!!!我现在甚至怀疑其他IDE或编辑器都使用了类似的实现方案~

6. 小结

总结一下两种解决方案:

第一种寻找特定字体的方案花费了大量的时间和精力,却没有得到一个比较完善的解决方案;

第二种方案由于之前没有类似问题的处理经验,忽略了JS处理内容和样式的作用,最后得到了一个还不错的解决方案。

写这篇文章,一小部分是记录这个文本对齐的样式调整问题;另外主要的目的是提醒自己不要沉醉在各种层出不穷的前端框架中,所有在Web中实现的功能,最终都会回归到HTML、CSS和JS中。像最近的vue-lit,如果等待某一天浏览器完美支持创建各种原生组件,我们是不是就不再需要Vue、React等框架了?