在本章,我们将讨论怎么用适配不同时区的所有用户的方式来处理日期和时间,无论他们身处地球上的何处。
显示日期和时间是 Microblog 应用中长期被忽略的其中一个方面。直到现在,我也只是让 Python 渲染了 User
模型中的 datetime
对象,并且完全忽略了 Post
模型中的 datetime
对象。
时区地域
我们先来 Python shell 中看看这个例子:
>>> 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()
调用返回我所处位置的本地时间,而 datetime.utcnow()
调用则返回 UTC 时区(协调世界时)中的时间。假设世界不同地区的多人同时运行上面的代码,datetime.now()
函数将为每个人返回不同的结果,但是无论身在哪个时区,datetime.utcnow()
总是会返回同一时间。那么你认为哪一个更适合用在一个用户遍布世界各地的 Web 应用中呢?
很明显,服务器必须管理一致且独立于位置的时间。我们肯定不希望每个用户都在写入不同时区的时间戳到数据库,因为这会导致其无法正常地运行。 UTC 是最常用的统一时区,并且在 datetime
类中受到支持,因此我将会使用它。
现在可以得出结论,对处于不同时区的用户,我们需要知道它的 UTC时间和所在的时区,再通过时区转换的计算,从而正确地为用户显示当地格式的时间。
时区转换
该问题的直接解决方案是将所有时间戳从存储的 UTC 单位转换为每个用户的本地时间。这样一来,服务器可以继续使用 UTC 来保持时区的一致性,而针对每个用户量身定制的即时转换来解决可用性问题。这个解决方案棘手的部分是要知道每个用户的位置。
其中一个解决方法是要求用户在个人资料处选择他们的时区。这将需要为此添加一个新的功能。也可以要求用户在第一次访问网站时,作为注册的一部分,会被要求选择他们的时区。
虽然该方案可以解决问题,但要求用户选择他们已经在其操作系统中配置的信息有点奇怪。 如果我能从他们的计算机操作系统中获取时区信息,似乎效率会更高。
其实通过 Web 浏览器就可以获取用户的时区,并通过标准的日期和时间 JavaScript API 暴露它。 有两种方法来利用 JavaScript 提供的时区信息:
“老派”方法是当用户第一次登录到应用程序时,Web 浏览器以某种方式将时区信息发送到服务器。这可以通过 Ajax 调用完成,或者更简单地使 meta refresh。一旦服务器知道了时区,就可以将其保存在用户的会话中,或者将其写入用户在数据库中的条目中,然后在渲染模板时从中调整所有时间戳。
“新派”的做法是不改变服务器中的东西,而在浏览器端使用 JavaScript 来对 UTC 和本地时区之间进行转换。
两种选择都是有效的,但第二种选择有很大优势。 光是知道用户的时区并不足以以用户期望的格式呈现日期和时间。 浏览器还可以访问系统区域配置,该配置指定 AM/PM 与 24 小时制,DD/MM/YYYY
与 MM/DD/YYYY
,以及许多其他文化或地区风格之类的东西。
如果这还不够,新派方法还有另一个优势,用一个开源的库来完成所有这些工作!
Moment.js 和 Flask-Moment 简介
Moment.js 是一个小型的 JavaScript 开源库,它将日期和时间转换成目前可以想象到的所有格式。而 Flask-Moment
,一个小型 Flask 插件,它可以使你在应用中轻松使用 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
一起工作,因此应用的所有模板都必须包含 moment.js
。为了确保该库始终可用,我将把它添加到基础模板 base.html
中,可以通过两种方式完成。 最直接的方法是显式添加一个 标签来引入库。
还记得我们上一章讲 Flask-Bootstrap
时候讲到可以使用 {% block content %}
块来插入 Javascript 脚本吗。结合 Flask-Moment
的 moment.include_moment()
函数,我们就可以很容易地实现它,它会直接生成了一个 标签并在其中包含
moment.js
:
# app\templates\base.html
{% block scripts %}
{{ super() }}
{{ moment.include_moment() }}
{% endblock %}
我在这里添加的 scripts 块是 Flask-Bootstrap
基础模板暴露的另一个块,这是 JavaScript 引入的地方。该块与之前的块不同的地方在于它已经在基础模板中定义了一些内容了。我想要追加 moment.js
库的话,就需要使用 super()
语句,才能继承基础模板中已有的内容,否则就是替换。
使用 Moment.js
Moment.js
为浏览器提供了一个 moment
类。呈现时间戳的第一步是创建此类的对象,并以 ISO 8601 格式传递所需的时间戳。
运行我们的应用后,我们在浏览器打开它,并打开浏览器的控制器,在控制器中,我们演示一下如何使用 Moment.js
。
let t = moment('2021-08-02T21:45:23Z')
如果你对日期和时间不熟悉 ISO 8601 标准格式,格式如下: {{ year }}-{{ month }}-{{ day }}T{{ hour }}:{{ minute }}:{{ second }}{{ timezone }}
。我因为我们只使用 UTC 时区,因此最后一部分总是将会是 Z,它表示 ISO 8601 标准中的 UTC。
moment
对象为不同的渲染选项提供了几种方法。 以下是一些最常见的几种:
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"
此示例创建了一个 moment
对象,该对象被初始化为 2017 年 9 月 28 日晚上 9:45 UTC。你可以看到,我上面尝试的所有选项都以 UTC+8 时区来呈现,因为这是我计算机上配置的时区。你可以在 microblog 上进行此操作,只要你引入了 moment.js
。或者你也可以在 https://momentjs.com/ 上尝试。
请注意不同的方法是如何创建的不同的表示。使用 format()
,你可以控制字符串的输出格式,类似于 Python 中的 strftime
函数。fromNow()
和calendar()
方法很有趣,因为它们会根据当前时间显示时间戳,因此你可以获得诸如“一分钟前”或“两小时内”等输出。
如果你直接在 JavaScript 中运行,则上述调用将返回渲染后的时间戳字符串。 然后,你可以将此文本插入页面上的适当位置,这需要 JavaScript 与 DOM 配合使用。
Flask-Moment
插件通过启用一个类似于 JavaScript 上的 moment
对象,大大简化了对 moment.js
的使用,并融合了所需的 JavaScript 逻辑,使渲染后的时间展示在页面上。
我们来看看出现在个人主页中的时间戳。 当前的 user.html
模板使用 Python 生成时间的字符串表示。 现在我可以使用 Flask-Moment
渲染此时间戳:
# app\templates\auth\user.html
{% if user.last_seen %}
Last seen on: {{ moment(user.last_seen).format('LLL') }}
{% endif %}
如你所见,Flask-Moment
使用的语法类似于 JavaScript 库的语法,其中一个区别是,moment()
的参数现在是 Python 的 datetime
对象,而不是ISO 8601 字符串。 从模板发出的 moment()
调用也会自动生成所需的 JavaScript 代码,以将呈现的时间戳插入DOM的适当位置。
使用 Flask-Moment
和 moment.js
的第二个地方是被主页和个人主页调用的 _post.html
子模板。 现在我可以添加一个用 fromNow()
渲染的时间戳:
{{ post.author.username }}
says: {{ moment(post.timestamp).fromNow() }}
{{ post.body }}
下面,你可以看到这两个时间戳在 Flask-Moment
和 moment.js
的渲染下:
现在我们的应用已经具有根据用户系统的时区自动转换成本地时间的功能了,不信?可以修改系统时区再刷新我们的应用试试。
本文源码:https://github.com/SingleDiego/Flask-Tutorial-Source-Code/tree/SingleDiego-patch-12