摘要

产品矩阵里有四个 Web 工作台(语音、视频、配音、外贸)加一个官网主站,用户提了两个要求:全部默认暗黑模式,风格与主站统一;以后要做主站白天模式时不能翻车。听起来是”改四个开关”的小事,实际上最有价值的是排查方法论:代码里写了默认暗 ≠ 首屏渲染是暗。localStorage 残留、系统主题跟随、登录页和主应用两张皮,都能让”我以为暗了”变成”用户看到的还是亮”。本文记录判定方法和三个真实根因。涉及域名与路径已脱敏。

封面:四块深色界面面板拼成一面墙

一、判定标准:先定义”什么叫做默认暗黑”

三条硬标准,缺一不认:

  1. 干净环境:全新的浏览器 profile,localStorage.clear() 都嫌不够,直接每站开新 context——老用户残留的主题偏好会骗人;
  2. 零交互:页面加载完立即测,不点任何按钮(点了切换钮再测等于测了个别的状态);
  3. 量像素:不信变量、不信 class,量实际渲染出来的背景色。
from playwright.sync_api import sync_playwright
import math

def luminance(page):
    rgb = page.evaluate("""() => {
      const c = getComputedStyle(document.body).backgroundColor;
      return c.match(/\\d+/g).map(Number);
    }""")
    r, g, b = rgb[:3]
    return round(0.2126*r + 0.7152*g + 0.0722*b, 1)

for url in SITES:
    with sync_playwright() as p:
        b = p.chromium.launch()          # 不装任何 storage_state
        pg = b.new_page()
        pg.goto(url, wait_until="load")
        print(url, luminance(pg))
        b.close()

实测输出(阈值 <40 判暗):

voice.example.com  → rgb(10,14,23)  L=13.8  DARK ✓
video.example.com  → rgb(11,13,16)  L=12.8  DARK ✓
dub.example.com    → rgb(10,14,23)  L=13.8  DARK ✓
trade.example.com  → rgb(10,14,23)  L=13.8  DARK ✓

二、根因一:登录页和控制台是两张皮

外贸站公网 /console/ 会 302 到登录页——如果只量登录页就收工,属于测了个假目标。本案例里登录页恰好早就被钉成暗色(html,body 直接 background:#0a0e17 加 color-scheme:dark),但真正的控制台本体是另一份代码、另一台服务器上的独立应用。

正确动作分两步:

像素标尺:DARK/LIGHT 阈值带与四站实测 L 值

  1. 公网实测登录页渲染 luminance(上面那套脚本);
  2. 进源码查本体:grep 出样式 token 与主题切换逻辑——
:root { --bg:#0a0e17; --panel:#0f1520; --text:#e6edf3; }

本体没有 prefers-color-scheme 自动切亮逻辑、没有“默认 light 等 JS 覆盖”的窗口期 → 判定通过。教训:多页应用要按“用户会看到的所有页面”逐个量,302 跳转链上的每一站都算。

三、根因二:编译产物改不了,就在注入层做主权

视频站是打包后的 SPA,assets/index-*.js 是压缩混淆产物,bundle 不能动(改了 hash 就变,文件名对不上)。但 Gradio/SPA 类应用大多有一个可读写的 index.html 外壳——静态服务直接读盘,改完刷新即生效,连后端都不用重启。

策略:一切定制进外壳,不动 bundle。注入层做四件事:

<style>/* 品牌色/布局覆盖,选择器用 !important 压过 bundle */</style>
<script>
  // 1. 主题兜底:无记录时强制暗色,跟随 bundle 自己的存储键
  const K = "site-theme";
  if (!localStorage.getItem(K)) localStorage.setItem(K, "night");
  // 2. 品牌导航条(顶栏三药丸 + 互链)
  // 3. 语言切换(中英词典,DOM 改写)
  // 4. 图标 SVG 替换
</script>

注意坑:注入的 CSS 若依赖“选择器权重恰好赢”,一次 bundle 升级就可能输。所有关键规则显式 !important,并把 bundle 里的存储键名(这里是 "night" 这类字符串值)抄准——猜错值等于白写。

四、根因三:SVG 图标的两个隐形炸弹

品牌统一还揪出两个图标问题,都不影响“能渲染”,但影响“在别的上下文里正常”:

炸弹一:零宽 bbox + objectBoundingBox 渐变。渐变写成 gradientUnits="objectBoundingBox"(默认值)时,渐变坐标相对元素自身包围盒归一化。一旦图标在某些容器里包围盒退化(宽度为 0 的包裹元素、display:none 预热渲染),渐变直接算成零面积——图标中心整块空白,换个页面布局又“莫名好了”。修法:改 gradientUnits="userSpaceOnUse",坐标钉死在 viewBox 用户坐标系,与包围盒脱钩。

炸弹二:emoji 当功能图标。主题切换钮用 🌙/☀️ 表达,Windows/macOS/Android 的 emoji 字形、尺寸、基线全不一样,跨平台视觉必然漂移。定稿换手绘矢量:月亮/太阳各一枚 path,描边加填充,currentColor 跟随主题变量:

<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
  <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/>
</svg>

五、为未来的白天模式留好门

现在全员暗、以后要加主站白天模式,最怕的是“当时钉死的暗”以后翻不动。三条留门规矩:

  1. 默认值集中:暗色只通过“存储键缺省 = night”表达,不散落 hardcode;白天模式上线 = 改缺省逻辑一处,用户手动偏好仍优先;
  2. 存储键对齐:各站主题键统一语义与取值(别一个用 theme:dark 一个用 mode:night),跨站跳变时才不会出现“主站亮、工作台暗”的撕裂;
  3. 回归用同一把尺:把本文的 luminance 探针存成常驻脚本,未来任何主题改动后重跑四站,10 秒出结论,不靠人眼。

六、可迁移清单

  • “默认暗黑”这类视觉状态断言,用新 profile + 零交互 + 像素 luminance三件套验收,代码审查只能当辅助;
  • 登录页 ≠ 应用本体,302 链上每个页面都要量;
  • 编译产物不可改时,index.html 注入层是主权阵地,但存储键与取值必须从 bundle 里抄真实值;
  • SVG 渐变用 userSpaceOnUse 更抗布局变化;功能图标一律矢量,不用 emoji;
  • 给未来的反向需求(白天模式)留单一缺省点,比任何文档都可靠。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注