众所周知,深色(暗黑)模式(体验本站的深色模式,PC端可以点击页面右上角的月亮/太阳图标,移动端可以点击页面左上角进入菜单后点击月亮/太阳图标)主要是通过CSS的媒体查询(MediaQuery
)实现的,如下:
- 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 {
...
}
}
其语法为:
- 1
@media (prefers-color-scheme: <light|dark>)
浏览器兼容情况如下图:

然而,仅此而已吗?
实际上,还有一系列的需求:
- 支持手动切换深色模式的同时,也支持动态切换深色模式。
- 对于不支持深色模式的系统环境,需要提供替代方案。
- 缓存用户设置偏好,以便在刷新时或某些特殊环境下(如黑暗的室内)不会出现重置情况。
- 以CSS主题的方式抽象出深色模式的定义,摆脱CSS样式的重复定义,提高效率和代码可维护性。
1、深色模式监听
JS中,同样是通过媒体查询来实现深色模式的判断的:
- 1
- 2
const isLight = window.matchMedia('(prefers-color-scheme: light)').matches;
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
其(matchMedia
)浏览器支持情况较prefers-color-scheme
media query更为广泛,具体情况可以访问MDN或caniuse。
通过MDN文档,可以看到,matchMedia
返回结果(MediaQueryList
)继承自EventTarget
,因此,可以直接调用addEventListener
、removeEventListener
。如此,实现深色模式的监听也就很简单了,如下:
- 1
- 2
- 3
window.matchMedia(MEDIA_QUERY_THEME_DARK).addEventListener('change', (event) => {
this.commonService.setTheme(event.matches ? Theme.Dark : Theme.Light);
});
至于上述代码中的setTheme
方法,作用是在HTML节点上设置属性标识是否深色模式,并发送事件通知。如下:
- 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);
}
2、不支持深色模式的替代方案
并不是所有操作系统都支持深色模式,因此需要提供一个替代方案,在不支持深色模式的系统环境中也提供深色模式的浏览体验。代码如下:
- 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;
}
代码分解如下;
- 默认先获取缓存的深色模式设置;
- 如无,再判断是否处于浏览器环境,且是否支持对应的媒体查询,支持则返回当前匹配的模式;
- 不支持,则判断当前系统时间是否处于夜间,是则返回深色模式。
更进一步,对于时间的判断,可再考虑用户的自定义设置以及用户所处的地区/时区(日出、日落时间)。
3、缓存用户深色模式设置
缓存可以有多种方式,包括:
- URL参数
- 保存在服务器中
- localStorage、sessionStorage
- cookie
各种方式的优劣此处不赘述,简单比较下localStorage/sessionStorage
和cookie
两种。前者的问题在于缓存设置缺乏有效期(或只能设置为会话有效期),且服务端无法获取到缓存设置(需要前端再做一层处理)。因此,通过cookie
方式缓存要更简单、合理。如下:
- 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
});
}
4、CSS深色模式主题
对于深色模式的CSS定义,并不是简单扔进@media (prefers-color-scheme: dark) {...}
块中即可,更合理的方式是定义一系列的CSS变量,并在样式定义中引用。
而CSS的变量定义又可以有两种方式:
一种是@
前缀方式:
- 1
@base-color: #333;
另一种是var
方式:
- 1
--base-color: #333;
对于此处的深色模式场景,显然需要后者(无需import
导入)。代码如下:
- 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);
...
}
}
调用代码如下:
- 1
- 2
- 3
body {
color: var(--base-color);
}
至此,完美解决了上述4个问题,实现了理想的Angular深色(暗黑)模式。[]~( ̄▽ ̄)~*