众所周知,深色(暗黑)模式(体验本站的深色模式,PC端可以点击页面右上角的月亮/太阳图标,移动端可以点击页面左上角进入菜单后点击月亮/太阳图标)主要是通过CSS的媒体查询(MediaQuery)实现的,如下:

cssCopy code
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
/* Light mode */ @media (prefers-color-scheme: light) { body { ... } } /* Dark mode */ @media (prefers-color-scheme: dark) { body { ... } }
/* Light mode */ @media (prefers-color-scheme: light) { body { ... } } /* Dark mode */ @media (prefers-color-scheme: dark) { body { ... } }

其语法为:

cssCopy code
  • 1
@media (prefers-color-scheme: <light|dark>)
@media (prefers-color-scheme: )

浏览器兼容情况如下图:

深色模式浏览器支持情况
深色模式浏览器支持情况

然而,仅此而已吗?

实际上,还有一系列的需求:

  1. 支持手动切换深色模式的同时,也支持动态切换深色模式。
  2. 对于不支持深色模式的系统环境,需要提供替代方案。
  3. 缓存用户设置偏好,以便在刷新时或某些特殊环境下(如黑暗的室内)不会出现重置情况。
  4. 以CSS主题的方式抽象出深色模式的定义,摆脱CSS样式的重复定义,提高效率和代码可维护性。

1、深色模式监听

JS中,同样是通过媒体查询来实现深色模式的判断的:

typescriptCopy code
  • 1
  • 2
const isLight = window.matchMedia('(prefers-color-scheme: light)').matches; const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const isLight = window.matchMedia('(prefers-color-scheme: light)').matches; const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;

其(matchMedia)浏览器支持情况较prefers-color-scheme media query更为广泛,具体情况可以访问MDNcaniuse。

通过MDN文档,可以看到,matchMedia返回结果(MediaQueryList)继承自EventTarget,因此,可以直接调用addEventListenerremoveEventListener。如此,实现深色模式的监听也就很简单了,如下:

typescriptCopy code
  • 1
  • 2
  • 3
window.matchMedia(MEDIA_QUERY_THEME_DARK).addEventListener('change', (event) => { this.commonService.setTheme(event.matches ? Theme.Dark : Theme.Light); });
window.matchMedia(MEDIA_QUERY_THEME_DARK).addEventListener('change', (event) => { this.commonService.setTheme(event.matches ? Theme.Dark : Theme.Light); });

至于上述代码中的setTheme方法,作用是在HTML节点上设置属性标识是否深色模式,并发送事件通知。如下:

typescriptCopy code
  • 1
  • 2
  • 3
  • 4
  • 5
setTheme(theme: Theme) { const htmlNode = this.document.getElementsByTagName('html')[0]; htmlNode.setAttribute('data-theme', theme); this.darkMode.next(theme === Theme.Dark); }
setTheme(theme: Theme) { const htmlNode = this.document.getElementsByTagName('html')[0]; htmlNode.setAttribute('data-theme', theme); this.darkMode.next(theme === Theme.Dark); }

2、不支持深色模式的替代方案

并不是所有操作系统都支持深色模式,因此需要提供一个替代方案,在不支持深色模式的系统环境中也提供深色模式的浏览体验。代码如下:

typescriptCopy code
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
getTheme(): Theme { const cacheTheme = this.cookieService.get(STORAGE_KEY_THEME); if (cacheTheme) { return cacheTheme === Theme.Dark ? Theme.Dark : Theme.Light; } if (this.platform.isBrowser) { if (window.matchMedia(MEDIA_QUERY_THEME_DARK).matches) { return Theme.Dark; } if (window.matchMedia(MEDIA_QUERY_THEME_LIGHT).matches) { return Theme.Light; } } const curHour = new Date().getHours(); const isNight = curHour >= 19 || curHour <= 6; return isNight ? Theme.Dark : Theme.Light; }
getTheme(): Theme { const cacheTheme = this.cookieService.get(STORAGE_KEY_THEME); if (cacheTheme) { return cacheTheme === Theme.Dark ? Theme.Dark : Theme.Light; } if (this.platform.isBrowser) { if (window.matchMedia(MEDIA_QUERY_THEME_DARK).matches) { return Theme.Dark; } if (window.matchMedia(MEDIA_QUERY_THEME_LIGHT).matches) { return Theme.Light; } } const curHour = new Date().getHours(); const isNight = curHour >= 19 || curHour <= 6; return isNight ? Theme.Dark : Theme.Light; }

代码分解如下;

  1. 默认先获取缓存的深色模式设置;
  2. 如无,再判断是否处于浏览器环境,且是否支持对应的媒体查询,支持则返回当前匹配的模式;
  3. 不支持,则判断当前系统时间是否处于夜间,是则返回深色模式。

更进一步,对于时间的判断,可再考虑用户的自定义设置以及用户所处的地区/时区(日出、日落时间)。

3、缓存用户深色模式设置

缓存可以有多种方式,包括:

  1. URL参数
  2. 保存在服务器中
  3. localStorage、sessionStorage
  4. cookie

各种方式的优劣此处不赘述,简单比较下localStorage/sessionStoragecookie两种。前者的问题在于缓存设置缺乏有效期(或只能设置为会话有效期),且服务端无法获取到缓存设置(需要前端再做一层处理)。因此,通过cookie方式缓存要更简单、合理。如下:

typescriptCopy code
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
cacheTheme(theme: Theme) { this.cookieService.set(STORAGE_KEY_THEME, theme, { path: '/', domain: environment.cookie.domain, expires: environment.cookie.expires }); }
cacheTheme(theme: Theme) { this.cookieService.set(STORAGE_KEY_THEME, theme, { path: '/', domain: environment.cookie.domain, expires: environment.cookie.expires }); }

4、CSS深色模式主题

对于深色模式的CSS定义,并不是简单扔进@media (prefers-color-scheme: dark) {...}块中即可,更合理的方式是定义一系列的CSS变量,并在样式定义中引用。

而CSS的变量定义又可以有两种方式:

一种是@前缀方式:

lessCopy code
  • 1
@base-color: #333;
@base-color: #333;

另一种是var方式:

lessCopy code
  • 1
--base-color: #333;
--base-color: #333;

对于此处的深色模式场景,显然需要后者(无需import导入)。代码如下:

lessCopy code
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
:root { &[data-theme='light'] { --base-color: #333; ... } &[data-theme='dark'] { --base-color: rgba(255, 255, 255, .6); ... } }
:root { &[data-theme='light'] { --base-color: #333; ... } &[data-theme='dark'] { --base-color: rgba(255, 255, 255, .6); ... } }

调用代码如下:

lessCopy code
  • 1
  • 2
  • 3
body { color: var(--base-color); }
body { color: var(--base-color); }

至此,完美解决了上述4个问题,实现了理想的Angular深色(暗黑)模式。[]~( ̄▽ ̄)~*