一直以來常被 JSON 日期序列化時區問題困擾,問題主要發生於從資料庫查詢日期欄位,轉為 .NET DateTime 型別時其 Kind 屬性為 Unspecified,而以 DateTime.Now、DateTime.Today 取得的日期物件,Kind 則為 Local,二者不一致可能導致前端出現 8 小時時差。為解決問題,先前想到的做法是先宣告 JsonConvert.DefaultSettings DateTimeZoneHandling = DateTimeZoneHandling.Utc,將 DateTime 統一轉為 "yyyy-MM-ddTHH:mm:ssZ",至於資料庫查詢取得的 DateTime 則使用自製的 FixUnspecifiedDateKind() 方法將 Unspecified 改為 Local。(關於 JSON 時差問題的更多說明,可參考先前文章:Json.NET日期序列化的時區問題EF日期欄位之JSON序列化時區問題

不過,在實際用了一陣子,感覺  FixUnspecifiedDateKind() 並不能算理想做法。EF 可攔截 ObjectMaterialized 每次查詢後自動轉換還算省事,但使用 Dapper 或 IDbCommand 查詢就得每次記得手工補上轉換才不會出錯,讓我萌生改進的念頭。經過一番研究測試,想到兩個更好的解法。

解法一 改用 JsonConvert.DefaultSettings DateTimeZoneHandling = DateTimeZoneHandling.Local

如此,資料庫讀取的 DateTimeKind.Unspecified 日期在 SerializeObject 會自動被視為本地時間,符合我們的期望。而不管 Unspecified、Local、UTC,都一律轉成 yyyy-MM-ddTHH:mm:ss+08:00,前端 DataReviver 統一處理即可。如下範例,dateUnspecified 視同 dateLocal(黃底文字所示),避免資料庫讀取時間被誤為 UTC 提早 8 小時的問題。

解法二,智慧型 DateReviver 函式

在上圖中,我發現一件過去忽略的事:Unspecified、Local、UTC 三種 DateTime 的 Json.NET 轉換結果是有區別的,分別為 "yyyy-MM-ddTHH:mm:ss"'、"yyyy-MM-ddTHH:mm:ss+08:00" 及 "yyyy-MM-ddTHH:mm:ssZ"。利用這項差異,我們可以改寫 DateReviver 函式聰明地將不同 DateTimeKind 時間轉成正確時間,範例如下:

function dateReviver(key, value) {
    var a;
    if (typeof value === 'string') {
        //UTC
        a = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
        if (a) {
            return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6]));
        }
        //Unspecified
        a = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)$/.exec(value);
        if (a) {
            return new Date(+a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6]);
        }
        //with Timezone
        a = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)([+-])(\d{2}):(\d{2})$/.exec(value);
        if (a) {
            var dir = a[7] == "+" ? -1 : 1;
            return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4] + dir * a[8], +a[5] + dir * a[9], +a[6]));
        }
    }
    return value;
}
 
console.log(JSON.parse('"2017-08-08T00:00:00Z"', dateReviver));
console.log(JSON.parse('"2017-08-08T08:00:00"', dateReviver));
console.log(JSON.parse('"2017-08-08T08:00:00+08:00"', dateReviver));

 

經過評估,DateReviver 自動依日期 JSON 字串格式決定時區的做法,只需調整 JavaScript 端程式就好,尤其在 Web API 來自第三方無法配合修改的惡劣環境也能存活,特封為 JSON 時區問題之奧林匹克指定解法。


Comments

Be the first to post a comment

Post a comment