考慮時區了嗎?

邊城發表於2021-11-26

前端工程師們,在拿到一個日期/時間資料的時候,你是怎麼處理的呢?有沒有考慮時區的問題呢?

也許你會說:嗯,沒怎麼關注時區,不過我測過,沒問題。

我能怎麼說呢?可能你還沒遇到國外的使用者吧!

被偷走的一天

如果拿到一個日期字串 "2021-11-17",為了方便計算和處理資料,可能會先把它封成一個 Date 物件:

const s = "2021-11-17";
const d = new Date(s);

現在使用 d.getDate() 看會得到什麼?—— 是 17,沒毛病!

其實 —— 怎麼說呢 —— 這是因為你在中國。如果這段程式在瀏覽器執行,而瀏覽器正好位於加拿大,還會是 17 嗎?不妨試試。

在 Windows 下進入時間設定,把時間改為“太平洋時間(美國和加拿大)”或者其他 UTC-XX:00 的時區。再到瀏覽器裡去看看,會發現 d.getDate() 得到了 16

如果是在 Linux 下,也可以重新設定系統時間,然後用 Node 控制檯來檢視,如下圖:

Snipaste_2021-11-17_20-32-01.png

為什麼少了一天?

瀏覽器始終使用本地時間

要問為什麼少一天,不妨看看 d 到底是個什麼樣的資料(記得先把時區改回來哦!)

d.toString();         // Wed Nov 17 2021 08:00:00 GMT+0800 (中國標準時間)
d.toLocaleString();   // 2021/11/17 上午8:00:00

你看,new Date("2021-11-17") 建立日期物件的時候,是把 2021-11-17 當作 UTC 00:00:00 來建立的物件。而這個時間在中國 ,就是 2021-11-17 08:00:00。同理,如果是在 -06:00 的加拿大中部,它會是 2021-11-16 18:00:00,這時候 .getDate(),當然會得到 16

在處理時間這個問題上,瀏覽器處理得簡單粗暴,就是根據系統的時區設定來按照本地時間進行處理。如果我們給的是一個精確的時間,瀏覽器這樣處理沒毛病。畢竟時間是世界的,在這個時間點上,中國是早晨,加拿大是黃昏 —— 問題在於,Date 物件不能只描述日期,還要描述時間,它自動按 UTC 補了時間資料之後,我們就可能得到非預期的結果。

帶時間部分的字串

那麼,如果拿到的表示時間的字串帶時間的呢?是不是會準確一點 …… 試試吧:

// 為了方便檢視,註釋裡的結果資料採用了簡潔的描述
new Date("2021-11-17");           // 2021-11-17 08:00 +08:00
new Date("2021-11-17 00:00:00");  // 2021-11-17 00:00 +08:00
new Date("2021-11-17 09:00:00");  // 2021-11-17 09:00 +08:000

注意第 2 條。在我們的常識中,2021-11-17 00:00:002021-11-17 應該是同一個時間吧?後者不帶時間部分,所以預設它是一天的開始,也就是 00:00:00 —— 問題就在這裡,Date 物件預設它是 UTC 的 0 時,而不是本地時間的 0 時。但如果給了時間,Date 會把它按本地時間來處理 —— 莫名其妙地就出現了時差。

該如何做到無差別

因為 new Date() 的時候是按本地時間來解析的,所以在不同地區的瀏覽器裡,同一個表示日期時間的字串會被解析成不同的時間 —— 你看,中國的早晨 9 點和加拿大的早晨 9 點絕對不是同一個時間對吧!

如果我們希望 new Date() 拿到的是同一個時間怎麼辦?

說起來也挺簡單的,只要拿到字串包含時區資訊就行了,比如符合 RFC3339 標準的時間表示 2021-11-17T09:00:00+08:00。RFC3339 這裡就不詳述了,只需要簡單地理解為

  • 日期部分是 yyyy-MM-dd 格式
  • 時間部分是 HH:mm:ssHH:mm:ss.sss 格式
  • 日期和時間之間使用 T 連線
  • 時區部分是 Z 或者 ±HH:mm 格式,其中 Z 表示無時差,即 UTC 時間。比如下面兩個時間是在同一個時間點上

    2021-11-17T09:00:00+08:00

    2021-11-17T01:00:00Z

很幸運 Date 能識別符合 RFC3339 標準的時間描述,所以用上例兩個字串建立的時間是相同的

const d1 = new Date("2021-11-17T09:00:00+08:00");
const d2 = new Date("2021-11-17T01:00:00Z");
console.log(d1.getTime() === d2.getTime());

還要注意什麼

道理已經講明白了,但是實際操作中還是會出現一些奇奇怪怪的問題。而出現這些問題的原因,歸根結底還是沒在注意到“時區”。比如,我問幾個問題,看能不能搞得明白?

  • 拿到簡單表示日期/時間的字串,不符合 RFC3339 標準,比如前面提到的 "2021-11-17 09:00:00"。請問他是 UTC 時間還是本地時間?
  • 如果是本地時間 —— 它是應用伺服器的本地時間?還是資料庫伺服器的本地時間?還是錄入時間的使用者所在時區的本地時間……?

你看,簡單的一個時區問題,並不只是前端單方面的問題,它可能是整個系統設計的時候就因為忽略了時區概念而造成的問題。

如果在系統設計的時候就把時區問題考慮進去,所有儲存和傳輸都使用相同時區的時間描述,事件就會變得好辦得多。當然,如果直接使用 UTC 時間,或者 Unix Time Stamp 之類精確的描述會更理想。

也許有人覺得,瀏覽器確實只支援本地時間,但是我們可以用 Moment.js 庫,可以用 Day.js 庫,可以用 date-fns 庫 …… 是的,你可以用各種各樣的工具,但是還是得搞明白拿到的時間描述是準確無歧義的。再強調一次,準確的時間描述有:

  • 帶時間資訊的字串,比如符合 RFC3339 標準的字串描述;
  • 基於某個特定時間的秒級、毫秒級或更細微級別的時間偏移值,比如 Unix Time Stamp。

我們的程式生在中國,但要面向世界 —— 你考慮時區了嗎?

相關文章