原文:VARVET BLOG — WORKING 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.strptime
或Time.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)