导语 微信公众平台的图文编辑器拥有极高的开放性以及庞大的插件市场,用户可以在这里编辑非常丰富的样式来提升阅读体验,其自由度在业界属于极少数的存在,也因此在文章中会存在大量的自定义 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 同类型产品方案
- 强制将 color 及 background-color 设为指定的色值;
- 在图片上盖一层灰色蒙层来将图片亮度调低。
[ 同类型产品 Light Mode - Dark Mode 对比 ]
尽管 Dark Mode 下的阅读体验还不错,但因为颜色丢失还是会造成一些问题:
- 用户通过颜色设置的效果被丢失(如:强调)
- 没有用户预期,假设用户想要在 Dark Mode 下保留颜色,只能通过插入图片
- 容易造成政治问题(想象一下,一篇充满红色革命缅怀先烈的文章在 Dark Mode 下失去了浓浓的爱国红)
理(防)所(止)当(被)然(抓),我们没有采取这种方案。
再看看业界常见的处理方式「反色」,以下是两个比较有代表性的反色算法。
3.2 Google Chrome 内核反色算法
我们参考 Google Chrome 内核(源码[4],C )实现了其反色算法(源码[5],js)。
[ Google Chrome 内核反色算法流程图 ]
由于在图文的场景中不需要对样式表做处理,所以应用到图文里我改为对内联样式做处理。
[ Google Chrome 内核反色算法 Light Mode - Dark Mode 对比 ]
方案总结:
- 全反色误伤严重,效果不乐观;
- 不支持 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] 上了。
[ Outlook 算法流程图 ]
[ Outlook 算法 Light Mode - Dark Mode 对比 ]
方案总结:
- 非简单模式下有颜色对比判断,减少很多反色误伤,但仍有 bad case;
- 支持 Light Mode - Dark Mode 切换,但每次切换都需要进行大量的 DOM 操作。
4 自定方案
尽管 Outlook 的 Dark Mode 方案效果很好,但仍有缺陷(毕竟我们政治立场坚定,不接受把爱国红反色成萝卜橙的行为),所以最后还是放弃 Outlook 方案,自定一套微信图文的 Dark Mode 方案(已开源,可点击底部阅读原文访问)。
自定的方案主要需要解决以下两个问题:
- 切换 Dark Mode 时避免进行 DOM 操作;
- 尽可能地保留原来的颜色。
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:带颜色节点
- 只处理背景颜色、字体颜色、边框颜色和字体阴影颜色,由算法统一进行转换(算法持续优化中,见如下算法思路)
- 渐变色先计算出 mix 颜色后再处理(Dark Mode 下转为纯色)
- SVG 节点跳过(后续优化)
- 有背景图节点跳过(见如下处理 2)
注意:
- 背景颜色、字体颜色处理前后色值需透传子节点用于子节点计算
- 只有背景颜色无字体颜色节点需追加计算后字体颜色
处理 2:带背景图片节点
- 图片上方存在文本,则给图片底层补 Light Mode 原背景色(保证图片上方文本的可读性)
- 图片上层加蒙层(降低背景亮度)
- SVG 节点跳过(SVG 存在动画状态,存在不可控因素)
注意:
- 背景图处理规则影响子节点,直到子节点出现可见背景颜色(透明度需 ≥ 0.05)恢复为处理 1
- 使用 border-image 设置背景图节点暂无法在图片上层添加蒙层
处理 3:IMG 图片节点
- 添加滤镜降低图片亮度
注意:
- 添加 filter 后会创建新的 Stacking Contexts[10],影响节点层级(副作用)
算法思路:颜色转换
- 背景颜色:
- 优先处理背景色,根据转换后的背景色再处理字体颜色
- 高亮度灰白背景颜色(l > 40)降低亮度(感知亮度高于阈值视为白色处理,阈值:250)
- 高感知亮度背景颜色(> 阈值)感知亮度调整为阈值(阈值:190)
- 低感知亮度背景颜色(< 阈值)感知亮度调整为阈值(阈值:22)
- 字体颜色、边框颜色:
- 高亮字体颜色(接近白色感知亮度),不处理,保持高亮
- 根据背景颜色计算出感知亮度差值,高于阈值不处理,低于阈值调整为阈值(阈值:65)
- 字体阴影颜色:
- 处理方式和背景颜色一致,但不会根据转换后的字体阴影颜色去处理字体颜色
注意:
- 背景颜色透明度加入感知亮度计算
[ 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>
[ 运行结果 ]
可以发现,只要子元素设置为 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
,