前言

随着苹果发布 iOS 13 正式版,推出了备受期待的深色模式(Dark Mode)。因该特性在浏览时提供更好的可视性和沉浸感。支持深色模式已然成为现代移动应用和网站的一个潮流趋势。

那么就跟随趋势,开始进行深色模式的适配吧。

技术方案

CSS 媒体查询

prefers-color-scheme 是一种用于检测用户是否有将系统的主题色设置为亮色或者暗色的 CSS 媒体特性。利用其设置不同主题模式下的 CSS 样式,浏览器会自动根据当前系统主题加载对应的 CSS 样式。light 适配浅色主题,dark 适配深色主题,no-preference 表示获取不到主题时的适配方案。

@media (prefers-color-scheme: light) {
  .content {
    color: #333;
    background-color: #fff;
  }
}

@media (prefers-color-scheme: dark) {
  .content {
    color: #fff;
    background-color: #292934;
  }
}

先来看一下效果,将系统设置为浅色外观:

然后将系统设置为深色外观:

CSS 变量

在项目中,往往需要写大量的 CSS 样式类名,如果把样式根据不同的外观模式各写一份,其工作量可想而知。而通过将不同外观模式下的颜色定义为 CSS 变量。在外观模式切换时,只需修改颜色变量即可。

// 示例代码
:root {
  --white: #fff;
  --gray: #333;
  --text-color: var(--gray);
}

@media (prefers-color-scheme: light) { 
  --text-color: var(--gray);
}

@media (prefers-color-scheme: dark) { 
  --text-color: var(--white);
}

.container {
  color: var(--text-color);
}

Window.matchMedia

浏览器提供了 window.matchMedia 方法,可以用来查询指定的媒体查询字符串解析后的结果。

if (window.matchMedia('prefers-color-scheme: dark').matches) {
  // 深色模式做什么
} else {
  // 浅色模式做什么
}

另外还可以监听系统外观模式的状态:

window.matchMedia('(prefers-color-scheme: dark)').addListener(e => {
  if (e.matches) {
    // 系统开启深色模式后做什么
  } else {
    // 系统开启浅色模式后做什么
  }
});

项目实践

以 vue-cli 搭建的应用为例。

组件库定制主题

项目中大都引用第三方开源组件库,组件库一般会使用 Sass、Less 等 CSS 预处理器定义颜色变量作为组件的基础色值,可以修改基础色值来自定义主题和深色模式适配。

例如使用 vant 组件库,代码如下:

:root {
  --white: #fff;
  --gray-1: #4E4C56;
  --gray-2: #f8f8f8;
  --dark-1: #292934;
  --dark-2: #23232B;
}

@media (prefers-color-scheme: light) { 
  --text-color: var(--gray-1);
  --background-color: var(--gray-2);
  --card-background-color: var(--white);
}

@media (prefers-color-scheme: dark) { 
  --text-color: var(--white);
  --background-color: var(--dark-2);
  --card-background-color: var(--dark-1);
}

// 覆盖 vant less 样式变量
@text-color: var(--text-color);
@tabbar-background-color: var(--card-background-color);

更多关于 vant 定制主题的内容,可查阅官方文档

图片显示

如果一张图是暗色调,在明亮模式色彩对比度强、观看流畅,但在暗黑模式下便会存在和背景色对比度弱,不方便查看。所以需要在不同模式下显示不同的图片。实现方式就是使用 picture 元素。

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/25423296/163456776-7f95b81a-f1ed-45f7-b7ab-8fa810d529fa.png" />
  <img src="https://user-images.githubusercontent.com/25423296/163456779-a8556205-d0a5-45e2-ac17-42d089e3c3f8.png" />
</picture>

用户设置

应用应该允许用户主动去选择外观模式,设置后外观模式将不再跟随系统设置。

具体实现的核心代码如下:

function getThemeMode () {
  return localStorage.getItem('THEME_MODE')
}

function setThemeMode (value) {
  localStorage.setItem('THEME_MODE', value)
}

export default new Vuex.Store({
  state: {
    themeMode: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
  },
  getters: {
    themeMode (state) {
      return getThemeMode() || state.themeMode
    }
  },
  mutations: {
    SET_THEME_MODE (state, mode) {
      state.themeMode = mode
    }
  }
})

在根节点上添加 theme-mode 属性,使应用初次加载时,也可渲染当前设置的外观模式效果。

<template>
  <div id="app" :theme-mode="themeMode"></div>
</template>

<script>
export default {
  computed: {
    themeMode () {
      return this.$store.getters.themeMode
    }
  },
  mounted () {
    window.matchMedia('(prefers-color-scheme: dark)').addListener((e) => {
      this.$store.commit('SET_THEME_MODE', e.matches ? 'dark' : 'light')
    })
  },
}
</script>
:root {
  --white: #fff;
  --gray-1: #4E4C56;
  --gray-2: #f8f8f8;
  --dark-1: #292934;
  --dark-2: #23232B;
}

.theme-mode-light {
  --text-color: var(--gray-1);
  --background-color: var(--gray-2);
  --card-background-color: var(--white);
}

.theme-mode-dark {
  --text-color: var(--white);
  --background-color: var(--dark-2);
  --card-background-color: var(--dark-1);
}

#app[theme-mode=light] {
  &:extend(.theme-mode-light);
}

#app[theme-mode=dark] {
  &:extend(.theme-mode-dark);
}

@media (prefers-color-scheme: light) { 
  :root {
    &:extend(.theme-mode-light);
  }
}

@media (prefers-color-scheme: dark) { 
  :root {
    &:extend(.theme-mode-dark);
  }
}

总结

本文介绍了深色模式适配方案和项目中会遇到的问题。如果有错误的地方或者有更好的解决想法,欢迎留言讨论。