十二、flask博客项目实战-之处理日期和时间

oakcdrom0条评论 524 次浏览

概述

本章将学习如何以适合所有用户的方式使用日期和时间,无论他们身居何处。

目前为止,一直忽略Microblog应用程序显示日期和时间的问题,只是让Python渲染了User模型中的datetime对象,并完全忽略Post模型中的datetime对象。

时区“地狱”

在服务器上用Python去呈现日期和时间,在Web浏览器上以这种方式渲染给用户可不是一个好主意。如示例,在Python解释器中运行如下内容:

(venv) [root@python blog]# python3
Python 3.9.0 (default, Oct 16 2020, 10:57:11) 
[GCC 4.8.5 20150623 (Red Hat 4.8.5-39)] on linux
Type "help", "copyright", "credits" or "license" for more information.

>>> from datetime import datetime
>>> str(datetime.now())
'2018-08-23 09:52:14.895893'
>>> str(datetime.utcnow())
'2018-08-23 01:52:28.247986'
>>> quit()

(venv) [root@python blog]#

调用datetime.now()将返回我当前所在地区(中国-北京(时间))的正确的时间,而调用datetime.utcnow()将返回UTC时区的时间。如果我能让生活在世界不同地区的许多人同时一起运行上述代码,datetime.now()函数将为每个人返回不同的结果,但datetime.utcnow()无论地理位置在哪,都将返回相同的时间。那么你认为哪一个更适合一个很可能让用户遍布全球的Web应用程序中使用?

很明显,服务器必须管理 一致且独立于位置的时间。如果这个应用程序增长到世界各地需要多个生产服务器的程序,当然就不希望每个服务器在不同时区写入数据库的时间戳,因为这样就无法使用这些时间。由于UTC是最常用的统一时区,并且在datetime类中受支持,因此在此将使用它。

但这种方法存在一个重要问题。对于不同时区的用户,如果他们在UTC看到时间,那么将很难弄清楚发布帖子的时间。他们需要提前时间是UTC,以便他们能够“精神上”调整时间到他们自己的时区。想象一下,PDT时区的用户在下午3点发布了一些内容,并立即看到这个帖子在UTC时间晚上10点出现,或更准确的说是22:00。这将是很令人困惑的。

虽然从服务器的角度上,将时间戳标准化很有意义,但这会用户带来可用性问题。本章的目标是解决这个问题,同时保持服务器以UTC格式管理的所有时间戳。

时区转换

这个问题显而易见的解决方案是:将所有时间戳 从存储的UTC单位转换为每个用户的本地时间。这允许服务器继续使用UTC来保持一致性,同时为每个用户量身定制地即时转换解决可用性问题。这个解决方案的棘手部分 是了解每个用户的位置。

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

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

事实证明,Web浏览器知道用户的时区,并通过标准日期和时间JavaScript API公开它。实际上,通过JavaScript,有两种方法可利用时区信息:

“老派”方法是在用户首次登陆应用程序时,让Web浏览器以某种方式将时区信息发送到服务器。这可以通过Ajax调用完成,或更简单地使用 meta refresh tag。一旦服务器知道时区,它就可以将其保存在用户的会话中,或将其写入数据库中的用户条目,然后在渲染模板时用它调整所有时间戳。
“新派”方法是不改变服务器中的东西,而在客户端中使用JavaScript在客户端中进行从UTC到本地时区的转换。
这两个选项都是有效的,但第二个选项有很大的优势。光是知道用户的时区,并不足以以用户期望的格式呈现日期和时间。浏览器还可访问系统区域配置,这个配置指定AP/PM与24小时制,DD/MM/YYYY与MM/DD/YYYY,以及许多其他文化或区域样式之类的内容。

如果上述还不够,那么“新派”方法还有一个优势。有一个开源库可完成所有这些工作。

介绍Moment.js和Flask-Moment

Moment.js是一个小型开源JavaScript库,它将日期和时间渲染成另一个级别,因为它提供每一个可想象的格式化选项。不久前,建立了Flask-Moment,它是一个小型Flask扩展,它可以轻易地将moment.js合并到应用程序中。

首先,安装Flask-Moment:版本0.6.0

(venv) [root@python blog]#pip3 install flask-moment
Collecting flask-moment
  Using cached https://files.pythonhosted.org/packages/dd/f7/13e9d7480f9097e0efe945e17309c34e0a547a6cfb3f9728324d2f9bf462/Flask_Moment-0.6.0-py2.py3-none-any.whl
Requirement already satisfied: Flask in d:\microblog\venv\lib\site-packages (from flask-moment)
Requirement already satisfied: Jinja2>=2.10 in d:\microblog\venv\lib\site-packages (from Flask->flask-moment)
Requirement already satisfied: itsdangerous>=0.24 in d:\microblog\venv\lib\site-packages (from Flask->flask-moment)
Requirement already satisfied: Werkzeug>=0.14 in d:\microblog\venv\lib\site-packages (from Flask->flask-moment)
Requirement already satisfied: click>=5.1 in d:\microblog\venv\lib\site-packages (from Flask->flask-moment)
Requirement already satisfied: MarkupSafe>=0.23 in d:\microblog\venv\lib\site-packages (from Jinja2>=2.10->Flask->flask-moment)
Installing collected packages: flask-moment
Successfully installed flask-moment-0.6.0

以常规方式添加到Flask应用程序中:

app/__init__.py:添加Flask-Moment实例

# ...
from flask_moment import Moment

app = Flask(__name__)
# ...
moment = Moment(app)

与其他扩展不同,Flask-Moment与moment.js一起使用,因此应用程序的所有模板都必须包含这个库。为了确保这个库始终可用,将在 基础模板中添加它。这可通过两种方式完成。最直接的方法是显示地以导入库的方式添加一个<script>标签,但Flask-Moment使其变得更容易,即通过公开一个moment.include_moment()函数,它会生成<script>标签。
app/templates/base.html:在基础模板中包含moment.js

...
{% block scripts %}
    {{ super() }}
    {{ moment.include_moment() }}
{% endblock %}

在这添加的scripts块是Flask-Bootstrap的基础模板导出的另一个块。这是包含JavaScript导入的地方。这个块与之前的块不同,因为它已经在基础模板中定义了一些内容。我想的是 添加moment.js库,而不会失去基本内容。这是通过super()语句实现的,这个语句将保留基础模板中的内容。如果在没有使用super()的情况下,在你的模板中定义一个块,那么在基础模板中,为这个块定义的任何内容都将失去。

使用Moment.js

Moment.js使得一个moment类可供浏览器使用。渲染时间戳的第一步是创建这个类的对象,以ISO 8601格式传递所需的时间戳。下方是例子:

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

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

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

moment('2017-09-28T21:45:23Z').format('L')
"09/28/2017"
moment('2017-09-28T21:45:23Z').format('LL')
"September 28, 2017"
moment('2017-09-28T21:45:23Z').format('LLL')
"September 28, 2017 2:45 PM"
moment('2017-09-28T21:45:23Z').format('LLLL')
"Thursday, September 28, 2017 2:45 PM"
moment('2017-09-28T21:45:23Z').format('dddd')
"Thursday"
moment('2017-09-28T21:45:23Z').fromNow()
"7 hours ago"
moment('2017-09-28T21:45:23Z').calendar()
"Today at 2:45 PM"

上述示例 创建了一个时刻对象,初始化为 2017年9月28日晚上9:45 UTC。上面尝试的所有选项都以UTC-7时区(在中国的话,将用UTC+8)来呈现,因为这是作者的计算机上配置的时区。可在浏览器的控制台中输入上述命令,得确保打开控制台的页面包含moment.js。如果引入了moment.js,也可以在Microblog上操作。或者在https://momentjs.com/上操作。

注意不同方法是如何创建不同的表示的。使用format(),可以控制字符串的输出格式,类似Python中的strftime()函数。fromNow()和calendar()方法很有趣,因为它们会根据当前时间显示时间戳,所以会得到如“一分钟前”或“两个小时内”的输出。

如果直接使用JavaScript,那么上述调用将返回具有呈现时间戳的字符串。然后,可将此文本插入页面上的适当位置,遗憾的是,需要JavaScript与DOM配合使用。Flask-Moment在模板中启用类似于JavaScript的对象,极大地简化moment.js的使用。

我们来看一下 个人资料页面 中显示的时间戳。当前 user.html模板允许使用Python生成时间的字符串表示。现在使用Flask-Moment渲染这个时间戳,如下:
app/templates/user.html:使用moment.js渲染时间戳

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

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

可以利用Flask-Moment和moment.js的第二个地方是_post.html子模板,它是从/index和/user页面中调用的。在当前版本的模板中,每个帖子前面都有一个“username says:”行。现在添加一个fromNow()时间戳:
app/templates/_post.html:

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

flask run运行程序,效果:

(venv) [root@python blog]# flask run
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off
[2018-08-23 15:10:27,042] INFO in __init__: Microblog startup
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
127.0.0.1 - - [23/Aug/2018 15:10:36] "GET /user/oldiron HTTP/1.1" 200 -
127.0.0.1 - - [23/Aug/2018 15:11:04] "GET /edit_profile HTTP/1.1" 200 -
127.0.0.1 - - [23/Aug/2018 15:11:05] "GET /index HTTP/1.1" 200 -
127.0.0.1 - - [23/Aug/2018 15:11:08] "GET /user/oldiron HTTP/1.1" 200 -
127.0.0.1 - - [23/Aug/2018 15:11:21] "GET /explore HTTP/1.1" 200 -

file

目前为止,项目结构

    microblog/
    app/
        templates/
            email/
                reset_password.html
                reset_password.txt
            _post.html
            404.html
            500.html
            base.html
            edit_profile.html
            index.html
            login.html
            register.html
            reset_password.html
            reset_password_request.html
            user.html
        __init__.py
        email.py
        errors.py
        forms.py
        models.py
        routes.py
    logs/
        microblog.log
    migrations/
    venv/
    app.db
    config.py
    microblog.py
    tests.py

发表评论

? razz sad evil ! smile oops grin eek shock ??? cool lol mad twisted roll wink idea arrow neutral cry mrgreen