Trong phần này, chúng ta sẽ tìm hiểu cách xử lý và hiển thị thời gian trong ứng dụng cho người sử dụng ở những địa điểm và múi giờ khác nhau.
Để giúp cho bạn dễ theo dõi, sau đây là danh sách các bài viết trong loạt bài hướng dẫn này:
- Phần 1: Hello, World
- Phần 2: Tìm hiểu về template
- Phần 3: Tìm hiểu về Web Forms
- Phần 4: Sử dụng cơ sở dữ liệu
- Phần 5: Xử lý đăng nhập
- Phần 6: Hồ sơ cá nhân và ảnh đại diện
- Phần 7: Xử lý lỗi
- Phần 8: Tạo chức năng follower
- Phần 9: Phân trang
- Phần 10: Hỗ trợ email
- Phần 11: Nâng cấp giao diện
- Phần 12: Xử lý thời gian (Bài viết này)
- Phần 13: Hỗ trợ đa ngôn ngữ
- Phần 14: Sử dụng Ajax
- Phần 15: Tinh chỉnh cấu trúc ứng dụng
- Phần 16: Hỗ trợ tìm kiếm
- Phần 17: Triển khai ứng dụng trên Linux
- Phần 18: Triển khai ứng dụng với Heroku
- Phần 19: Triển khai ứng dụng với Docker
- Phần 20: JavaScript nâng cao
- Phần 21: Thông báo cho người sử dụng
- Phần 22: Tìm hiểu về tác vụ nền
- Phần 23: Xây dựng API
Bạn có thể truy cập mã nguồn cho phần này tại GitHub.
Cho đến giờ, chúng ta vẫn không can thiệp đến việc hiển thị thời gian và để Flask xử lý theo mặc định. Hơn thế nữa, chúng ta chỉ để Python hiển thị đối tượng datetime trong mô hình dữ liệu User mà không để ý đến mô hình Post.
Các rắc rối với múi giờ
Sử dụng Python để xử lý và hiển thị thời gian từ server chưa bao giờ là một ý tưởng hay. Chúng ta có thể minh họa điều này bằng ví dụ sau đây: Giả sử chúng ta đăng một bài viết vào lúc 3:00 trưa vào ngày 8/10/2019. Vì múi giờ tác giả đang ở tại một địa điểm theo múi giờ MDT (hoặc tính theo giờ UTC là UTC -6). Khi lấy giờ hiện tại bằng Python, chúng ta sẽ thấy như sau:
1 2 3 4 5 |
>>> from datetime import datetime >>> str(datetime.now()) '2019-10-08 15:00:05.327183' >>> str(datetime.utcnow()) '2019-10-08 21:00:15.024319' |
Lệnh datetime.now()
trả về thời điểm hiện tại nơi tác giả đang ở, còn lệnh datetime.utcnow()
trả về thời gian theo chuẩn UTC (Coordinated Universal Time). Nếu có nhiều người tại các địa điểm khác nhau trên thế giờ đồng thời thực hiện lệnh datetime.now()
, mỗi người sẽ nhận được một kết quả khác nhau. Nhưng với lệnh datetime.utcnow()
thì khác, mỗi người sẽ nhận được cùng một kết quả bất kể vị trí vật lý. Như vậy, bạn nghĩ lệnh nào sẽ tốt hơn cho các ứng dụng với các user trên toàn thế giới?
Rõ ràng rằng server phải sử dụng thời gian đảm bảo tính nhất quán và độc lập với vị trí của người sử dụng. Nếu ứng dụng của chúng ta phát triển và được cài đặt đồng thời trên nhiều server trên thế giới ở các múi giờ khác nhau, việc mỗi server lưu trữ thời gian theo giờ địa phương sẽ làm cho việc quản lý và hiển thị thời gian là “Nhiệm vụ bất khả thi”. Đó cũng là lý do việc lưu trữ và hiển thị thời gian theo chuẩn UTC trở nên phổ biến. Và đó cũng là điều chúng ta sẽ làm.
Tuy nhiên, có một vấn đề khá quan trọng khi sử dụng thời gian theo UTC: User ở các múi giờ khác nhau sẽ khó theo dõi thời gian nếu nó được hiển thị theo UTC. Bạn hãy tưởng tượng nếu mình ở trong một khu vực với múi giờ ICT (Indochine Time của Việt nam hay UTC+7) và đăng một bài viết vào lúc 3:00 chiều, nhưng trên ứng dụng bạn sẽ thấy bài viết được đăng lúc 8:00 sáng theo giờ UTC. Đối với phần lớn user, điều này thật rắc rối và khó hiểu.
Vì vậy, dù rằng việc chuẩn hóa thời gian theo UTC là rất hợp lý cho server, nó cũng tạo ra một vấn đề cho user. Đó cũng là điều mà chúng ta sẽ tập trung giải quyết trong phần này.
Chuyển đổi thời gian giữa các múi giờ
Giải pháp rõ ràng cho vấn đề này là đổi tất cả dữ liệu về thời gian được lưu trữ trong cơ sở dữ liệu thành thời gian địa phương của từng user trước khi hiển thị cho họ. Điều này đảm bảo tính nhất quán của dữ liệu thời gian được lưu trong Cơ sở dữ liệu đồng thời giải quyết vấn đề về hiển thị thời gian mà chúng ta vừa đề cập ở trên. Tuy vậy, điều này khó ở chỗ phải làm sao để xác định vị trí địa lý của mỗi user để chuyển đổi cho phù hợp.
Nhiều Web site đưa ra giải pháp bằng cách tạo ra một trang cấu hình để người sử dụng xác lập thông tin về múi giờ của họ. Để tạo ra một trang như vậy, chúng ta cần tạo thêm một template mới với một Web form trong đó có một danh sách các múi giờ khác nhau. User sẽ phải chọn múi giờ cho khu vực mà họ ở khi thực hiện thủ tục đăng ký với với Web site.
Mặc dù đây là một phương án có thể thực hiện được, nó không phải là một phương án hay. Thật lạ là chúng ta phải buộc người sử dụng nhập vào một thông tin đã có sẵn trong hệ điều hành của họ. Sẽ tốt hơn nhiều nếu chúng ta có thể lấy thông tin về múi giờ của user từ dữ liệu có sẵn trong hệ điều hành của họ. Thật ra thì trình duyệt có các thông tin về múi giờ của người sử dụng và chúng ta có thể truy cập các thông tin này từ các API tiêu chuẩn của JavaScript. Có hai cách để lấy thông tin về múi giờ của người sử dụng thông qua JavaScript:
- Cách truyền thống là làm cho trình duyệt gởi thông tin về múi giờ đến server khi user đăng nhập vào ứng dụng lần đầu. Điều này có thể được thực hiện với Ajax hay đơn giản hơn là dùng thẻ HTML refresh. Sau khi nhận được, server có thể lưu các thông tin này kèm theo các thông tin về phiên làm việc của user (trong session variable) hoặc trong cơ sở dữ liệu và dùng nó để tính ra thời gian địa phương trước khi truyền nó vào template và gởi về trình duyệt của user để hiển thị.
- Cách “hiện đại” là hoàn toàn không can thiệp đến dữ liệu tại server và sẽ thực hiện việc chuyển đổi từ UTC sang giờ địa phương trên trình duyệt của user nhờ vào JavaScript.
Cả hai cách đều hợp lệ, nhưng cách thức hai có ưu điểm hơn. Thông tin về múi giờ của user không phải lúc nào cũng đủ để hiển thị thời gian theo định dạng mà người sử dụng muốn. Trình duyệt có thể truy cập các thông tin này từ cấu hình hệ thống (ví dụ như AM/PM, định dạng 24 giờ, DD/MM/YYYY hoặc MM/DD/YYY và nhiều tham số khác tùy thuộc vào các văn hóa khác nhau).
Và hơn thế nữa, cách thứ hai còn có một lợi thế lớn nữa là có sẵn một thư viện mã mở để làm tất cả những điều này cho chúng ta.
Giới thiệu Moment.js và Flask-Moment
Moment.js là một thư viện JavaScript mã nguồn mở để hiển thị thời gian theo nhiều cách khác nhau. Flask có hỗ trợ thư viện này qua thư viện mở rộng Flask-Moment, nhờ đó giúp cho việc tích hợp thư viện này vào ứng dụng Flask dễ dàng hơn rất nhiều. Để bắt đầu, chúng ta hãy cài đặt Flask-Moment:
1 |
(myenv) $ pip3 install flask-moment |
Thư viện mở rộng này được tích hợp vào ứng dụng Flask khác với cách thông thường:
app/__init__.py: Thực thể Flask-Moment.
1 2 3 4 5 6 |
... from flask_moment import Moment app = Flask(__name__) ... moment = Moment(app) |
Không như các thư viện mở rộng khác, Flask-Moments làm việc với moment.js. Vì vậy, tất cả các template trong ứng dụng phải tham chiếu đến thư viện này. Để bảo đảm rằng thư viện này luôn luôn hiện diện, chúng ta sẽ đưa tham chiếu đến nó vào trong template base.html. Điều này có thể được thực hiện bằng một trong hai cách. Cách trực tiếp nhất là thêm một thẻ <script>
để tham chiếu đến thư viện trong mã của template. Tuy nhiên, chúng ta sẽ theo cách thứ hai là dùng hàm moment.include_moment()
của thư viện Flask-Moment để tạo ra thẻ <script>
một cách gián tiếp:
app/templates/base.html: Tham chiếu đến moment.js trong template base
1 2 3 4 5 6 |
... {% block scripts %} {{ super() }} {{ moment.include_moment() }} {% endblock %} |
Khối scripts
mà chúng ta sử dụng trong template trên được định nghĩa trong template base của Flask-Bootstrap (không phải template base.html trong ứng dụng của chúng ta). Đây là nơi mà chúng ta sẽ đặt các tham chiếu đến các thư viện JavaScript. Khối này khác với các khối khác là template base định nghĩa nó kèm theo một số nội dung khác. Vì chúng ta chỉ muốn thêm thư viện moment.js mà không làm ảnh hưởng đến các nội dung sẵn có, chúng ta phải sử dụng mệnh đề super()
để giữ các nội dung có sẵn (tương tự như khi sử dụng override trong lập trình đối tượng). Nếu bạn định nghĩa một khối trong template mà không sử dụng super()
, các nội dung có sẵn trong template base sẽ không được giữ lại.
Sử dụng Moment.js
Moment.js cho phép chúng ta truy cập và sử dụng lớp moment
trong trình duyệt. Bước đầu tiên để hiển thị thời gian là tạo một đối tượng của lớp này và truyền thông tin về thời gian mà chúng ta muốn hiển thị cho nó theo định dạng ISO 8601 như ví dụ sau:
1 |
t = moment('2019-10-08T15:00:05Z') |
Nếu bạn không quen thuộc với chuẩn định dạng ISO 8601, nó được định dạng như sau: {{ năm }}-{{ tháng }}-{{ ngày }}T{{ giờ }}:{{ phút }}:{{ giây }}{{ múi giờ }}
. Bởi vì chúng ta sẽ sử dụng thời gian theo UTC, thông tin về múi giờ sẽ có giá trị là Z
, đại diện cho giờ UTC theo chuẩn ISO 8601.
Đối tượng moment có một số cách hiển thị khác nhau. Sau đây là một vài cách hiển thị phổ biến:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
moment('2019-10-08T15:00:05Z').format('L') "10/08/2019" moment('2019-10-08T15:00:05Z').format('LL') "October 8, 2019" moment('2019-10-08T15:00:05Z').format('LLL') "October 8, 2019 9:00 AM" moment('2019-10-08T15:00:05Z').format('LLLL') "Tuesday, October 8, 2019 9:00 AM" moment('2019-10-08T15:00:05Z').format('dddd') "Tuesday" moment('2019-10-08T15:00:05Z').fromNow() "6 hours ago" moment('2019-10-08T15:00:05Z').calendar() "Today at 9:00 AM" |
Ví dụ trên khởi tạo một đối tượng moment
với thời gian là ngày 08 tháng Mười năm 2019 lúc 11:35 sáng giờ UTC. Bạn có thể thấy toàn bộ các lựa chọn khác nhau trong ví dụ đều hiển thị giờ theo thời gian UTC-7 – đây cũng là múi giờ được thiết lập trên máy tính của tác giả. Bạn có thể thử bằng cách tạo một trang Web có tham chiếu đến moment.js, mở trang Web đó trong trình duyệt và nhập các lệnh JavaScript trên vào cửa sổ lệnh (console) của trình duyệt. Bạn cũng có thể thử các lệnh trên với ứng dụng của chúng ta sau khi đã thực hiện các chỉ dẫn cần thiết để tham chiếu đến moment.js như đã nói ở trên.
Khi sử dụng các chọn lựa (option) khá nhau trong lệnh format, bạn sẽ có các hiển thị thời gian khác nhau. Điều bạn cần làm là cung cấp các chuỗi định dạng thích hợp cho lệnh này, tương tự như khi dùng hàm strftime của Python. Bên cạnh đó, chúng ta cũng thấy một số điểm đáng lưu ý với các hàm fromNow()
và calendar()
. Các hàm này hiển thị thời gian tương đối so với thời điểm hiện tại, do đó, bạn sẽ thấy các giá trị như “a minute ago” hay là “in two hours”, …
Nếu bạn sử dụng trực tiếp JavaScript, việc gọi các hàm trên sẽ trả về một chuỗi với giá trị thời gian sẽ được hiển thị. Tuy vậy, bạn phải quyết định đặt các chuỗi này ở vị trí nào trong trang Web bằng cách sử dụng JavaScript lần nữa để thao tác DOM (Document Object Model). Thư viện Flask-Moment sẽ giúp chúng ta đơn giản hóa công việc này rất nhiều bằng cách tạo ra một đối tượng cũng được gọi là moment
và có chức năng tương tự như đối tượng JavaScript cùng tên để chúng ta có thể sử dụng trong các template.
Trở lại với thời gian đang được hiển thị trong trang hồ sơ cá nhân. Cho đến thời điểm hiện tại, template user.html đang sử dụng chuỗi thời gian mặc định do Python tạo ra. Nhưng đã đến lúc chúng ta có thể thay thế chuỗi mặc định này bằng chuỗi mới từ Flask-Moment như sau:
app/templates/user.html: Hiển thị thời gian trong template với moment.js
1 2 3 |
{% if user.last_seen %} <p>Last seen on: {{ moment(user.last_seen).format('LLL') }}</p> {% endif %} |
Như bạn thấy, Flask-Moment có cú pháp tương tự như thư viện JavaScript ngoại trừ việc tham số được truyền vào đối tượng moment()
sẽ là một đối tượng datetime
của Python thay vì một chuỗi theo chuẩn ISO 8601. Khi hàm moment()
được gọi từ template, nó sẽ tự động tạo ra các mã JavaScript để chèn các chuỗi hiển thị thời gian vào vị trí thích hợp trong DOM.
Nơi tiếp theo chúng ta có thể sử dụng lợi thế của Flask-Moment và moment.js là trong template con _post.html – được sử dụng bởi trang chủ và trang hồ sơ cá nhân. Hiện giờ trong template này, mỗi bài viết đều bắt đầu với dòng “username: “. Bây giờ, chúng ta có thể thay thế dòng này với chuỗi hiển thị thời gian tạo ra bởi hàm fromNow()
:
app/templates/_post.html: Hiển thị thời gian trong template con _post.html
1 2 3 4 5 6 |
<a href="{{ url_for('user', username=post.author.username) }}"> {{ post.author.username }} </a> said {{ moment(post.timestamp).fromNow() }}: <br> {{ post.body }} |
Bạn có thể thấy kết quả với các chuỗi hiển thị thời gian sử dụng Flask-Moment và moment.js dưới đây:
Chúng ta sẽ kết thúc phần này ở đây. Hẹn gặp bạn trong phần tiếp theo.