导语 微信公众平台的图文编辑器拥有极高的开放性以及庞大的插件市场,用户可以在这里编辑非常丰富的样式来提升阅读体验,其自由度在业界属于极少数的存在,也因此在文章中会存在大量的自定义 DOM 及样式,导致平台在支持 Dark Mode 的路上举步维艰。本文将讲述微信公众平台探索图文 Dark Mode 的历程。

项目已开源,可点击底部阅读原文访问。

1 什么是 Dark Mode

Dark Mode,即「深色模式」。2018 年苹果在 macOS 10.14 上加入了 Dark Mode,Android 系的厂商也陆续加入支持,Google 在 Android P 当中也正式推出了自己的 Dark Mode,去年在 WWDC 大会上 iOS 13 正式引入 Dark Mode。

Dark Mode 可以显著降低屏幕的整体视觉亮度,减少眼睛的视觉压力。在 Dark Mode 下,所有的界面元素都退居幕后,使得我们真正与之交互和操作的内容可以被凸显出来。同时随着 OLED 屏幕的普及,支持 Dark Mode 更有了一定的现实意义:省电。由于 OLED 屏幕中每个像素都是自主发光而非 LCD 由整个一块背光面板发光,所以在显示深色元素时像素所消耗的电流更低,在纯黑色下像素点可以完全关闭达到省电的效果。

如今 Dark Mode 已经成为大势所趋。

2 如何识别 Dark Mode

如果浏览器支持 Dark Mode,那么可以通过 css 媒体查询 prefers-color-scheme: dark [1] 来识别并对 Dark Mode 进行样式兼容。

@media (prefers-color-scheme: dark) { /* Dark Mode Styles */ }

在 js 里我们可以使用 API matchMedia [2] 来匹配 CSS 媒体查询的结果。

const mql = window.matchMedia('(prefers-color-scheme: dark)'); // 匹配媒体查询 const switchToDarkmode = ({ matches }) => { if (matches) { ... // do something in Dark Mode } }; mql.addListener(switchToDarkmode); // 监听 switchToDarkmode(mql); // 先手动执行一次

另外,建议在 head 里加上 meta 标签[3],通知浏览器该网页支持的 color-scheme。

<meta name="color-scheme" content="light dark">

3 在图文场景下仍存在问题

一般情况下,我们只需要使用上述方法对 Dark Mode 写一套样式来兼容就可以解决了。

但由于公众平台的图文编辑器支持富文本编辑,用户可以通过编辑内联样式来实现许多自定义效果,所以在图文里会存在许多我们无法预知的元素及样式,因此也无法对其书写对应的 Dark Mode 兼容样式。

我们拿了一些同类型产品做调研分析,打算学习一下它们的做法。

3.1 同类型产品方案

  1. 强制将 color 及 background-color 设为指定的色值;
  2. 在图片上盖一层灰色蒙层来将图片亮度调低。

微信公众平台图文投票(微信公众平台图文)(1)

[ 同类型产品 Light Mode - Dark Mode 对比 ]

尽管 Dark Mode 下的阅读体验还不错,但因为颜色丢失还是会造成一些问题

理()所()当()然(),我们没有采取这种方案。

再看看业界常见的处理方式「反色」,以下是两个比较有代表性的反色算法。

3.2 Google Chrome 内核反色算法

我们参考 Google Chrome 内核(源码[4],C )实现了其反色算法(源码[5],js)。

微信公众平台图文投票(微信公众平台图文)(2)

[ Google Chrome 内核反色算法流程图 ]

由于在图文的场景中不需要对样式表做处理,所以应用到图文里我改为对内联样式做处理。

微信公众平台图文投票(微信公众平台图文)(3)

[ Google Chrome 内核反色算法 Light Mode - Dark Mode 对比 ]

方案总结:

3.3 Dark Mode for Email

众所周知,邮件支持富文本内容,在某种程度上,图文和邮件的场景其实是一致的。于是我们研究了 Outlook 的 Dark Mode 方案《Dark Mode for Email: What it is and How to Cope》[6]。

在文章中有贴算法的代码 Gist(ts)[7],但是这个算法不支持透明度计算,于是我 Forked 源码并加上透明度支持后也发到 Gist(ts)[8] 上了。

微信公众平台图文投票(微信公众平台图文)(4)

[ Outlook 算法流程图 ]

微信公众平台图文投票(微信公众平台图文)(5)

[ Outlook 算法 Light Mode - Dark Mode 对比 ]

方案总结:

4 自定方案

尽管 Outlook 的 Dark Mode 方案效果很好,但仍有缺陷(毕竟我们政治立场坚定,不接受把爱国红反色成萝卜橙的行为),所以最后还是放弃 Outlook 方案,自定一套微信图文的 Dark Mode 方案(已开源,可点击底部阅读原文访问)。

自定的方案主要需要解决以下两个问题:

  1. 切换 Dark Mode 时避免进行 DOM 操作;
  2. 尽可能地保留原来的颜色。

4.1 确定写入 Dark Mode 样式的方式

调研的方案都是通过 js 遍历所有节点并对其写入 inline style 来设置 Dark Mode 样式的,这种方式在切换时因为要将 inline style 还原回去,所以免不了会有大量的 DOM 操作,并且频繁触发页面重绘。

众所周知,DOM 操作的性能是极差的,能免则免。

上面提到过 CSS 媒体查询 prefers-color-scheme: dark,而 CSS 媒体查询本就是为切换样式而生的,于是我们决定使用 CSS 媒体查询来替代改写 inline style 的方式。

在遍历节点时我们对颜色进行处理后生成 Dark Mode 的 css,并给这个节点加上特定的 class,遍历结束后在页面上插入新样式表,格式为 @media (prefers-color-scheme: dark) {.${class} { ${css} }} 。

[ 写入 Dark Mode 样式的方式 ]

如此一来,只会在页面加载时进行 DOM 操作设置 Dark Mode 样式,由于遍历结束后一次性插入样式表,所以能够有效减少页面重绘次数。并且切换时不会有额外的 DOM 操作,样式切换全部交给 CSS 媒体查询来处理。

4.2 制定颜色处理算法

Google Chrome 方案是对颜色做全反色,而 Outlook 方案则是对颜色做色差对比,色差达到阈值后再做反色。但无论阈值设置为多少都难免会有 bad case 出现,无法保留原来的颜色。

所以我们决定放弃反色方案,另寻别的方式。

浏览器支持的颜色表示方式除了常见的 hex(16进制)和 rgb(红、绿、蓝)以外,还支持 hsl(色相、饱和度、亮度)。

如果我们将所有颜色格式都转换为 hsl 并结合感知亮度[9] 根据一定的规则来调整亮度或感知亮度,那么理论上就可以做到保留原色并且在 Dark Mode 中看起来不那么刺眼。基于这个理论,我们进行了大量的尝试和考量,最终得出以下方案:

处理 1:带颜色节点

注意:

处理 2:带背景图片节点

注意:

处理 3:IMG 图片节点

注意:

算法思路:颜色转换

注意:

[ Dark Mode 效果对比 ]

5 性能优化

尽管在切换 Dark Mode 时使用 CSS 媒体查询可以避免进行大量 DOM 操作来提升性能,但是页面初始化时仍会进行大量 DOM 操作(遍历正文所有节点)。

下表是统计各个算法下的 Dark Mode 处理耗时(耗时视文章长度会有差异,该统计使用的是这篇图文)。

算法

Dark Mode 处理耗时

Google Chrome

~20ms

Outlook

~50ms

自定算法

~30ms

由于我们对正文进行了阻现操作(正文容器默认设置 visibility: hidden,Dark Mode 处理完成后再设置为 visibility: visible),所以正文会经历一个从空白到出现内容的过程,而这个耗时在理想状态下(忽略 Dark Mode 以外的处理)则等于 Dark Mode 处理耗时。

尽管当前耗时为毫秒级,但是实际体验时还是会觉得空白时间略长(我们对图文的阅读体验要求相对较高),于是我们思考了一些优化策略。

5.1 首屏加载优化

上面说到我们通过对正文容器设置 visibility: hidden 来隐藏正文内容,visibility 和 display 都可以达到隐藏的效果,前者除了保留高度以外,还有一个很特别的特性,可以看下面这个例子[11]。

<div style="border: 1px solid black; padding: 0 16px;"> <!-- visibility --> <div style="visibility: hidden;"> <p style="visibility: visible;">Para 1</p> <p>Para 2</p> </div> <!-- display --> <div style="display: none;"> <p style="display: block;">Para 3</p> <p>Para 4</p> </div> </div>

微信公众平台图文投票(微信公众平台图文)(6)

[ 运行结果 ]

可以发现,只要子元素设置为 visibility: visible,即使父元素设置了 visibility: hidden,子元素也依旧可以显示。而 display 则不能这么玩。

那么,基于这个特性,我们只要在遍历节点时记录首屏节点,把首屏节点的 Dark Mode 样式提前写入样式表(我们称之为首屏样式表),再给所有首屏节点加上 visibility: visible,就可以达到优先显示首屏的效果了。

文章

首屏优化前

首屏优化后

倍数

文章1

27ms

4ms

6.75

文章2

99ms

18ms

5.5

(PS:实际效果视文章长度会有差异)

5.2 优化非 Dark Mode

以上所有 js 逻辑全是针对 Dark Mode 所做的处理,包括大量的 DOM 操作以及算法,而对非 Dark Mode 或是不支持 Dark Mode 的用户来说,则没有半点关系,我们不能因为一个用不上的功能而降低用户体验。

所以我们做了这样的一个优化:加载页面时检测是否为 Dark Mode,是则运行 Dark Mode 处理逻辑,否则延迟运行(当用户切换 Dark Mode 时再运行)且无需进行首屏判断以及阻现逻辑。

5.3 优化弱网环境

由于 Dark Mode 逻辑全部是在 js 资源里,假设在弱网环境下已经收到了 html,但是 js 资源加载非常缓慢,那么正文将一直被隐藏,无法阅读(实测在弱网 wifi 下,正文会被隐藏 3~4s),这会大大降低用户体验。

针对这种情况,我们将 Dark Mode 逻辑迁移至 html 中,也就是俗话说的直出

因为 Dark Mode 逻辑仅依赖 color 模块和 color-name 模块,所以迁移成本不高,迁移后增加的文件体积也在可接受范围内。

6 写在最后

每个算法都有它的不足,我们的算法自然也逃不过这个定律。

目前仍存在少量 bad case,我们也在持续进行优化,如果大家有更好的方式可以提 Issues 一起探讨,共同进步。

相关外链

[1] prefers-color-scheme: dark 兼容性:https://caniuse.com/#search=prefers-color-scheme

[2] matchMedia MDN:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/matchMedia

[3] meta 标签相关说明:https://medium.com/dev-channel/what-does-dark-modes-supported-color-schemes-actually-do-69c2eacdfa1d

[4] Google Chrome 内核源码:https://cs.chromium.org/chromium/src/third_party/blink/renderer/platform/graphics/dark_mode_filter.h

[5] 小程序团队实现的反色算法源码:https://gist.github.com/JaminQ/32cd8b6c2d4975ec48db4f5e507d5ed5

[6] 《Dark Mode for Email: What it is and How to Cope》:https://www.emailonacid.com/blog/article/email-development/dark-mode-for-email/

[7] Outlook 算法 Gist:https://gist.github.com/hteumeuleu/51b5a8ea95cb47e344b0cb47bc1f2289#file-darkmodehandler-ts-L131

[8] Outlook 算法 Gist (Forked,支持透明度计算):https://gist.github.com/JaminQ/68b089b51a6054e5b8169949244d0c46

[9] 感知亮度:https://www.w3.org/TR/AERT/#color-contrast

[10] Stacking Contexts:https://philipwalton.com/articles/what-no-one-told-you-about-z-index/

[11] CodePen:https://codepen.io/jaminqian/pen/GRgvRGy

作者:JaminQian

来源:WeChatFE

出处:https://mp.weixin.qq.com/s/jgipW2ihmXJBj-4WuiV_rw

,