November 29, 2021
10月份我进行了一次暗黑模式的 code review
11月份我进行了一场 Docs 暗黑模式的分享,在此我整理成一篇博客,记录一下
本次分享带来的是 Docs 适配暗黑模式的来龙去脉、简要介绍开发方案、分享项目中踩过的坑、引起的思考,希望给大家带来帮助。此外工程上有一些改动,因此借此机会宣讲后续开发规范。
随着 iOS 13 和 Android 10 正式发布,暗黑(深色)模式逐渐进入大家视野,各大 APP 纷纷进行适配。MacOS 为用户提供了“浅色”、“深色”两种模式进行切换,暗黑对于程序员来说很容易接受,常见的 IDE 默认主题一般都是深色背景。 暗黑模式的好处:
效果预览:
回顾项目开发的过程:
由于需求排期较长,在项目初期进行了细化的排期
9.29-10.12 色值排查与替换(使用脚本),整理用到的组件(包括 不规范的色值、单色/多色 icon、图片 ) 10.13-10.18 组件样式改造(基于 antd 本地化分支) 10.19-10.25 icon、图片整理与替换(可能延期2天) 走查与测试: 10.13,设计师开始色值走查 10.25,设计师开始全局走查 11.01,设计师走查延后两周 11.8-11.17 共计8PD,前端 0.5 * 1 人力支持,设计走查 0.2 * 3 人力 11.17-11.19 测试介入 11.24 全量上线代码 关键文档:组件梳理文档,分模块梳理各个页面用到的 单色 icon、多色 icon 和图片,三个部分
使用了 css variable 修改主题色,经调研符合目前系统的兼容性要求
初始化时,在 js 逻辑中根据 isKim & 媒体查询,给 html 标签添加 className: .kim-dark-mode
监听 media 的 change 事件,回调时修改 html 标签的 className
// config/public/css-variables.ejs
:root {
--Gray01: #ffffff;
}
:root.kim-dark-mode {
--Gray01: #17181A;
}
// packages/ui/style/colors.less
@Gray01: var(--Gray01);
// packages/ui/style/utils.less
// 媒体查询 暗黑模式特殊判断逻辑
// 传入代码片段, IMP: 注意外层不能嵌套选择器
.darkRules(@rules) {
html.kim-dark-mode {
@rules();
}
}
没有使用 less 的 modifyVars 方法,modifyVars 方法基于 less 在浏览器中的编译,工程改造成本较高
使用 css variables 的形式定义变量
基于 colors-src.ts 文件构建出三个文件(npm run buildColor)
ts 使用方法如下(不推荐,可能存在副作用):
// packages/ui/style/colors.ts 源文件
import themes from './color-src';
import { isKimDark } from '@is-docs-packages/components/common-utils/dark';
const mode = isKimDark ? 'dark' : 'light';
const themeColors = themes[mode];
export const Gray01 = themeColors.Gray01;
// xxx.tsx 使用侧
import { colors } from '@is-docs-packages/ui/style/colors';
colors.Gray01
基于上述色值切换原理,使用色值收敛脚本替换颜色,自动实现 业务组件 暗黑模式切换
原理:编写 node 脚本,遍历项目各个目录中的文件,使用正则进行字符串替换
css变量中,十六进制色值修改为 @Gray01 的形式;rgba 修改为 rgba(var(—Gray01RGB), 0.2); 的形式。目前 docs 项目都迁移成了变量与 fade 形式 (除知识库),改色值比较容易
基于 ant design 本地化,修改变量文件,实现 ant design 组件 的暗黑模式切换
ant design 的色值未支持暗黑模式,为了实现暗黑模式,需要将 ant design 的色值变量全部替换为支持切换暗黑模式的色值
Docs 项目使用了很多 ant design 的组件(首页居多),以前使用样式覆盖,修改不方便。本地化后的目录是组件库项目的 【packages/ui/docs-antd】目录,以后修改 ant design 组件的基础样式都在这个目录下修改
本地化后,适配暗黑模式更方便
ant design 色彩系统使用了函数计算,根据某一个基准色值生成一条渐变色板,优势是 样式变量高度收敛,劣势是 定制性比较差,不够灵活,3.x后色板算法没再更新,计算得出的某些色值 无法达到足够适合阅读的对比度 。关于 ant design 色板生成算法 的原理、现存问题,有兴趣的同学可以看我的博客
我们 Docs 项目有比较强的定制性需求,因此色板使用的是设计师定义的色值,但需要设计师维护所有色值,更新色值相对麻烦一些
@primary-1: color(~`colorPalette('@{primary-color}', 1) `); // replace tint(@primary-color, 90%)
@primary-2: color(~`colorPalette('@{primary-color}', 2) `); // replace tint(@primary-color, 80%)
@primary-3: color(~`colorPalette('@{primary-color}', 3) `); // unused
@primary-4: color(~`colorPalette('@{primary-color}', 4) `); // unused
@primary-5: color(~`colorPalette('@{primary-color}', 5) `;
@primary-6: @primary-color;
@primary-7: color(~`colorPalette('@{primary-color}', 7) `); // replace shade(@primary-color, 5%)
@primary-8: color(~`colorPalette('@{primary-color}', 8) `); // unused
@primary-9: color(~`colorPalette('@{primary-color}', 9) `); // unused
@primary-10: color(~`colorPalette('@{primary-color}', 10) `); // unused
// 转换成
@primary-1: @Blue09;
@primary-2: @Blue08;
@primary-3: @Blue07;
@primary-4: @Blue06;
@primary-5: @Blue05;
@primary-7: @Blue04;
@primary-8: @Blue03;
@primary-9: @Blue02;
@primary-10: @Blue01;
转换过程使用了 tinyColor 第三方库的 readability 函数,该函数返回两种颜色之间的对比度,需要先确定该色值所属的色板,相对来说更加准确,我的策略是先替换再走查
另一种方式: 之前第一版收敛色值时策略是先走查再替换:使用的是 deltaE 来计算颜色的相似性,梳理了各工程用到的色值,和设计师给出的色板进行了对应,梳理页面如下:
设计师对梳理的结果进行微调,确定最终替换脚本的规则,附:
在 js 中计算 deltaE: https://stackoverflow.com/questions/54738431/calculate-deltae94-in-javascript
第三方库 chroma 也有实现这个功能: https://gka.github.io/chroma.js/#chroma-deltae
更新的一般包括图片、tsx 中的色值
组件库 is-docs-components/packages/components/component-ctx.tsx
文件提供了暗黑模式的 context 实例,包含 isDark: boolean; theme: 色值表 这两个变量,Docs 首页项目在 App.tsx 中使用该 context,并提供 Provider
其他工程没有提供 Provider,则取到的 isDark 为 false,即不会影响未接入暗黑模式的工程
原理:
iconfont.cn 项目中,暗黑模式的命名是在对应的浅色模式 icon 后面加 “_dark” 后缀
执行脚本【npm run buildIcon(scripts/dark-icons-build.js)】
从 iconfont 平台下载后,读取下载的 iconfont 文件,根据每个 svg 标签的 key 值是否包含 “_dark”后缀 打包成两个 iconfont.js 文件,初始化时根据 isKim & media isDark 确定引入的是 fontxxx.js 或者 fontxxx_dark.js
媒体查询监听暗黑模式 onchange 时,删除 html 中对应的 svg sprite 标签,拼接新 script 标签(iconfont.js 文件)并 append 到 dom 中:
// 执行 dark-icons-build.js 时,给 svg 标签加上 xmlns:theme 自定义属性,以便通过选择器查找
document.querySelectorAll(`svg[xmlns\\:theme=${isDark ? 'light' : 'dark'}]`)[0].remove();
const script = document.createElement('script');
script.src = isDark ? PC_ICON_URL_DARK : PC_ICON_URL;
document.body.appendChild(script);
Icon 的 key 值不变,但对应的 Symbols 中引用的 iconfont path 已经改变,从而实现切换 light/dark 模式的 icons
封装 <IconFont /> 组件,color 属性通过 props 的形式传入色值的 key 值,传入错误的 key 值在开发环境会报 warning
推荐通过 className & less 写色值,而不是通过 color props 传入
<BasicActiveRectIcon
type="k-docs-icon-cebianshouqi"
style={{ flexShrink: 0 }}
color="red"
/>
<IconFont
style={{ fontSize: '16px', color: colors.Gray09 }}
color="Gray09"
/>
通用图片收敛到组件库的 packages/components/assets/dark[light] 目录
两个目录: 根目录 & dark
根据 context 中的 isDark flag,使用相同/不同图片
import nullImg from '@src/assets/light/null.png';
import nullImgDark from '@src/assets/dark/null.png';
import useThemeContext from '@is-docs-packages/hooks/useThemeContext';
const [isDark] = useThemeContext();
<Empty image={isDark ? nullImgDark : nullImg} />
测试与走查方案:
a. docs 测试环境配置灰度,由于 Kima 包尚未准备好,docs 首页走查时前期是在 web 端走查,注释了 isKim 判断是否在 kim 内的逻辑
b. 测试打出暗黑模式专用的 Kima 包,docs 标签页使用的是 docs 测试环境
灰度方案
a. Kim 端计划在12月发布灰度版本,灰度范围逐步扩大,Docs 与 Kim 保持灰度范围一致
b. 新增的需求使用色值变量,自动实现暗黑模式,需求测试/走查时应注意回归暗黑模式样式
上线方案
a. 11.25 全量上线暗黑模式代码,目的是代码 尽早 合入master,增量的业务尽早适配暗黑模式的写法,否则新需求代码合进来都需要为适配暗黑模式做转换
b. 上线之后通过 分享 的形式对齐工程与规范的变动
c. 上线后灰度范围 与 Kim 保持一致,直到 kim 全量上线
内部文档:GUI 一致性 2.0,不便对外展示,列举出部分目录,大体能看到包含的内容:
修改组件库样式
a. 修改 ant design 组件的默认样式
b. 历史遗留:修改自定义 ant design 组件样式
c. 组件库的正确使用姿势:
场景色如下:
PrimaryColor: colorBase.light.Blue03, // 主色 (@Blue03)
CardBg: colorBase.light.Gray01, // 卡片背景色
CardNestedBg: colorBase.light.Gray02, // 卡片内嵌套区域背景色
// 主按钮 - primary
ButtonPrimaryBg: colorBase.light.Blue03, // 主按钮背景色
ButtonPrimaryText: colorBase.light.Gray01, // 主按钮文本色
ButtonPrimaryBgHover: colorBase.light.Blue04, // 背景色 - hover
ButtonPrimaryTextHover: colorBase.light.Gray01, // 文本色 - hover
ButtonPrimaryBgActive: colorBase.light.Blue02, // 背景色 - active
ButtonPrimaryTextActive: colorBase.light.Gray01, // 文本色 - active
ButtonPrimaryBgDisabled: colorBase.light.Blue05, // 背景色 - disabled
ButtonPrimaryTextDisabled: colorBase.light.Gray01, // 文本色 - disabled
// 次级按钮 - secondary
ButtonSecondaryBg: colorBase.light.Gray01, // 次级按钮背景色
ButtonSecondaryText: colorBase.light.Gray11, // 次级按钮文本色
ButtonSecondaryBgHover: colorBase.light.Gray02, // 背景色 - hover
ButtonSecondaryTextHover: colorBase.light.Gray11, // 文本色 - hover
ButtonSecondaryBgActive: colorBase.light.Gray03, // 背景色 - active
ButtonSecondaryTextActive: colorBase.light.Gray11, // 文本色 - active
ButtonSecondaryBgDisabled: colorBase.light.Gray03, // 背景色 - disabled
ButtonSecondaryTextDisabled: colorBase.light.Gray09, // 文本色 - disabled
// 幽灵按钮 - ghost
ButtonGhostBg: colorBase.light.Gray01, // 幽灵按钮背景色
ButtonGhostText: colorBase.light.Blue03, // 幽灵按钮文本色
ButtonGhostBgHover: colorBase.light.Blue09, // 背景色 - hover
ButtonGhostTextHover: colorBase.light.Blue03, // 文本色 - hover
ButtonGhostBgActive: colorBase.light.Blue08, // 背景色 - active
ButtonGhostTextActive: colorBase.light.Blue03, // 文本色 - active
ButtonGhostBgDisabled: colorBase.light.Gray03, // 背景色 - disabled
ButtonGhostTextDisabled: colorBase.light.Gray09, // 文本色 - disabled
ContainerBorder: rgbaString(colorBase.light.Gray11, 0.12), // 容器 border
DividerColor: rgbaString(colorBase.light.Gray11, 0.08), // 全局分割线
SelectColor: rgbaString(colorBase.light.GrayBlue01, 0.08), // item select
HoverColor: rgbaString(colorBase.light.GrayBlue01, 0.05), // item hover
ModalMaskBg: rgbaString(colorBase.light.Gray14, 0.45), // modal 半透明遮罩
CardBoxShadow: rgbaString(colorBase.light.Gray11, 0.04), // 卡片阴影
TooltipBg: colorBase.light.Gray11, // tooltip 背景色
TooltipText: colorBase.light.Gray01 // tooltip 文本色
中间插入需求
a. toast 样式改版——中间插入消化
b. 线上样式问题—— 样式迭代,量大,本期不改
设计师要求增加排期(toast)
a. 使用甘特图梳理排期;
b. 拆解大任务为小任务,每个任务定具体的交付时间节点,到节点时或邻近节点时确认;
c. 了解各方诉求,寻求合适的方案
与组内同学沟通
a. 已建立的规范 —— 色值一致性
b. 提高效率 —— less 插件 c. ant design 本地化 d. 移动端入侵 e. 上线前与工程 owner 沟通,确定下周上线的需求,提前合并代码(危)
f. 进行 code review 或 workshop ,聆听大家的建议
与其他部门沟通
a. 设计师高频沟通
i. 开发、测试方案 ii. 摹客上面设计规范修改
b. 测试、产品
i. 11.16 开会确认当前进度与测试方案 暗黑模式进度
ii. 提前与 qa 确认测试时间及排期
c. Kim 端
i. 暗黑模式配置的开关能力如何通知 Docs
ii. Kim 端的方案与规范,统一认知与目标
iii. 测试时打包
iv. 沟通灰度与上线方案
其他工程接入包含暗黑模式的组件库代码,需要调整代码