本文翻译自The Flask Mega-Tutorial Part XII: Dates and Times

这是Flask Mega-Tutorial系列的第十二部分,我将告诉你如何以适配所有用户的方式处理日期和时间,无论他们身处地球上的何处。

显示日期和时间是Microblog应用中长期被忽略的其中一个方面。 直到现在,我也只是让Python渲染了User模型中的datetime对象,并且完全忽略了Post模型中的datetime对象。

本章的GitHub链接为:Browse, Zip, Diff.

时区地狱

使用服务器端的Python渲染日期和时间来展示到用户的浏览器并非一个好主意。考虑如下的例子, 我在2017年9月28日下午4点06分写这篇文章。我身处的时区是PDT(UTC-7),在Python解释器中运行如下:

  1. >>> from datetime import datetime
  2. >>> str(datetime.now())
  3. '2017-09-28 16:06:30.439388'
  4. >>> str(datetime.utcnow())
  5. '2017-09-28 23:06:51.406499'

datetime.now()调用返回我所处位置的本地时间,而datetime.utcnow()调用则返回UTC时区中的时间。 如果我可以让遍布世界不同地区的多人同时运行上面的代码,那么datetime.now()函数将为他们每个人返回不同的结果,但是无论位置如何,datetime.utcnow()总是会返回同一时间。 那么你认为哪一个更适合用在一个很可能其用户遍布世界各地的Web应用中呢?

很明显,服务器必须管理一致且独立于位置的时间。 如果这个应用增长到在全世界不同地区都需要部署生产服务器的时候,我不希望每个服务器都在写入不同时区的时间戳到数据库,因为这会导致其无法正常地运行。 由于UTC是最常用的统一时区,并且在datetime类中也受到支持,因此我将会使用它。

但这种方法存在一个严重问题。 对处于不同时区的用户,如果他们看到的是UTC时区中的时间,那么很难确定是何时发布的信息。 他们需要事先知道展示的时间是UTC时区的,才能在精神上调整自己的时区。 设想一下PDT时区中的一个用户在下午3点发布了一些内容,并立即看到该帖子以UTC时间表示的晚上10:00或更准确的22:00,这太混乱了。

从服务器的角度来说,将时间戳标准化为UTC,意义重大,但这会为用户带来可用性问题。 本章的目标就是解决该问题,同时保持服务器中以UTC格式管理的所有时间戳。
While standardizing the timestamps to UTC makes a lot of sense from the server’s perspective, this creates a usability problem for users. The goal of this chapter is to address this problem while keeping all the timestamps managed in the server in UTC.

时区转换

该问题的直接解决方案是将所有时间戳从存储的UTC单位转换为每个用户的本地时间。 这样一来,服务器可以继续使用UTC来保持时区的一致性,而针对每个用户量身定制的即时转换来解决可用性问题。 这个解决方案棘手的部分是要知道每个用户的位置。

许多网站都有一个配置页面供用户指定他们的时区。 这将需要我添加一个新的页面,其中我向用户显示带有时区列表的下拉列表。 也可能用户在第一次访问网站时,作为注册的一部分,会被要求输入他们的时区。

虽然该方案可以解决问题,但要求用户输入他们已经在其操作系统中配置的信息有点奇怪。 如果我能从他们的计算机中获取时区设置,似乎效率会更高。

事实证明,Web浏览器可以获取用户的时区,并通过标准的日期和时间JavaScript API暴露它。 实际上有两种方法来利用JavaScript提供的时区信息:

  • “老派”方法是当用户第一次登录到应用程序时,Web浏览器以某种方式将时区信息发送到服务器。 这可以通过Ajax)调用完成,或者更简单地使用meta refresh tag。 一旦服务器知道了时区,就可以将其保存在用户的会话中,或者将其写入用户在数据库中的条目中,然后在渲染模板时从中调整所有时间戳。
  • “新派”的做法是不改变服务器中的东西,而在客户端中使用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来开始吧:

  1. (venv) $ pip install flask-moment

使用常规方法添加该插件到Flask应用中:

app/__init__.py:Flask-Moment实例。

  1. # ...
  2. from flask_moment import Moment
  3. app = Flask(__name__)
  4. # ...
  5. moment = Moment(app)

与其他插件不同的是,Flask-Moment与moment.js一起工作,因此应用的所有模板都必须包含moment.js。为了确保该库始终可用,我将把它添加到基础模板中,可以通过两种方式完成。 最直接的方法是显式添加一个<script>标签来引入库,但Flask-Moment的moment.include_moment()函数可以更容易地实现它,它直接生成了一个<script>标签并在其中包含moment.js:

app/templates/base.html:在基础模板中包含moment.js

  1. ...
  2. {% block scripts %}
  3. {{ super() }}
  4. {{ moment.include_moment() }}
  5. {% endblock %}

我在这里添加的scripts块是Flask-Bootstrap基础模板暴露的另一个块,这是JavaScript引入的地方。该块与之前的块不同的地方在于它已经在基础模板中定义了一些内容了。我想要追加moment.js库的话,就需要使用super()语句,才能继承基础模板中已有的内容,否则就是替换。

使用Moment.js

Moment.js为浏览器提供了一个moment类。 呈现时间戳的第一步是创建此类的对象,并以ISO 8601格式传递所需的时间戳。 这里是一个例子:

  1. t = moment('2017-09-28T21:45:23Z')

如果你对日期和时间不熟悉ISO 8601标准格式,格式如下:{{ year }}-{{ month }}-{{ day }}T{{ hour }}:{{ minute }}:{{ second }}{{ timezone }}。 我已经决定我只使用UTC时区,因此最后一部分总是将会是Z,它表示ISO 8601标准中的UTC。

moment对象为不同的渲染选项提供了几种方法。 以下是一些最常见的几种:

  1. moment('2017-09-28T21:45:23Z').format('L')
  2. "09/28/2017"
  3. moment('2017-09-28T21:45:23Z').format('LL')
  4. "September 28, 2017"
  5. moment('2017-09-28T21:45:23Z').format('LLL')
  6. "September 28, 2017 2:45 PM"
  7. moment('2017-09-28T21:45:23Z').format('LLLL')
  8. "Thursday, September 28, 2017 2:45 PM"
  9. moment('2017-09-28T21:45:23Z').format('dddd')
  10. "Thursday"
  11. moment('2017-09-28T21:45:23Z').fromNow()
  12. "7 hours ago"
  13. moment('2017-09-28T21:45:23Z').calendar()
  14. "Today at 2:45 PM"

此示例创建了一个moment对象,该对象被初始化为2017年9月28日晚上9:45 UTC。 你可以看到,我上面尝试的所有选项都以UTC-7时区来呈现,因为这是我计算机上配置的时区。你可以在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/user.html: 使用moment.js渲染时间戳。

  1. {% if user.last_seen %}
  2. <p>Last seen on: {{ moment(user.last_seen).format('LLL') }}</p>
  3. {% endif %}

如你所见,Flask-Moment使用的语法类似于JavaScript库的语法,其中一个区别是,moment()的参数现在是Python的datetime对象,而不是ISO 8601字符串。 从模板发出的moment()调用也会自动生成所需的JavaScript代码,以将呈现的时间戳插入DOM的适当位置。

我可以利用Flask-Moment和moment.js的第二个地方是被主页和个人主页调用的_post.html子模板。 在该模板的当前版本中,每条用户动态都以“用户名说:”行开头。 现在我可以添加一个用fromNow()渲染的时间戳:

app/templates/_post.html: 在用户动态子模板中渲染时间戳。

  1. <a href="{{ url_for('user', username=post.author.username) }}">
  2. {{ post.author.username }}
  3. </a>
  4. said {{ moment(post.timestamp).fromNow() }}:
  5. <br>
  6. {{ post.body }}

下面,你可以看到这两个时间戳在Flask-Moment和moment.js的渲染下,表现如何:

Flask-Moment