今天帮同事排查一个邮件渲染问题,花了两个多小时,最后发现根因只有一行 CSS。记录一下完整过程和可复用的排查方法,踩过的坑都写在里面。
背景是这样的:业务侧发出去的 HTML 邮件,在网页端预览完全正常,但同事反馈好几个客户回消息说邮件字体很奇怪,有的地方是宋体,和设计的字体完全对不上。对方用的客户端有 Outlook、QQ 邮箱网页版、163 邮箱,还有 Windows 上自带的邮件应用。我一开始的直觉是编码问题,因为中文字体丢失最常见的表现就是乱码或者回退。
第一步先做了对照实验。同一份 HTML,分别在自己机器上的浏览器打开、在邮件客户端打开、在移动端打开。浏览器里完全正常,说明 HTML 结构和 CSS 语法本身没写错。邮件客户端里字体回退,说明问题出在客户端的渲染环境差异上,不是代码有语法错。这个判断很重要,因为它把排查范围从「代码写错了」缩小到「客户端不支持某个东西」。
第二步看 HTML 源码结构。原来的写法是把字体声明写在了 body 的 style 属性上,大概就是 body style 里塞一个 font-family 列表。这个写法在浏览器里是生效的,所以本地测试永远看不出问题。但邮件客户端对 HTML 的处理和浏览器不一样,很多客户端会做一轮「消毒」,把不认识的标签和属性剥掉。body 上的 style 属性就在被剥的范围内,尤其是 Outlook 用的还是 Word 的渲染引擎,对 CSS 的支持程度和浏览器差得很远。
这是这次最重要的一个认知:邮件客户端的渲染环境不是浏览器。浏览器是「宽容」的,写错了也尽量渲染;邮件客户端是「严格」的,不认识的东西直接丢掉,而且不同客户端丢的东西还不一样。
第三步是验证。为了确认到底是 body 上的 style 被剥了,还是字体列表里的字体本身不存在,我做了一个最小化实验:写三份邮件,第一份只在 body 上声明字体,第二份在 html 标签上声明,第三份在最外层 table 上声明。发出去看结果,第一份回退,第二份和第三份正常。这就定位清楚了,问题不是字体不存在,是声明被剥掉了。
顺便说一个诊断技巧:不要靠猜,让用户在邮箱里点「显示原始邮件」或者「查看源码」,看 html 和 body 标签上到底还在不在。这个方法比反复试写法的效率高得多,因为你能直接看到客户端删了什么。
最终的修复方案是三层同时注入。第一层在 html 标签上声明,第二层在最外层 table 的 style 上声明,第三层在 body 上声明。三层都写同样的字体列表。这样任何一个客户端剥掉其中一层,另外两层还能兜住。这看起来有点笨,但确实是邮件开发里被验证过的做法,因为每个客户端剥的东西不一样,多层冗余是唯一稳的方案。
还有一个容易被忽略的点是字体回退链。设计稿用的通常是苹方,但苹方只有 macOS 和 iOS 上有,Windows 上根本没有这个字体。所以回退链里必须包含 Windows 上真实存在的字体,比如 Microsoft YaHei 也就是微软雅黑,还有 SimHei 黑体。如果回退链只写苹方和几个不存在的字体,那在 Windows 上必然掉到默认的宋体。回退链的顺序也要注意,把最想要的放前面,把一定存在的通用字体放最后兜底。
这次还犯了一个低级错误值得记下来。在构建邮件 HTML 的时候,我复制粘贴了一段含引号的字符串,导致引号嵌套层级错了,脚本直接报语法错误。同一个会话里这个错犯了两次,一次是构建失败,一次是脚本报 SyntaxError。教训是任何包含 HTML 的脚本,在跑之前先做一次 node --check 或者 python -m py_compile,成本是一秒钟,能省掉后面一轮排查。含引号的 HTML 字符串最好用三引号包起来,别用单引号一层层转义。
最后总结可复用的方法。排查这类「本地正常、用户端异常」的问题,先做对照实验把变量分离,确认是代码问题还是环境问题;然后用最小化实验,一次只改一个变量,看哪个变量影响结果;再让用户提供原始邮件源码这种第一手证据,别靠猜;最后修复方案要考虑到环境多样性,用冗余而不是用某个「正确」的写法。
这次的经验也适用于其他场景。凡是渲染环境不可控的地方,比如邮件、各种第三方平台的富文本编辑器、不同版本的系统内置浏览器,都不能假设「我本地测通了就没问题」。要反过来想,对方的环境可能剥掉什么、可能不支持什么,然后做冗余。