鼓搗 blog 的時候一直覺得夜間模式和閱讀模式啥的挺好,Typecho 倒是有插件可以實現,但是 hexo 好像就比較難了,因為我用的主題沒有 pjax 的技術,所以每次刷新都可能使這些樣式失效。但是我又挺想整一個,於是找到了一個神奇的方案:JavaScript + CSS + PHP + Cookie。

主要思路

使用搜狐的 PV 統計的 API 獲取到的 IP,通過 ajax 提交數據,將 IP 映射到大致的經緯度,然後用 PHP 在後端計算日出日落的時間戳,存在 Cookie 裡,然後每次刷新用 JavaScript 讀取 Cookie 然後判斷是否需要渲染遮罩。(依賴 JavaScript 加載會導致每次刷新的時候遮罩沒有及時加載而有“閃爍”的效果)通過 PHP 文件讀取 Cookie 來判斷輸出對應的 CSS 樣式。

缺陷

在頁面中包含部分由 JavaScript 實現的動畫的時候,可能會導致元素在動畫區間內沒有被遮罩覆蓋。

實現

相關接口

首要解決的問題就是,使用 JavaScript 完成 IP 的獲取以及 IP 到經緯度的映射。這裡提供兩個咱找到的接口。

1
https://pv.sohu.com/cityjson?ie=utf-8

直接使用 script 標籤包括並放在頁面的 html 代碼中,就能得到如下數組。之後可以用 JavaScript 直接讀取。

1
var returnCitySN = {"cip": "YourIPAddressHere", "cid": "cidHere", "cname": "YourLocationHere"};

至於 IP 到經緯度的映射,我找到了一個 GeoIP 庫,但是速度並不是太快,這也是我使用 Cookie 來存儲數據的原因之一——減少請求次數。此接口只需要修改 GET 參數中的 ip 即可。

1
http://api.ipinfodb.com/v3/ip-city/?key=c9dcc88453e33a9e63ebad8d65f91583e87abd8185dd95f09fbeef6c62264f7d&ip=YourIPHere

返回的結果中會包括大致地點和大致經緯度以及時區

1
OK;;8.8.8.8;US;United States of America;California;Mountain View;94043;37.406;-122.079;-08:00
PHP 實現部分
α. 日出日落時間計算

這一部分主要就是根據前端的請求數據映射位置並計算日出日落的時間戳。關於時間戳的計算,PHP 提供了兩個現成的函數 不愧是 PHP。具體請參照 PHP Manual。

PHP Manual: data_sunrise()

PHP Manual: data_sunset()

β. CSS 輸出部分

這裡首先要做的就是在 hexo 模板文件的你喜歡的位置加入一個 div,並指定一個 div。這樣就可以在 CSS 中使用 id 作為篩選器而避免影響到其他元素。因為日出日落時間已經存在 Cookie 裡了,所以只需要在 PHP 裡讀取 Cookie 來判斷即可。這裡貼上我隨便碼的代碼(真就隨便碼的,上線測試沒幾個小時就下線了)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
header('Content-Type: text/css; charset=utf-8');
echo "#layerShade{";
echo "position:fixed;";
if(time() > $_COOKIE['sunset'] || time() < $_COOKIE['sunrise']){
echo "background-color:#000;";
}else{
echo "background-color:transparent;";
}
echo "top:0;";
echo "left:0;";
echo "z-index:2147483647;";
echo "pointer-events:none;";
echo "opacity:0.33;";
echo "width:100%;";
echo "height:100%;";
echo "}";

主要需要注意的地方就是 header,帶上這個才能輸出為 text/css,否則是 plain text,就不會被識別成 stylesheet。使用的時候只需要像一般的 CSS 一樣用 link 標籤在 html 中引用即可。

JavaScript 實現部分

由於要使用 JavaScript 來操作 Cookie 和發起請求,所以用 jQuery 會方便不少。因此,可以在模板中加上如下兩條語句。

1
2
<script src="https://cdn.bootcss.com/jquery-cookie/1.4.1/jquery.cookie.js"></script>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>

主要就是判斷 Cookie 是否存在、發起請求、保存 Cookie 這樣幾個步驟,其中的細節不再贅述,直接放上之前測試的時候寫的代碼。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
let timeNow = new Date();
let timestampNow = timeNow.getTime();
if($.cookie('sunrise') === undefined || $.cookie('sunset') === undefined || returnCitySN["cip"] !== $.cookie('ip')) {
console.log("IsNight? Run!");

//request data process
var details = {};
details["TIME"] = parseInt(new Date().getTime() / 1000);
details["IP_ADDRESS"] = returnCitySN["cip"];
let data = JSON.stringify(details);
data = BASE64.encode(data);
data = "PLKOZ23476/" + data;

//request for sunrise/sunset timestamp
$.ajax({
type: "GET",
async: false,
url: "https://api.lemonprefect.cn/?" + data,
success: function (data) {

//Setup Cookie expires till the next day
let isNigntArray = data.split(",");
let timeExpire = new Date(timeNow.toLocaleDateString()).getTime() - 1;
let timeLeftTillExpire = 24 * 60 * 60 * 1000 - (timestampNow - timeExpire);
let cookieValidTime = new Date();
cookieValidTime.setTime(timeLeftTillExpire + timestampNow);
$.cookie('sunrise',isNigntArray[0],{expires: cookieValidTime,path: '/'});
$.cookie('sunset',isNigntArray[1],{expires: cookieValidTime,path: '/'});
$.cookie('ip',returnCitySN["cip"],{expires: cookieValidTime,path: '/'});
},
error: function () {
console.log("Is Night?");
}
});
}
let timeNowCompare = parseInt(timestampNow / 1000);
if(timeNowCompare > $.cookie('sunset') || timeNowCompare < $.cookie('sunrise')){
console.log("Good Evening!");
}else{
console.log("Maybe it's daytime now.");
}
console.log("Timestamp Sunrise:" + $.cookie('sunrise'));
console.log("Timestamp Sunset:" + $.cookie('sunset'));

至此,將所有代碼寫好之後上線,就能組合在一起正常運作。