Calvert's murmur

在 Ruby on Rails 使用時區

2018-09-18

約 3421 字 / 需 19 分鐘閱讀

原文:VARVET BLOGWORKING WITH TIME ZONES IN RUBY ON RAILS

Rails 提供了很好的工具來處理時區,但仍有很多事情可能會出錯。這篇文章會點出這些問題並提供解決方案。

讓我被欺騙最多次的應該是,Rails 會愚弄你,讓你誤以為它替你處理好了所有和時間有關的部分。別誤會我的意思,我希望 Rails 盡可能的為我做更多事情,但在學習的過程中由於不夠熟悉 Rails 為我做了些什麼,因此碰到了許多困難。另外需要注意的是,在處理時區上,有更多是你意想不到需要處理的。需要考慮資料庫、伺服器、開發機器、系統設定、使用者設定和瀏覽器。

設定你的 Rails 應用程式

作為 Rails 開發人員,我們可以使用哪些工具?最重要的一個是 config/application.rb 檔案中的 config.time_zone 設定。Active Record 會協助你轉換 UTC 和你選擇的時區(文件沒有解釋)。也就是,如果你所做的只是要透過表單取得用戶文章發佈時間,並使用 Active Record 來保存它,那麼你可以開始了。

處理時間資訊

那麼在保存時間資訊前要做些什麼呢?

解析

重要的是永遠不要在沒有指定時區的情況下解析時間資訊。最好的方法是使用 Time.zone.parse(會使用 config.time_zone 指定的時區)而不是使用 Time.parse(會使用電腦的時區)。

使用數字和 Active Record 屬性

如果可以,請使用如 2.hours.ago 這樣的方法,它會使用你設定的時區!Active Record 模型的時間屬性也是如此。

post = Post.first
post.published_at
# => Fri, 10 Aug 2018 00:00:00 JST +09:00

Active Record 從資料庫取得 UTC 時間,並將其轉換為 config.time_zone 中設定的時區。

Date 與 Time

Time 包含日期和時間資訊,但是 Date 只有日期資訊。即使你認為時間資訊並不重要,但很快地就會意識到它的重要性。安全地使用 Time(或 DateTime,如果你需要支援較遠的時間)。

假設你需要將日期視為時間,至少要確保將它轉換為你設定的時區:

1.day.from_now
# => Sat, 11 Aug 2018 12:11:11 JST +09:00

Date.current.in_time_zone
# => Fri, 10 Aug 2018 00:00:00 JST +09:00

永遠別用:

Date.today.to_time
# => 2018-08-10 00:00:00 +0800

查詢

由於 Rails 知道時間資訊是以 UTC 格式儲存於資料庫中,它會將你給它的任何時間轉換為 UTC。

Post.where(['posts.published_at > ?', Time.current])

永遠不要手動建立查詢字串,應使用 Time.current 取得目前時間,以確保時間資訊是正確的。

使用於 APIs

提供端

建立 Web API 提供其他人使用?請確保始終以 UTC 格式傳送所有時間資料。

Time.current.utc.iso8601
# => "2018-08-10T03:11:35Z"

在此閱讀更多關於為什麼 iso8601 是好的:http://devblog.avdi.org/2009/10/25/iso8601-dates-in-ruby/

用戶端

當你從外部 API 取得時間資訊時,你無法掌控它,只需要確定它傳送給你的格式和時區。因為 Time.zone.parse 可能無法使用在你收到的格式,你可能需要使用:

Time.strptime(time_string, '%Y-%m-%dT%H:%M:%S%z').in_time_zone

上面的範例假設了 time_string 是 iso8601 格式的字串。當時間字串的格式與格式樣板參數不匹配時,strptime 會拋出一個非常不直覺的錯誤。in_time_zone 預設會使用 Rails 設定的時區。

目前可以使用 Time.zone.strptimeTime.zone.parse 來解析時間資訊。

使用用戶時區

許多系統需要支援用戶在各種時區輸入和查看時間資訊。要實現此目的,你需要儲存每個用戶的時區(可能只是一個在 rake time:zones:all 中找到的時區字串名稱)。然後,要實際使用該時區最常見的方式是在 ActionController 中建立一個私有方法,並將其作為 around_action 執行。

around_action :user_time_zone, if: :current_user

def user_time_zone(&block)
  Time.use_zone(current_user.time_zone, &block)
end

這與 config.time_zone 做的事完全相同,但是是基於每個請求。我仍然建議將預設的 config.time_zone 更改為用戶的預設時區。

測試

以上所有內容都是測試應該涵蓋的範圍。問題是你作為開發伺服器的用戶和電腦恰好位於同一時區。在生產環境中這種情況很少發生。

有一個 Zonebie gem 可以幫助你解決這個問題。我還沒有時間試試看,但看起來很有希望。如果你覺得這有點矯枉過正,至少要確保你的測試執行時將 Time.zone 設定為另一時區,而不是開發機所在的時區!

速查表

可以做

2.hours.ago
# => Fri, 10 Aug 2018 10:12:02 JST +09:00

1.day.from_now
# => Sat, 11 Aug 2018 12:12:15 JST +09:00

Time.zone.parse('2018-08-08T12:34:56Z')
# => Wed, 08 Aug 2018 21:34:56 JST +09:00

Time.current
# => Fri, 10 Aug 2018 12:12:40 JST +09:00

Time.current.utc.iso8601
# 當提供 API 時("2018-08-10T03:12:47Z")

Time.strptime('2018-08-08T12:34:56Z', '%Y-%m-%dT%H:%M:%S%z').in_time_zone
# 如果你不能使用 Time.zone.parse(Wed, 08 Aug 2018 21:34:56 JST +09:00)

Date.current
# 如果由於某種原因你真的不能有 Time 或 DateTime(Fri, 10 Aug 2018)

Date.current.in_time_zone
# 如果你有日期並希望充分利用它(Fri, 10 Aug 2018 00:00:00 JST +09:00)

不可以做

Time.now
# 返回系統時間並忽略你設定的時區。(2018-08-10 11:13:30 +0800)

Time.parse('2015-08-27T12:09:36Z')
# 會假設時間字串是在系統的時區。(2015-08-27 12:09:36 UTC)

Time.strptime('2015-08-27T12:09:36Z', '%Y-%m-%dT%H:%M:%S%z')
# 與 Time.parse 相同的問題。(2015-08-27 12:09:36 UTC)

Date.today
# 取決於機器的時區,這可能是昨天或明天,更多相關資訊,請參閱 https://github.com/ramhoj/time-zone-article/issues/1。(Fri, 10 Aug 2018)