VOID 2.0 版本起支持了夜间模式,其控制逻辑基础是 Jad 的这篇文章。最近在 Hran 那边看到了 macOS 10.14.4 及以上的 Safari 浏览器中通过媒体查询获得用户操作系统颜色偏好的方法,并且受到 iOS Nightshift 功能的启发,我决定使这个功能更上一层楼。
控制逻辑
本文主要关注功能逻辑,不讨论夜间模式样式方面的内容。为了得到良好的体验,这个功能需要前后端结合实现。
后端添加一个颜色模式的设置,分为「日间模式」、「夜间模式」、「自动模式」,其中:
- 日间模式:前后端均不做任何处理
- 夜间模式:后端直接输出 class 至 HTML 中,前端不处理
自动模式:
首先,为了防止前端闪烁,后端应该根据是否存在 cookie 来直接输出对应的 class 在 HTML 中。另外,前端的逻辑如下:
- 若操作系统为深色,则切换至深色,并设置较长的 cookie 过期时间,否则进行下一步
若能够获得地理位置,则计算该地日出日落时间,并且:
- 若处于夜晚,则切换至夜间模式并设置 cookie,至日出时 cookie 过期
- 若处于白天,切换至日间模式,清除 cookie
- 若不能获得地理位置,则以固定的时间作为日出日落时间,切换逻辑与 2 中相同
厘清逻辑后实现并不困难。剩下的部分说说实现中较为关键的步骤。
Cookie 的访问与设置
Cookie 用于在前端存储一些信息,常用于鉴权、保存标志位等。只要浏览器没有禁用 Cookie,前端的 Cookie 会随网络请求发送至后端,这使我们可以利用该技术为各用户(浏览器)提供针对性的服务。
在前端设置一个 Cookie:
var cookieString = '[NAME]=[VALUE];max-age=[AGE];path=[PATH]';
document.cookie = cookieString;
其中包括 [NAME]
、[AGE]
、[PATH]
参数,分别表示 Cookie 名,过期时间(秒),作用域。例如,设置 theme_dark=1
,过期时间 1 小时,作用域为 /
:
var cookieString = 'theme_dark=1;max-age=3600;path=/';
document.cookie = cookieString;
在后端读取一个 Cookie(PHP):
$_COOKIE['theme_dark']; // = '1'
其结果为一个字符串。更严谨的操作中需要先检查 $_COOKIE
数组中是否包含 theme_dark
字段。
操作系统深色模式检查
由于这个属性尚没有 JS API,Hran 给出了一个迂回方法。首先设定 CSS 属性:
.dark-mode-state-indicator {
position: absolute;
top: -999em;
left: -999em;
z-index: 1;
}
@media (prefers-color-scheme: dark) {
.dark-mode-state-indicator {
z-index: 11;
}
}
前端使用 JS 检查:
var getDeviceState = function(element) {
var zIndex;
if (window.getComputedStyle) {
// 现代浏览器
zIndex = window.getComputedStyle(element).getPropertyValue('z-index');
} else if (element.currentStyle) {
// ie8-
zIndex = element.currentStyle['z-index'];
}
return parseInt(zIndex, 10);
};
var getPrefersDarkModeState = function () {
var indicator = document.createElement('div');
indicator.className = 'dark-mode-state-indicator';
document.body.appendChild(indicator);
return getDeviceState(indicator) === 11;
};
getPrefersDarkModeState(); // true or false
地理位置获取
根据 MDN:
Navigator.geolocation
只读属性返回一个Geolocation
对象,通过这个对象可以访问到设备的位置信息。使网站或应用可以根据用户的位置提供个性化结果。
需注意,此 API 仅在 HTTPS 协议下、现代浏览器中可用,并且需要用户授权。根据我的实践,该 API 在不同浏览器中的行为并不是那么一致,若是更严肃的场合,可能需要使用百度等服务的 workaround。
检查浏览器是否支持该 API:
'geolocation' in navigator; // true or false
获取用户的位置信息:
navigator.geolocation.getCurrentPosition(function(position){
// success
console.log(position);
},function(data){
// failed
console.log(data);
});
getCurrentPosition
方法接受两个回调函数,第一个是成功时的回调,第二个是出错时的。出错时的回调中可以根据 data.code
获取出错原因,包括:
- PERMISSION_DENIED
- POSITION_UNAVAILABLE
- TIMEOUT
- UNKNOWN_ERROR
其中 PERMISSION_DENIED
表示用户手动禁止了网站访问位置,为了良好的体验,开发者应该向用户说明为什么会需要访问位置以及会如何使用位置信息,然后祈祷用户能重新赋予网站该权限。
时间计算与比较
日出日落时间
这个模块还是相对比较复杂的,其实我目前也并没有搞懂。但是令人开心的是已经有人为我们造好了轮子:Triggertrap/sun-js。这个库为原生的 Date 类注入了两个新的方法:
var sunset = new Date().sunset(latitude, longitude);
var sunrise = new Date().sunrise(latitude, longitude);
返回值是 Date 对象。结合 Geolocation,获取方法如下:
navigator.geolocation.getCurrentPosition(function(position) {
var sunset = new Date().sunset(position.coords.latitude, position.coords.longitude);
var sunrise = new Date().sunrise(position.coords.latitude, position.coords.longitude);
});
比较时间
sun-js 库得到的日出与日落时间根据当前时间不同不一定是当天的时间,例如晚间获取的日出时间其实是第二日的日出时间。考虑到 24 小时内日出日落时间不会有太大变化,为了方便比较,将日出日落时间均转换至同一天(当天),并且只精确至分钟。
navigator.geolocation.getCurrentPosition(function(position){
sunset = new Date().sunset(position.coords.latitude, position.coords.longitude);
sunrise = new Date().sunrise(position.coords.latitude, position.coords.longitude);
// 全部转换至当天
sunset = new Date(new Date().setHours(sunset.getHours(), sunset.getMinutes(), 0));
sunrise = new Date(new Date().setHours(sunrise.getHours(), sunrise.getMinutes(), 0));
}
如此确定当前是否处于夜间:
var current = new Date();
// 格式化为小时
var sunset_s = sunset.getHours() + sunset.getMinutes()/60;
var sunrise_s = sunrise.getHours() + sunrise.getMinutes()/60;
var current_s = current.getHours() + current.getMinutes()/60;
if(current_s > sunset_s || current_s < sunrise_s){
// 夜间
}else{
// 日间
}
然后计算当前距离日出的时间:
if(current_s > sunset_s) // 如果当前为夜晚,日出时间应该切换至第二日
sunrise = new Date(sunrise.getTime() + 3600000*24);
// 现在距日出还有 (s)
var toSunrise = (sunrise.getTime() - current.getTime())/1000; // 秒
这个时间就应该作为 Cookie 的过期时间,至日出时,该 Cookie 过期,网站则平滑地切换至日间模式。
代码
比较冗长,没必要贴在这里。我把代码摘出来建了一个 Gist,你可以点击查看:前往。
2019-04-06 更新
事实证明利用精确的位置来计算日出与日落大材小用了,并且随之而来的授权弹窗更是让浏览体验大打折扣。在评论区的建议下,增加了利用时区来获取大概位置,并计算相应日出日落时间的方法。
核心的功能依赖 jsTimezoneDetect,通过该库获取到时区名称后,将其转换为大致的位置(即时区名称对应城市的位置),然后再按照前文所述方法计算对应的日出与日落时间。我制作了一个时区名称到经纬度的转换表,点击这里查看。
如我在评论区中所述,仅使用时区来确定日出日落时间是不精确的,许多国家或地区只用一个时区(比如中国),但是从东到西时间差会很大(中国达到 4 小时之多)。不过权衡一下授权弹窗带来的差劲体验,这种误差也许可以接受吧。