JSON日期轉換的時區陷阱

在使用Kendo UI DatePicker時,出現選好日期送至後端卻變成前一天的狀況。

以下程式可重現問題,kendoDatePicker所選日期透過.value()可得到一個JavaScript Date物件,JSON.stringify()後傳至Server端,使用Json.NET還原回DateTime後,以ToString("yyyy-MM-dd HH:mm:ss")方式傳回Client端alert顯示。

<%@ Page Language="C#" %>
<%@ Import Namespace="Newtonsoft.Json" %>
 
<!DOCTYPE html>
 
<script runat="server">
    protected void Page_Load(object sender, EventArgs e)
    {
        if (Request["m"] == "post")
        {
            var p = Request["d"];
            var nd = JsonConvert.DeserializeObject<DateTime>(p);
            Response.Write(string.Format("{0}->{1:yyyy-MM-dd HH:mm:ss}", p, nd));
            Response.End();
        }
    }
</script>
 
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
    <title></title>
    <link href="../Content/kendo/2013.1.319/kendo.common.min.css" rel="stylesheet" />
    <link href="../Content/kendo/2013.1.319/kendo.bootstrap.min.css" rel="stylesheet" />
</head>
<body>
    <form id="form1" runat="server">
        <input data-bind="kendoDatePicker: { value: TheDate, format: 'yyyy-MM-dd' }" />
        <br />
        <span data-bind="text: TheDate"></span>
        <input type="button" data-bind="click: post" value="POST" />
    </form>
    <script src="../Scripts/jquery-1.9.1.min.js"></script>
    <script src="../Scripts/kendo/2013.1.319/kendo.web.min.js"></script>
    <script src="../Scripts/knockout-2.2.1.js"></script>
    <script src="../Scripts/knockout-kendo.min.js"></script>
    <script>
        function myViewModel() {
            var self = this;
            self.TheDate = ko.observable("2012-12-21");
            self.post = function () {
                $.post("",
                    { m: "post", d: JSON.stringify(self.TheDate()) },
                    function (r) {
                        alert("Result = " + r);
                    });
            };
        }
        var vm = new myViewModel();
        ko.applyBindings(vm);
    </script>
</body>
</html>

測試結果如下:

明明選了12/22日,但傳到.NET端ToString後卻是12/21日! 問題出在12/22的本地時間在JSON.stringify時被轉成UTC,12/22凌晨0點減去8小時,於是.NET端得到 DateTimeKind = UTC 的DateTime -- 12/21 16:00 UTC。

依據Telerik RD的說法,kendo.stringify跟JSON.stringify一樣,會將本地時間轉換成UTC時間,而kendoDatePicker .value()傳回的是JavaScript Date物件時區則會以本地時間為準,JSON轉成UTC後,若.NET處理時沒轉回本地時間或UTC時間,就會出問題。

知道原委,我理解到這個問題與Kendo UI無關,而是JSON具有全球化觀點,.NET端沒跟上造成的。在一個全球化網站,傳送時間需反應使用者所在時區,Server端才能精準掌握真正時點,但前提是.NET端應將來自各地的時間一律轉為UTC時間或本地時間才合理,直接ToString()看到的是當地時間,忽略時區差異便會衍生問題。

因此,我們可以重塑一個與Kendo UI無關的精簡範例:

<%@ Page Language="C#" %>
<%@ Import Namespace="Newtonsoft.Json" %>
 
<!DOCTYPE html>
 
<script runat="server">
    protected void Page_Load(object sender, EventArgs e)
    {
        if (Request["m"] == "post")
        {
            var p = Request["d"];
            var nd = JsonConvert.DeserializeObject<DateTime>(p);
            Response.Write(string.Format("{0}->{1:yyyy-MM-dd HH:mm:ss}", p, nd));
            Response.End();
        }
    }
</script>
 
<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
        <span id="sTime"></span>
    </form>
    <script src="../Scripts/jquery-1.9.1.min.js"></script>
    <script>
        var d = new Date();
        $("#sTime").text(d.toString());
        $.post("",
            { m: "post", d: JSON.stringify(d) },
            function (r) {
                alert("Result = " + r);
            });
    </script>
</body>
</html>

在早上8:00以前,將new Date()經JSON轉換後送到.NET,還原回DateTime再ToString(),看到的日期會是前一天!!

面對這個問題有兩個解決方向:

  1. 在Server端落實全球化概念,所有來自Client端的JSON時間,一律轉為UTC保存,顯示呈現時再視需求決定時區。
  2. 如果只是本土小公司使用的內網系統,所有Client端座落在方圓100公尺內,只因為用了JSON就要在系統推行全球化有點小題大作。而且,Server未必能配合修改,此時就要考慮由Client端解決。

要從Client端解決,我想到的做法是讓JSON.stringify()忽略時區差異,轉成"2013-06-22T07:18:48"(最後不加Z或+0800,對應成.NET DateTime相當於Kind = Unspecified)。實作技巧是偷偷將Date.prototype.toISOString()改成我們自訂的版本:

<script>
        //將原本的函數保留起來,必要時可以換回去
        var _toIsoDate = Date.prototype.toISOString;
        //借用kendo.toString做出Unspecified Kind的ISO8601格式
        Date.prototype.toISOString = function () {
            return kendo.toString(this, "yyyy-MM-ddTHH:mm:ss");
        };
 
        function myViewModel() {
            var self = this;
            self.TheDate = ko.observable("2012-12-21");
//...以下略...

重新評估後,改寫.toJSON()只會針對JSON轉換調整邏輯,較置換.toISOString()更符合目的,感謝Kuo-Chun Su提醒。

    <script>
        //借用kendo.toString做出Unspecified Kind的ISO8601格式
        Date.prototype.toJSON = function () {
            return kendo.toString(this, "yyyy-MM-ddTHH:mm:ss");
        };
 
        function myViewModel() {
            var self = this;

如此,應該就能避開惱人的JSON日期時差問題囉~

歡迎推文分享:
Published 25 June 2013 06:24 AM 由 Jeffrey
Filed under: ,
Views: 21,407



意見

# TonyQ said on 25 June, 2013 01:03 AM

這比較算是 bug 吧,JS Date 的表示法中,最後後綴為 Z ,本來就是表示 UTC 時間。

ex

var obj = new Date("2013/6/25Z");

console.log(obj);

//Tue Jun 25 2013 08:00:00 GMT+0800 (台北標準時間)

JSON.stringify() 本來就是忠實 follow 這個準則而已。

ex.

JSON.stringify(new Date())

"2013-06-25T05:02:54.579Z"

看不懂 Z 又要當 JSON parser 的話,顯然是不夠格的。

# Jeffrey said on 25 June, 2013 01:56 AM

to TonyQ, 用Json.NET解析JsonConvert.DeserializeObject<DateTime>("\"2013/6/25Z\"").ToUniversalTime()與JsonConvert.DeserializeObject<DateTime>("\"2013/6/25\"").ToUniversalTime()可以得到UTC 6/25 00:00及6/24 16:00,算是已正確盡到JSON Parser的職責。我想有問題的部分還是得歸在.NET端程式沒考量到DateTime資料屬UTC或Local分清楚,冒冒失失地直接用ToString()做轉換,少了全球化的觀點跟不上JSON規範的腳步。(感覺像是ANSI vs Unicode的演進)

你的看法呢?

(必要的) 
(必要的) 
(選擇性的)
(必要的) 
(提醒: 因快取機制,您的留言幾分鐘後才會顯示在網站,請耐心稍候)

5 + 3 =

搜尋

Go

<June 2013>
SunMonTueWedThuFriSat
2627282930311
2345678
9101112131415
16171819202122
23242526272829
30123456
 
RSS
創用 CC 授權條款
【廣告】
twMVC

Tags 分類檢視
關於作者

一個醉心技術又酷愛分享的Coding魔人,十年的IT職場生涯,寫過系統、管過專案, 也帶過團隊,最後還是無怨無悔地選擇了技術鑽研這條路,近年來則以做一個"有為的中年人"自許。

文章典藏
其他功能

這個部落格


Syndication