深色模式在 Web 端的适配方案

avatar
前端工程师 @上海阅文信息技术有限公司

本文作者: YFE 起点前端组

原创声明:本文为阅文前端团队 YFE 成员出品,请尊重原创,转载请联系公众号 ( id: yuewen_YFE ) 获取授权,并注明作者、出处和链接。

背景

随着 Mac OS、iOS 系统发布了深色外观,手机 APP 的深色模式开始出现在了大众视野,越来越多的原生 APP 为增强体验去适配 Dark Mode 深色模式。

起点读书 APP 也在近期进行了适配,包括原生页面以及 内嵌Web 端的页面 。那我们应该如何适配,才能满足既要快速响应需求又能降低开发成本呢 ?

方案选择 - CSS 自定义变量

为什么使用该方案?

首先,由我们设计师指定的 《色彩映射规范》 会提供给前端开发人员。 图片 然后我们会用 CSS 自定义属性去搭建一套基本的主题色 ,再通过 class 切换去更换所有需要映射改变的颜色,从而达到一种整体换肤的效果。若之后需要再扩展其他皮肤,我们只需按照设计师提供的新映射规则,去新增一套新的皮肤色就能轻松实现整个站点的换肤效果。

这样做主要有以下几个优点

  • 维护成本低:每次修改皮肤颜色只需局部修改颜色变量,无需按照以前传统的方式再对逐个页面去做颜色的修改,大大降低了响应需求的工作量。
  • 无破坏性:每次增加或修改颜色映射时不会引起各个页面的任何样式或逻辑代码问题。
  • 视觉还原统一性:任何使用了这套主题色系统的页面不会存在与设计稿颜色偏差的问题。
  • 接入效率快:接入新的主题皮肤色的工作量主要是设计师层面,对于内嵌页 H5 就是增加一套颜色映射规则。

核心实现过程

1.由于是基于 APP 切换模式,我们需要使用 JSSDK 调用客户端接口来获取当前颜色模式。

2.在 HTML 节点上添加模式 class 来指定当前页面的主题,如深色模式的 class 为:dark-mode

// 摘自 utils.js 获取系统颜色模式
utils.checkThemeMode().then(theme => {
    const localTheme = window.localStorage.getItem('local_theme');
    if (localTheme) theme = parseInt(localTheme);
    this.$store.commit('setThemeModel', theme);
    document.getElementsByTagName('html')[0].className = this.$store.state.global.themeClass;
    console.log('系统的颜色模式:' + theme);
});
// 摘自 global.js 设置系统的颜色模式
setThemeModel(state, value) {
    state.themeMode = value;
    switch (value) {
        case 0:
            state.themeClass = '';
            break;
        case 1:
            state.themeClass = 'dark-mode';
            break;
    }
}

3.在 dark-mode 模式下更换所有 css var 的颜色变量,根据设计师映射表的要求统一换色。

/* 摘自 themeVar.scss 和 global.scss */
$--Primary_500: #E5353E;
:root {
    --Primary_500: #{$--Primary_500};
}
.dark-mode {
    --Primary_500: #FF4D55;
}

4.在浏览器里写入本地缓存 local_theme,与 JSSDK 接口方法对齐,默认为 0,1 为深色模式。在方便本地调试的同时,也可以应对万一接口报错时进行容错。

图片

遇到的问题

低端机型如何处理

图片 移动端主要是低端机型系统例如 iOS 9.2 及以下 / 安卓 4.4 及以下不支持。据统计,这些设备在起点读书 App 中的用户数量已经不足 0.3% 。这样的兼容性已足以允许让我们在项目中开始使用 CSS 自定义属性,并对一些低端版本进行降低兼容的处理。

我们需要为所有的自定义属性颜色多写一个 SASS 变量作为保底颜色,例如:$--Primary_500: #E5353E

然后需要这样来兼容旧设备:(旧设备不支持时,会使用第一个属性的颜色)

color: $--Primary_500;
color: var(--Primary_500);

如何解决每次重复书写 2 次问题

为了提高开发时的代码编写效率,我们开发了自定义 Loader:reset-scss-loader 来对 SCSS 源码进行自动化构建处理,如编写以下 SCSS 源码:

.bottom-wrap {
    color: $--Surface_500;
    border: 1px solid $--Background_BW_White;
    background: linear-gradient(270deg, $--Gradient_Red_500_end 0%, $--Gradient_Red_500_start 100%);
}

那么通过我们的 loader 转译后为: 图片 可以看到,color: $--Surface_500 的语法被转译出来一份供旧设备使用的降级 CSS 代码:

color: #808080;
color: var(--Surface_500);

而旧设备则会忽略第二行的代码,新设备则会优先使用第二行的代码,由此达到优雅降级目的。 reset-scss-loader 接入需在 webpack 加入以下配置,exclude 则可以剔除不需编译的文件。

module: {
    rules: [
        {
            test: /\.scss$/,
            use: ['reset-scss-loader'],
            exclude: path.resolve(__dirname, './src/assets/css/global.scss')
        }
    ]
}

reset-scss-loader 原理是对 SCSS 内进行正则匹配和替换。

module.exports = str => {
    return str.replace(/([a-z-]+:[^;]*\$--[^;]+;)/gu, $1 => $1 + $1.replace(/\$(--[^\s;]*)(\s|;)/g, 'var($1)$2'));
};

作用域

由于我们使用的 $store 状态管理是加在页面顶层容器的 div 上,在某些页面还没有渲染之前,还有一些特殊的 通用型元素 ,比如 body 以内的平级兄弟元素,例如:loading 层等等,我们需要给 html 加上 class:dark-mode。然后在这些平级组件上也单独添加一下深色模式时的展示颜色规则,待整个项目的页面全部适配深色模式后,再和 CSS 自定义变量绑定映射关系,从而达到全局控制换肤的功能。

全站适配进度

目前已适配深色模式的模块有:新账户页、签到页、会员畅享卡页。

图片

总结与思考

CSS 自定义变量目前已支持各主流浏览器,低端版本可以采用兼容方案。更换主题的方法有很多种,从传统的换样式文件,到逐页面刷一套主题色。以往的方法都是维护成本较高、工作量逐步叠加,且改动风险较大的方案。CSS 自定义变量的支持随着时间的逐渐推移,浏览器的支持度也越来越好,使得实现主题换肤是一件很轻松的事,甚至今后还可以直接读取管理台的配置来更改 CSS 自定义属性的颜色,从而达到服务端下发主题换色的功能。

起点读书 App 体验 图片