在本章绎谦,我們將討論怎么用適配不同時(shí)區(qū)的所有用戶的方式來處理日期和時(shí)間,無論他們身處地球上的何處粥脚。
顯示日期和時(shí)間是 Microblog 應(yīng)用中長期被忽略的其中一個(gè)方面窃肠。直到現(xiàn)在,我也只是讓 Python 渲染了 User
模型中的 datetime
對(duì)象刷允,并且完全忽略了 Post
模型中的 datetime
對(duì)象冤留。
時(shí)區(qū)地域
我們先來 Python shell 中看看這個(gè)例子:
>>> from datetime import datetime
>>> str(datetime.now())
'2021-08-02 17:09:16.366550'
>>> str(datetime.utcnow())
'2021-08-02 09:09:23.533968'
datetime.now()
調(diào)用返回我所處位置的本地時(shí)間,而 datetime.utcnow()
調(diào)用則返回 UTC 時(shí)區(qū)(協(xié)調(diào)世界時(shí))中的時(shí)間树灶。假設(shè)世界不同地區(qū)的多人同時(shí)運(yùn)行上面的代碼纤怒,datetime.now()
函數(shù)將為每個(gè)人返回不同的結(jié)果,但是無論身在哪個(gè)時(shí)區(qū)天通,datetime.utcnow()
總是會(huì)返回同一時(shí)間泊窘。那么你認(rèn)為哪一個(gè)更適合用在一個(gè)用戶遍布世界各地的 Web 應(yīng)用中呢?
很明顯,服務(wù)器必須管理一致且獨(dú)立于位置的時(shí)間烘豹。我們肯定不希望每個(gè)用戶都在寫入不同時(shí)區(qū)的時(shí)間戳到數(shù)據(jù)庫瓜贾,因?yàn)檫@會(huì)導(dǎo)致其無法正常地運(yùn)行。 UTC 是最常用的統(tǒng)一時(shí)區(qū)携悯,并且在 datetime
類中受到支持祭芦,因此我將會(huì)使用它。
現(xiàn)在可以得出結(jié)論憔鬼,對(duì)處于不同時(shí)區(qū)的用戶龟劲,我們需要知道它的 UTC時(shí)間和所在的時(shí)區(qū),再通過時(shí)區(qū)轉(zhuǎn)換的計(jì)算轴或,從而正確地為用戶顯示當(dāng)?shù)馗袷降臅r(shí)間昌跌。
時(shí)區(qū)轉(zhuǎn)換
該問題的直接解決方案是將所有時(shí)間戳從存儲(chǔ)的 UTC 單位轉(zhuǎn)換為每個(gè)用戶的本地時(shí)間。這樣一來照雁,服務(wù)器可以繼續(xù)使用 UTC 來保持時(shí)區(qū)的一致性蚕愤,而針對(duì)每個(gè)用戶量身定制的即時(shí)轉(zhuǎn)換來解決可用性問題。這個(gè)解決方案棘手的部分是要知道每個(gè)用戶的位置囊榜。
其中一個(gè)解決方法是要求用戶在個(gè)人資料處選擇他們的時(shí)區(qū)审胸。這將需要為此添加一個(gè)新的功能亥宿。也可以要求用戶在第一次訪問網(wǎng)站時(shí)卸勺,作為注冊(cè)的一部分,會(huì)被要求選擇他們的時(shí)區(qū)烫扼。
雖然該方案可以解決問題曙求,但要求用戶選擇他們已經(jīng)在其操作系統(tǒng)中配置的信息有點(diǎn)奇怪。 如果我能從他們的計(jì)算機(jī)操作系統(tǒng)中獲取時(shí)區(qū)信息映企,似乎效率會(huì)更高悟狱。
其實(shí)通過 Web 瀏覽器就可以獲取用戶的時(shí)區(qū),并通過標(biāo)準(zhǔn)的日期和時(shí)間 JavaScript API 暴露它堰氓。 有兩種方法來利用 JavaScript 提供的時(shí)區(qū)信息:
“老派”方法是當(dāng)用戶第一次登錄到應(yīng)用程序時(shí)挤渐,Web 瀏覽器以某種方式將時(shí)區(qū)信息發(fā)送到服務(wù)器。這可以通過 Ajax 調(diào)用完成双絮,或者更簡單地使 meta refresh浴麻。一旦服務(wù)器知道了時(shí)區(qū),就可以將其保存在用戶的會(huì)話中囤攀,或者將其寫入用戶在數(shù)據(jù)庫中的條目中软免,然后在渲染模板時(shí)從中調(diào)整所有時(shí)間戳。
“新派”的做法是不改變服務(wù)器中的東西焚挠,而在瀏覽器端使用 JavaScript 來對(duì) UTC 和本地時(shí)區(qū)之間進(jìn)行轉(zhuǎn)換膏萧。
兩種選擇都是有效的,但第二種選擇有很大優(yōu)勢(shì)。 光是知道用戶的時(shí)區(qū)并不足以以用戶期望的格式呈現(xiàn)日期和時(shí)間榛泛。 瀏覽器還可以訪問系統(tǒng)區(qū)域配置蝌蹂,該配置指定 AM/PM 與 24 小時(shí)制,DD/MM/YYYY
與 MM/DD/YYYY
挟鸠,以及許多其他文化或地區(qū)風(fēng)格之類的東西叉信。
如果這還不夠,新派方法還有另一個(gè)優(yōu)勢(shì)艘希,用一個(gè)開源的庫來完成所有這些工作硼身!
Moment.js 和 Flask-Moment 簡介
Moment.js 是一個(gè)小型的 JavaScript 開源庫,它將日期和時(shí)間轉(zhuǎn)換成目前可以想象到的所有格式覆享。而 Flask-Moment
佳遂,一個(gè)小型 Flask 插件,它可以使你在應(yīng)用中輕松使用 moment.js
撒顿。
我們還是從安裝 Flask-Moment
來開始:
(venv) $ pip install flask-moment
一如既往丑罪,在 app\__init__.py
中初始化插件:
# app\__init__.py
from flask_moment import Moment
moment = Moment()
def create_app(config):
app = Flask(__name__)
moment.init_app(app)
# ...
return app
與其他插件不同的是,Flask-Moment
需要與 moment.js
一起工作凤壁,因此應(yīng)用的所有模板都必須包含 moment.js
吩屹。為了確保該庫始終可用,我將把它添加到基礎(chǔ)模板 base.html
中拧抖,可以通過兩種方式完成煤搜。 最直接的方法是顯式添加一個(gè) <script>
標(biāo)簽來引入庫。
還記得我們上一章講 Flask-Bootstrap
時(shí)候講到可以使用 {% block content %}
塊來插入 Javascript 腳本嗎唧席。結(jié)合 Flask-Moment
的 moment.include_moment()
函數(shù)擦盾,我們就可以很容易地實(shí)現(xiàn)它,它會(huì)直接生成了一個(gè) <script>
標(biāo)簽并在其中包含 moment.js
:
# app\templates\base.html
{% block scripts %}
{{ super() }}
{{ moment.include_moment() }}
{% endblock %}
我在這里添加的 scripts 塊是 Flask-Bootstrap
基礎(chǔ)模板暴露的另一個(gè)塊淌哟,這是 JavaScript 引入的地方迹卢。該塊與之前的塊不同的地方在于它已經(jīng)在基礎(chǔ)模板中定義了一些內(nèi)容了。我想要追加 moment.js
庫的話徒仓,就需要使用 super()
語句腐碱,才能繼承基礎(chǔ)模板中已有的內(nèi)容,否則就是替換掉弛。
使用 Moment.js
Moment.js
為瀏覽器提供了一個(gè) moment
類症见。呈現(xiàn)時(shí)間戳的第一步是創(chuàng)建此類的對(duì)象,并以 ISO 8601 格式傳遞所需的時(shí)間戳狰晚。
運(yùn)行我們的應(yīng)用后筒饰,我們?cè)跒g覽器打開它,并打開瀏覽器的控制器壁晒,在控制器中瓷们,我們演示一下如何使用 Moment.js
。
let t = moment('2021-08-02T21:45:23Z')
如果你對(duì)日期和時(shí)間不熟悉 ISO 8601 標(biāo)準(zhǔn)格式,格式如下: {{ year }}-{{ month }}-{{ day }}T{{ hour }}:{{ minute }}:{{ second }}{{ timezone }}
谬晕。我因?yàn)槲覀冎皇褂?UTC 時(shí)區(qū)碘裕,因此最后一部分總是將會(huì)是 Z,它表示 ISO 8601 標(biāo)準(zhǔn)中的 UTC攒钳。
moment
對(duì)象為不同的渲染選項(xiàng)提供了幾種方法帮孔。 以下是一些最常見的幾種:
moment('2021-08-02T21:45:23Z').format('L')
"08/03/2021"
moment('2021-08-02T21:45:23Z').format('LL')
"August 3, 2021"
moment('2021-08-02T21:45:23Z').format('LLL')
"August 3, 2021 5:45 AM"
moment('2021-08-02T21:45:23Z').format('LLLL')
"Tuesday, August 3, 2021 5:45 AM"
moment('2021-08-02T21:45:23Z').format('dddd')
"Tuesday"
moment('2021-08-02T21:45:23Z').fromNow()
"in 6 hours"
moment('2021-08-02T21:45:23Z').calendar()
"Tomorrow at 5:45 AM"
此示例創(chuàng)建了一個(gè) moment
對(duì)象,該對(duì)象被初始化為 2017 年 9 月 28 日晚上 9:45 UTC不撑。你可以看到文兢,我上面嘗試的所有選項(xiàng)都以 UTC+8 時(shí)區(qū)來呈現(xiàn),因?yàn)檫@是我計(jì)算機(jī)上配置的時(shí)區(qū)焕檬。你可以在 microblog 上進(jìn)行此操作姆坚,只要你引入了 moment.js
∈涤蓿或者你也可以在 https://momentjs.com/ 上嘗試兼呵。
請(qǐng)注意不同的方法是如何創(chuàng)建的不同的表示。使用 format()
腊敲,你可以控制字符串的輸出格式击喂,類似于 Python 中的 strftime
函數(shù)。fromNow()
和calendar()
方法很有趣碰辅,因?yàn)樗鼈儠?huì)根據(jù)當(dāng)前時(shí)間顯示時(shí)間戳懂昂,因此你可以獲得諸如“一分鐘前”或“兩小時(shí)內(nèi)”等輸出。
如果你直接在 JavaScript 中運(yùn)行乎赴,則上述調(diào)用將返回渲染后的時(shí)間戳字符串忍法。 然后潮尝,你可以將此文本插入頁面上的適當(dāng)位置榕吼,這需要 JavaScript 與 DOM 配合使用。
Flask-Moment
插件通過啟用一個(gè)類似于 JavaScript 上的 moment
對(duì)象勉失,大大簡化了對(duì) moment.js
的使用羹蚣,并融合了所需的 JavaScript 邏輯,使渲染后的時(shí)間展示在頁面上乱凿。
我們來看看出現(xiàn)在個(gè)人主頁中的時(shí)間戳顽素。 當(dāng)前的 user.html
模板使用 Python 生成時(shí)間的字符串表示。 現(xiàn)在我可以使用 Flask-Moment
渲染此時(shí)間戳:
# app\templates\auth\user.html
{% if user.last_seen %}
<p>Last seen on: {{ moment(user.last_seen).format('LLL') }}</p>
{% endif %}
如你所見徒蟆,Flask-Moment
使用的語法類似于 JavaScript 庫的語法胁出,其中一個(gè)區(qū)別是,moment()
的參數(shù)現(xiàn)在是 Python 的 datetime
對(duì)象段审,而不是ISO 8601 字符串全蝶。 從模板發(fā)出的 moment()
調(diào)用也會(huì)自動(dòng)生成所需的 JavaScript 代碼,以將呈現(xiàn)的時(shí)間戳插入DOM的適當(dāng)位置。
使用 Flask-Moment
和 moment.js
的第二個(gè)地方是被主頁和個(gè)人主頁調(diào)用的 _post.html
子模板抑淫。 現(xiàn)在我可以添加一個(gè)用 fromNow()
渲染的時(shí)間戳:
<td>
<a href="{{ url_for('auth.user', username=post.author.username) }}">
{{ post.author.username }}
</a>
says: {{ moment(post.timestamp).fromNow() }}
<br>
{{ post.body }}
</td>
下面绷落,你可以看到這兩個(gè)時(shí)間戳在 Flask-Moment
和 moment.js
的渲染下:
現(xiàn)在我們的應(yīng)用已經(jīng)具有根據(jù)用戶系統(tǒng)的時(shí)區(qū)自動(dòng)轉(zhuǎn)換成本地時(shí)間的功能了,不信始苇?可以修改系統(tǒng)時(shí)區(qū)再刷新我們的應(yīng)用試試砌烁。
本文源碼:https://github.com/SingleDiego/Flask-Tutorial-Source-Code/tree/SingleDiego-patch-12