如果前端的产品开发是技术的炫耀,那么邮件模板的开发能让人一下子回到解放前。总结下来邮件模板的开发就是tabletable的嵌套,注意没有拼接。

响应式

这是一个 Mobile firest 的年代,为了保证在小屏手机上的显示,一般需要声明一下meta头来让页面自动缩放,且在邮件正文的开发中,尽量避免指定具体宽度,以此达到响应式的设计目的。

<meta name="viewport" content="width=device-width"/>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>

下面是使用缩放与未使用的对比。

使用 viewport meta 标签使用 viewport meta 标签未使用 viewport meta 标签未使用 viewport meta 标签

使用 table 布局

邮件客户端不比浏览器,它的解析能力决定了我们不能在邮件中使用像网页那样复杂及随性的布局,取而代之需要使用最原始的 table 布局。欢迎回到 Web1.0时代,朋友。

同时,table 布局的意思还包含 table 的嵌套。并不只是整个页面使用 table 来写,里面的正文内容如果有布局需求,也是需要嵌套 table 来进行的。所以整个邮件的开发可以理解为 table 及 table 嵌套。

<!-- body -->
<table>
    <tr>
        <td>
            <!-- left padding or sth -->
        </td>
        <td>
            <!-- content -->
            <table>
                <tr>
                    <td>
                        <!-- content goes here... -->
                    </td>
                </tr>
            </table>
        </td>
        <td>
            <!-- right padding or sth -->
        </td>
    </tr>
</table>

那么为什么只能使用 table 来进行布局?

因为使用 table 可以解决大部分显示上的兼容性问题,别问为什么,这可是实践出真知的典型。特别是在浏览器里调好样式好,在一些主流邮件客户端上看问题也不大,但到一些手机自带邮件客户端里面就会出现样式上的错乱。

使用内嵌样式

为了保证大多数邮件客户端的兼容性,能够使用属性做为样式的直接使用属性,不写 style,比如在<table> 标签上指定背景色可以使用bgcolor

<table bgcolor="#ccc">
    content goes here...
</table>

即使写 style 样式也使用内嵌样式,不抽取到 <style> 标签。因为它 标签大多数情况下会被邮件客户端干掉而不生效。

<table style="background-color:#ccc">
    content goes here...
</table>

因此,邮件中所有样式就都由直接属性和内嵌样式组成,这样最安全可靠。且样式中尽量使用最基本的一些样式譬如字体字号,颜色行高,太先进的 css 是不可取的。

图片

关于图片,各邮件客户端策略会不一样。出于安全考虑,大多数邮件客户端默认会禁止掉图片的显示。需要用户手动同意后方可加载显示。

Gmail之前的做法是提示用户是还展示邮件中的图片,后来在 Inbox 中升级了规则,会把邮件中的图片资源转存到谷歌自家的服务器进行分析,经过一套严格的过滤规则后来决定是还展示图片。

其他客户端譬如 QQ 邮箱,除了 spam 系统的过滤外,随着用户的举报增多,也会影响图片的在邮件中的展示。

总之,对于图片这一块,可以肯定的一点是,从代码层面没有办法可以保证图片一定正常展示。

所以,针对图片的优化只有一个思路,那就是不使用图片。基于此,我们可以尝试把图片以另外一种形式展现,只要保证用户看到的内容不变,以什么形式存在倒也无所谓了。

将图片转 base64

这是最容易想到的一个点,将图片转成 base64 格式的字符串再指定到<img>标签的src

经过在 Gmail 里的尝试,这样反而会被直接干掉。而且,这种方法原理上还是以图片形式存在的,并没有本质区别。

将图片做为背景

将图片做为背景以及使用 base64 格式的图片加背景的方式经过实验,其实和直接使用图片或 base64 的情况是没多大区别的。

将图片由 table 拼接

将图片转由一个个由 <td> 设上背景色的像素点拼接而成。这种方法经过实践在显示上还好。但问题是一张小小的 logo 图片就需要上千多个<td>标签来拼接,直接导致邮件整体 DOM 数增加,而这些 DOM 节点会被认为是邮件的正文。所以 Gmail 会认为邮件正文内容过多,直接折叠。

Logo 图片由 HTML 拼接而成Logo 图片由 HTML 拼接而成

从图上还看出另一个问题,由于 LOGO 节点数太多,只显示了一半。这并不是我们想要的。

这还只是对于小图片而言,如果是正文里有大图片的话,此方法更是行不通的。

将图片转 ASCII 字符

如果邮件中有类似二维码之类的图片,倒是可以考虑将其转成 ASCII 字符的拼接,并且展示效果还很理想。

刚好有类似实现的介绍,参见这里

但这种方法对于邮件顶部 Logo 的处理同样会有上面的问题,就是这些 ASCII 字符是算作正文字符的。在邮件列表的中的预览会不友好,同时也会因为内容太长在 Gmail 中有显示不全的可能。

CID

某些邮件后台是支持将图片资源附到邮件正文中的,邮件正文中通过约定好的 CID 取到相应的图片后展示。比如 [sendgrid](https://sendgrid.com/ 代码类似于下面这样:

Nodejs 后台代码 https://sendgrid.com/blog/embedding-images-emails-facts/ source

var params = {
  smtpapi:  new sengrid.SmtpapiHeaders(),
  ... //some vars removed here!
  files: [
    {
      filename:     'image.jpg',          
      contentType:  'image/jpeg',
      cid:          'myimagecid',
      content:      ('yourbase64encodedimageasastringcangohere' | Buffer)
    }
  ],
  file_data:  {},
  headers:    {}
};

html 代码 https://sendgrid.com/blog/embedding-images-emails-facts/source

<html>
  <body>
    <img src="cid:myimagecid"/>
  </body>
</html>

这种方法有些过时,需要邮件后台支持,展示效果也因客户端而异吧,并且会增加邮件本身的大小,所以也并不完美。

其实最后发现图片还是直接使用<img>标签加正常的 URL 地址好些,这样客户端能正确识别图片,给出相应的提示给用户。

Gmail/Inbox 中的若干问题

Gmail 对邮件内容的智能处理反而给开发者带来了麻烦。先来看看发现的问题。

折叠

一个很重要的问题就是内容被折叠成三个点。情况类似于下面这样:

邮件内容在 Inbox 中被折叠邮件内容在 Inbox 中被折叠

邮件内容在 Gmail 中被折叠邮件内容在 Gmail 中被折叠

这直接导致新邮件无法正常首屏展示。

错误的引用导致文本成紫色

另一个问题则是有时候邮件内容或者部分内容会出现紫色。

邮件内容变成紫色邮件内容变成紫色

进一步又会发现,紫色部分是因为被 Gmail 用一个 带 im class 标签包裹起来导致的。而一般引用的内容会被这样处理。

邮件紫色内容被`im`样式类包围邮件紫色内容被`im`样式类包围

解决

上面的原因其他也是经过不断的摸索和 Google 后发现的。同时在网上也找到了类似的问题。

比如 stackoverflow 上的这个就很具有代表性。

里面描述的问题一样,并且给出了可能的解决方法。

来自 Gmail 产品论坛的这个帖子也解答了相应的问题。

于是根本原因算是清楚了。Gmail 默认会将邮件标题相同的邮件合并成对话的形式,如果里面有相似的内容,就会被折叠,或者变成引用。

试想一下,前后发一两封一模一样的邮件,这两封邮件标题一样,所以被合并成对话,第二封邮件由于内容和前面一封完全一样,于是就只显示成三个点,内容被完全折叠。

部分紫色的情况说明当部分内容相同时,会被转成引用,推测 Gmail 会认为当前邮件里引用了上一封邮件的内容,所以用紫色标识出来。

知道原因后那么解决方法就很明朗了。尽量保证邮件标题是变动的,如果是订阅相关的邮件,或许你看到过类似『playboy 周刊 #1』,『playboy 周刊 #2』… 这样的标题,这样的邮件就不会被合并成对话,所以不会有上面提到的问题。于是我们除了开发邮件需要规避一些问题外,运营者也需要考虑一下邮件标题。

同时,从搜索来的资料还解释清楚了另外一个问题,如果一个 <table> 使用了一个以上的<tr>标签,也是有被折叠的可能的。

一个 table 中使用了多个 tr 被折叠的情况一个 table 中使用了多个 tr 被折叠的情况

所以尽量一个 <table> 使用一个 <tr>, 不满足需求时考虑 table 的嵌套。

一份看上去不错的模板

根据以上摸索出来的原则,于是得到了大概像下面这样一个模板。在此基础上进行开发并遵循上面的建议会省很多时间。

以下代码在 github 上也可以找到。

<!DOCTYPE html>
<html style="font-size: 100%;margin: 0; padding: 0;">
    <meta name="viewport" content="width=device-width"/>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <title>
        邮件标题
    </title>
    <table style=" width: 100%; margin: 0; padding: 40px 0 20px 0;border-width: 0;border:none;">
        <tbody>
            <tr style=" margin: 0; padding: 0;">
                <!-- content -->
                <td bgcolor="#FFFFFF" style=" clear: both !important; display: block !important; max-width: 600px !important; Margin: 0 auto; padding: 16px; border-width:0;box-shadow: 0 1px 2px rgba(9, 2, 1, 0.1), 0 0 10px rgba(0, 0, 0, 0.06); ">
                    <!-- logo -->
                        <p>
                            <img src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" alt="LOGO" width="100" height="50"/>
                        </p>
                        <!-- logo end -->
                        <p style=" font-size: 14px;color: #354450; font-weight: normal; margin: 18px 0 18px; padding: 0;">
                            Allo,
                        </p>
                        <p style=" font-size: 14px; color: #354450;line-height: 1.6em; font-weight: normal; margin: 0 0 18px; padding: 0;">
                            这是邮件正文部分,这里进行邮件正文的填充。这是邮件正文部分,这里进行邮件正文的填充。这是邮件正文部分,这里进行邮件正文的填充。这是邮件正文部分,这里进行邮件正文的填充。
                        </p>
                        <p style=" font-size: 14px; color: #354450;line-height: 1.6em; font-weight: normal; margin: 0 0 18px; padding: 0;">
                            这是邮件正文部分,这里进行邮件正文的填充。这是邮件正文部分,这里进行邮件正文的填充。这是邮件正文部分,这里进行邮件正文的填充。这是邮件正文部分,这里进行邮件正文的填充。
                        </p>
                        <!-- action button -->
                        <table cellpadding="0" cellspacing="0" border="0" style=" width: auto !important; Margin: 30px auto; padding: 0;border-collapse: collapse;">
                            <tbody>
                                <tr style=" margin: 0; padding: 0;">
                                    <td style="font-family: 'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif; font-size: 14px; border-radius: 2px; text-align: center; vertical-align: top; background: #4184f3; margin: 0; padding: 0;" align="center" bgcolor="#348eda" valign="top">
                                        <a href="#" style=" line-height: 2; color: #ffffff; border-radius: 2px; display: inline-block; cursor: pointer; font-weight: bold; text-decoration: none; background: #4184f3; margin: 0; padding: 0; border-color: #4184f3; border-style: solid; border-width: 5px 20px;">
                                            按钮操作
                                        </a>
                                    </td>
                                </tr>
                            </tbody>
                        </table>
                        <!-- action button end -->
                        <p style=" font-size: 14px; color: #354450;line-height: 1.6em; font-weight: normal; margin: 0 0 18px; padding: 0;">
                            这是邮件其他正文部分<a href="#">这是一个链接</a>
                        </p>
                        <p style="color:#999; font-size: 12px; font-weight: normal; margin: 30px 0; padding: 0;">
                            这是一些脚注,不太重要的二级信息。详情请
                            <a href="#" style=" margin: 0; padding: 0;">
                                点击
                            </a>
                        </p>
                </td>
                <!-- content end-->
            </tr>
        </tbody>
    </table>
</html>

其他资料