给网页加深色模式,为什么比想象中难?

做 Night Safari 之前,“深色模式”看起来像一个颜色问题。背景暗一点,文字亮一点,好像就可以了。
实际调试时,问题却一个接一个出现:正文变暗了,弹窗还是白的;页面加载时正常,打开通知就闪;某个区域看起来没问题,切换主题后又恢复不了。
这篇记录我从扩展开发中遇到的几个问题。它们也解释了为什么通用网页转换需要反复适配。

网页里的颜色有用途

网页上有正文、标题、次要信息、链接、边框、输入框和状态提示。同样的颜色,放在不同位置,承担的作用可能不同。
把所有浅色背景都改成同一个深色,容易丢掉卡片之间的层次;把所有文字都改白,又可能分不清主要内容和辅助说明。
图片、视频和图表更需要小心。它们的颜色可能就是内容本身。对整页应用反色滤镜,虽然很快能产生一个暗色画面,但还需要补救媒体颜色和其他显示问题。
Dark Reader 的动态主题因此会分析网页颜色与背景,并保留照片颜色。它的官方介绍也回顾了早期滤镜方案遇到的显示问题。动态主题的设计背景
在自己的项目里,我也尽量把内容媒体与普通背景区别处理。

看见的背景,未必属于眼前这个元素

一个文字区域的背景可以是透明的,实际颜色来自父元素。父元素也可能透明,继续显示更外层的背景。
如果只看当前元素的背景值,就可能判断错它到底处在浅色还是深色区域里。然后,文字虽然被修改了,真实背景却没有跟着变,阅读反而更困难。
渐变、背景图和半透明遮罩也有类似问题。它们无法用一个简单的颜色值描述。
所以,对比度需要放在真实显示的环境里检查。对原生深色中的局部补齐,我暂时采用较保守的范围,将复杂背景留给单独验证。

网页一直在变化

网页打开之后,内容还会继续出现:滚动加载的新卡片、鼠标经过时的菜单、通知弹层、输入框的聚焦状态,或者网站稍后读取设置才启用的主题。
只在加载时转换一次,无法覆盖这些变化。持续监听,又会增加成本,也可能观察到扩展自己的修改。
Night 开发中出现的反馈问题就是一个例子:主题转换写入样式,监听器发现变化,再次执行主题判断,后续又写入新的样式。
如果不区分这些事件,页面空闲时也可能反复重建。
项目后来过滤引擎自身的变化,将普通局部更新与需要重新判断的主题变化分开处理。这种区分也减少了不必要的引擎启停。

原生主题和转换主题会互相影响判断

我希望优先保留网站的原生深色模式。但当引擎已经把页面变暗时,检测器看到的是转换后的页面,容易把自己的结果当作网站本来的主题。
因此,Night 在判断原生主题前会同步停用引擎,再按判断结果恢复策略。
这里还需要关注中间状态:检查原始颜色时,如果网站设置了颜色过渡,用户可能看见颜色变化的过程;如果反复移除补齐样式,网站的动态样式又可能跟着变化。
知乎页面的闪烁排查让我看到,启停顺序、样式复用和过渡处理,都可能影响最终体验。

为什么截图好看,还不能证明完成

截图只能留下某个瞬间。主题切换是否闪白,滚动后新内容是否正确,停用后能否恢复,都需要过程验证。
我会把检查写成一组连续动作:
打开页面 → 等待动态内容 → 打开弹层 → 滚动或切换主题 → 停用扩展 → 检查恢复
每一步都可能暴露前一个截图里看不见的问题。
另外,本地浏览器回归与真实 Safari 扩展有不同的环境。接口替身能帮助测试脚本行为,却不能验证真实权限、隔离环境和弹窗通信。
所以,验证记录需要注明浏览器、网站、操作和范围。“在这个场景里正常”是有价值的结果,也有清楚的边界。

有些资源,扩展读不到

转换引擎需要了解样式和资源,但读取能力受到浏览器与网站的限制。跨域样式、登录保护资源以及复杂的页面结构,都可能需要特殊处理。
Night 当前使用正常浏览器请求读取资源,没有通过后台代理绕过跨域限制。遇到资源请求失败,需要先定位具体 URL 和影响,再决定如何处理。
一次资源失败,不一定代表整个转换引擎失效;页面看起来已经变暗,也不代表所有资源都分析成功。
此外,嵌入 Dark Reader API 引擎不等于获得完整扩展的全部网站适配。项目仍需要维护自己的验证范围和修正策略。

我现在怎样看待深色模式

对我来说,这件事已经从“换一套颜色”,变成了理解页面的结构和变化:哪些颜色表示内容,哪些表示层次,变化由谁触发,什么时候需要重新处理。
最值得继续优化的,往往也是使用时最容易感到的小问题。一个弹窗有没有闪,输入框能不能看清,关闭扩展后有没有留下样式。
这些问题没有一个通用规则能全部解决。把每次遇到的场景记下来,再逐步验证,是我接下来继续开发 Night Safari 的方式。
相关项目:Night Safari
对于本文内容有任何疑问, 可与我联系.