Trong phần này, chúng ta sẽ xây dựng chức năng gởi thông điệp cá nhân cũng như hiển thị các thông báo trên thanh định hướng mà không cần phải tải lại (refresh) nội dung trang Web từ máy chủ.
Để 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
- 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 (Bài viết này)
- 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.
Chúng ta sẽ tiếp tục cải tiến trải nghiệm người sử dụng với ứng dụng Myblog bằng hệ thống thông báo (notification). Các ứng dụng xã hội dùng hệ thống thông báo để hiển thị các thông báo cho người dùng và cho họ biết rằng họ được nhắc đến hoặc nhận được các tin nhắn cá nhân từ những người sử dụng khác. Các thông báo này thường được trình bày dưới dạng một ô nhỏ với một con số trên thanh định hướng. Ngoài ra, hệ thống thôn báo còn được dùng trong những trường hợp khác để báo với người sử dụng những việc họ cần làm.
Để minh họa cho tác dụng của hệ thống thông báo cũng như các kỹ thuật có liên quan để xây dựng nó, chúng ta sẽ tạo ra một chức năng có sử dụng hệ thống này. Trong phần tiếp theo, chúng ta sẽ xây dựng một hệ thống nhắn tin cho phép người sử dụng gởi tin nhắn cho nhau. Thật ra thì việc tạo ra chức năng nhắn tin không hề khó, và nó cũng giúp chúng ta ôn lại các khái niệm về Flask mà chúng ta đã tìm hiểu từ đầu của loạt bài này. Sau khi đã có hệ thống nhắn tin, chúng ta sẽ tham khảo một vài tùy chọn và xây dựng chức năng hiển thị số tin nhắn cho ứng dụng.
Tin nhắn cá nhân
Chức năng nhắn tin cá nhân mà chúng ta sắp xây dựng sẽ rất đơn giản. Khi bạn đến thăm một trang hồ sơ cá nhân của một user nào đó, một liên kết để gởi một tin nhắn cá nhân cho user đó sẽ được hiển thị. Liên kết này sẽ dẫn bạn đến một trang web khác với form để gởi tin nhắn. Nếu bạn nhận được tin nhắn từ các user khác, thanh định hướng ở phía trên cùng của ứng dụng sẽ hiển thị liên kết “Message”. Liên kết này sẽ cho phép bạn truy nhập vào một trang Web có cấu trúc tương tự như trang chủ hay trang Explore, nhưng nó sẽ hiển thị các tin nhắn thay vì các bài viết.
Phần tiếp theo sẽ trình bày các bước cần thiết để tạo chức năng này.
Hỗ trợ cơ sở dữ liệu cho các tin nhắn cá nhân
Công việc đầu tiên là mở rộng cơ sở dữ liệu để hỗ trợ cho các dữ liệu về tin nhắn. Sau đây là mô hình dữ liệu Message
cho các tin nhắn:
app/models.py: Mô hình dữ liệu cho tin nhắn
1 2 3 4 5 6 7 8 9 |
class Message(db.Model): id = db.Column(db.Integer, primary_key=True) sender_id = db.Column(db.Integer, db.ForeignKey('user.id')) recipient_id = db.Column(db.Integer, db.ForeignKey('user.id')) body = db.Column(db.String(140)) timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) def __repr__(self): return '<Message {}>'.format(self.body) |
Mô hình dữ liệu này tương tự như mô hình dữ liệu Post
cho các bài viết. Điểm khác biệt duy nhất là hai khóa ngoại đến đối tượng user, một cho người gởi và một cho người nhận. Hai khóa ngoại này sẽ được liên kết với mô hình dữ liệu User
. Và cuối cùng, chúng ta có trường timestamp
để lưu thời điểm người nhận đọc tin nhắn lần cuối cùng:
app/models.py: Hỗ trợ tin nhắn cá nhân trong mô hình dữ liệu User
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class User(UserMixin, db.Model): ... messages_sent = db.relationship('Message', foreign_keys='Message.sender_id', backref='author', lazy='dynamic') messages_received = db.relationship('Message', foreign_keys='Message.recipient_id', backref='recipient', lazy='dynamic') last_message_read_time = db.Column(db.DateTime) ... def new_messages(self): last_read_time = self.last_message_read_time or datetime(1900, 1, 1) return Message.query.filter_by(recipient=self).filter( Message.timestamp > last_read_time).count() |
Hai quan hệ (relationship) được định nghĩa trong đoạn mã trên sẽ trả về các tin nhắn được gởi và nhận bởi một user, đồng thời thêm các tham chiếu hồi quy (back reference) là author
và recipient
đến mô hình dữ liệu Message
. Lý do chúng ta sử dụng tên author
thay vì sender
trong tham chiếu hồi quy là để thuận tiện cho việc hiển thị các tin nhắn vì chúng ta có thể sử dụng lại các logic tương tự như với các bài viết. Tiếp theo, trường last_message_read_time
sẽ chịu trách nhiệm lưu giữ thời điểm user đã truy cập trang tin nhắn lần cuối như đã nói ở trên, đồng thời cũng sẽ được dùng để phát hiện các tin nhắn mới dựa trên điều kiện là thời điểm lưu trữ các tin nhắn này sẽ “mới” hơn thời điểm trang được truy cập lần cuối. Hàm trợ giúp new_message()
sẽ dùng thông tin này để đếm và trả về số tin nhắn mới (chưa được đọc). Đến cuối phần này, ứng dụng của chúng ta sẽ hiển thị con số này trong một khung nhỏ trên thanh định hướng.
Đến đây, chúng ta đã hoàn thành các thay đổi trong cơ sở dữ liệu. Đã đến lúc chúng ta cần thực hiện việc chuyển đổi cơ sở dữ liệu theo các cập nhật này:
1 2 |
(myenv) $ flask db migrate -m "Tin nhắn cá nhân" (myenv) $ flask db upgrade |
Gởi tin nhắn cá nhân
Tiếp theo, chúng ta cần một form đơn giản để user có thể nhập và gởi tin nhắn:
app/main/forms.py: Lớp tạo form nhắn tin.
1 2 3 4 |
class MessageForm(FlaskForm): message = TextAreaField(_l('Message'), validators=[ DataRequired(), Length(min=0, max=140)]) submit = SubmitField(_l('Submit')) |
Chúng ta cũng cần một template HTML tương ứng với form trên để hiển thị nó:
app/templates/send_message.html: Template HTML để gởi tin nhắn.
1 2 3 4 5 6 7 8 9 10 11 |
{% extends "base.html" %} {% import 'bootstrap/wtf.html' as wtf %} {% block app_content %} <h1>{{ _('Send Message to %(recipient)s', recipient=recipient) }}</h1> <div class="row"> <div class="col-md-4"> {{ wtf.quick_form(form) }} </div> </div> {% endblock %} |
Chúng ta cũng cần tạo ra một định tuyến mới theo định dạng /send_message/
app/main/routes.py: Hàm xử lý và địa chỉ (Định tuyến) cho các tin nhắn cá nhân.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
from app.main.forms import MessageForm from app.models import Message ... @bp.route('/send_message/<recipient>', methods=['GET', 'POST']) @login_required def send_message(recipient): user = User.query.filter_by(username=recipient).first_or_404() form = MessageForm() if form.validate_on_submit(): msg = Message(author=current_user, recipient=user, body=form.message.data) db.session.add(msg) db.session.commit() flash(_('Your message has been sent.')) return redirect(url_for('main.user', username=recipient)) return render_template('send_message.html', title=_('Send Message'), form=form, recipient=recipient) |
Đoạn mã trên tương đối rõ ràng, việc gởi tin nhắn cá nhân thực chất là lưu một thực thể Message
vào cơ sở dữ liệu.
Công việc cuối cùng để kết nối các công đoạn trên đây là thêm một liên kết đến địa chỉ trên vào trang hồ sơ cá nhân:
app/templates/user.html: Liên kết đến trang gởi tin nhắn cá nhân từ trang hồ sơ cá nhân.
1 2 3 4 5 6 7 8 |
{% if user != current_user %} <p> <a href="{{ url_for('main.send_message', recipient=user.username) }}"> {{ _('Send private message') }} </a> </p> {% endif %} |
Đọc tin nhắn cá nhân
Phần quan trọng thứ hai trong chức năng nhắn tin là làm thế nào để user có thể đọc các tin nhắn đã nhận được. Để làm điều này, chúng ta sẽ thêm một địa chỉ mới là /messages và được xử lý tương tự như trang chủ hoặc trang Explore, bao gồm cả chức năng phân trang:
app/main/routes.py: Hàm hiển thị tin nhắn cá nhân.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
@bp.route('/messages') @login_required def messages(): current_user.last_message_read_time = datetime.utcnow() db.session.commit() page = request.args.get('page', 1, type=int) messages = current_user.messages_received.order_by( Message.timestamp.desc()).paginate( page, current_app.config['POSTS_PER_PAGE'], False) next_url = url_for('main.messages', page=messages.next_num) \ if messages.has_next else None prev_url = url_for('main.messages', page=messages.prev_num) \ if messages.has_prev else None return render_template('messages.html', messages=messages.items, next_url=next_url, prev_url=prev_url) |
Trong hàm này, đầu tiên chúng ta sẽ cập nhật trường User.last_message_read_time
với giá trị của thời điểm hiện tại. Điều này cũng đồng nghĩa với việc đánh dấu toàn bộ các tin nhắn đã nhận được như là đã được đọc. Sau đó, chúng ta sẽ truy vấn mô hình dữ liệu Message
để nhận được danh sách các tin nhắn cho user và được sắp xếp theo thứ tự từ mới đến cũ. Chúng ta sẽ sử dụng lại tham số cấu hình POSTS_PER_PAGE
cho việc phân trang vì trang tin nhắn và trang bài viết có cấu trúc tương tự như nhau. Logic cho mã phân trang đã được trình bày trước đây trong Phần 9. Vì vậy, chúng ta sẽ không nhắc lại ở đây.
Hàm hiển thị này sẽ hiển thị template /app/templates/messages.html dưới đây:
app/templates/messages.html: Template HTML để đọc tin nhắn.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
{% extends "base.html" %} {% block app_content %} <h1>{{ _('Messages') }}</h1> {% for post in messages %} {% include '_post.html' %} {% endfor %} <nav aria-label="..."> <ul class="pager"> <li class="previous{% if not prev_url %} disabled{% endif %}"> <a href="{{ prev_url or '#' }}"> <span aria-hidden="true">←</span> {{ _('Newer messages') }} </a> </li> <li class="next{% if not next_url %} disabled{% endif %}"> <a href="{{ next_url or '#' }}"> {{ _('Older messages') }} <span aria-hidden="true">→</span> </a> </li> </ul> </nav> {% endblock %} |
Ở đây, chúng ta sử dụng một mẹo nhỏ: bởi vì các thực thể Post
và Message
có cấu trúc tương tự nhau với một ngoại lệ duy nhất là Message
có thêm một quan hệ recipient
(chúng ta cũng không cần sử dụng đến giá trị này trong trang hiển thị tin nhắn vì nó luôn là user hiện tại), chúng ta sẽ sử dụng lại template con app/templates/_post.html để hiển thị các tin nhắn cá nhân. Đây là lý do chúng ta sử dụng biến lặp post
trong vòng lặp for post in messages
trong template trên (thay vì message) để các tham chiếu đến post trong template con _post.html cũng hoạt động được với các tin nhắn.
Để user có thể truy cập trang tin nhắn, chúng ta sẽ tạo liên kết mới “Messages” trong thanh định hướng:
app/templates/base.html: Liên kết đến trang tin nhắn trong thanh định hướng.
1 2 3 4 5 6 7 8 9 10 |
{% if current_user.is_anonymous %} ... {% else %} <li> <a href="{{ url_for('main.messages') }}"> {{ _('Messages') }} </a> </li> ... {% endif %} |
Đến đây, chúng ta đã hoàn tất chức năng nhắn tin. Tuy nhiên, trong quá trình xây dựng chức năng này, chúng ta đã thêm một số văn bản mới vào ứng dụng, vì vậy, để tiếp tục hỗ trợ đa ngôn ngữ, chúng ta cần cập nhật phần dịch thuật cho các văn bản này. Bước đầu tiên là cập nhật các file chỉ mục ngôn ngữ:
1 |
(myenv) $ flask translate update |
Sau đó, chúng ta sẽ cần cung cấp văn bản được dịch với các ngôn ngữ được hỗ trợ trong các file messages.po tương ứng nằm trong thư mục app/translation. Bạn có thể sử dụng file dịch thuật cho tiếng Việt tại GitHub cho phần này.
Ô hiển thị thông báo tĩnh
HIện giờ, người sử dụng đã có thể sử dụng hệ thống nhắn tin mới để gởi tin nhắn lẫn nhau. Tuy nhiên, ứng dụng lại chưa thể thông báo với user khi họ nhận được tin nhắn. Để user có thể biết được khi nào họ có tin nhắn mới, chúng ta sẽ tạo một ô thông báo nhỏ trên thanh định hướng để hiển thị số tin nhắn mà user nhận được nhưng chưa đọc. Chúng ta sẽ sử dụng mẫu huy hiệu (badge) từ Bootstrap để tạo ô thông báo và đặt nó vào trong template gốc (base.html) của ứng dụng:
app/templates/base.html: Ô hiển thị số tin nhắn trong thanh định hướng.
1 2 3 4 5 6 7 8 9 10 11 |
... <li> <a href="{{ url_for('main.messages') }}"> {{ _('Messages') }} {% set new_messages = current_user.new_messages() %} {% if new_messages %} <span class="badge">{{ new_messages }}</span> {% endif %} </a> </li> ... |
Ở đây chúng ta gọi trực tiếp phương thức new_message(
) vừa được thêm vào mô hình dữ liệu User
ở phần trên trong template và lưu giá trị trả về từ phương thức này vào biến new_massages
trong template. Nếu biến này lớn hơn 0, chúng ta sẽ thêm một ô thông báo hiển thị số lượng tin nhắn bên cạnh liên kết Messages như hình dưới đây:
Ô hiển thị thông báo động
Giải pháp chúng ta vừa thực hiện ở trên đơn giản và vừa đủ để hiển thị ô thông báo. Tuy vậy, nó bất tiện ở chỗ ô thông báo chỉ xuất hiện khi trang Web vừa được tải về từ server. Nếu user xem nội dung của trang trong một thời gian đủ dài và không bấm vào các liên kết, các tin nhắn mới trong suốt thời gian này sẽ không xuất hiện trên ô thông báo cho đến khi user bấm vào một liên kết vả tải về một trang mới.
Để ô thông báo hữu ích hơn cho người sử dụng, chúng ta cần làm cho nó có khả năng cập nhật số tin nhắn chưa được đọc mà không cần người sử dụng phải tải lại trang. Một trở ngại trong giải pháp tĩnh hiện thời là ô thông báo chỉ được hiển thị khi số tin nhắn lớn hơn 0 khi trang được tải về. Do đó, chúng ta cần làm cho ô thông báo này luôn luôn nằm trong thanh định hướng nhưng sẽ được giấu đi khi số tin nhắn là 0, điều này sẽ giúp chúng ta dễ dàng hiển thị ô thông báo với JavaScript khi cần:
app/templates/base.html: Ô thông báo “thân thiện” hơn với JavaScript.
1 2 3 4 5 6 7 8 9 10 11 |
<li> <a href="{{ url_for('main.messages') }}"> {{ _('Messages') }} {% set new_messages = current_user.new_messages() %} <span id="message_count" class="badge" style="visibility: {% if new_messages %}visible {% else %}hidden {% endif %};"> {{ new_messages }} </span> </a> </li> |
Với phiên bản này, chúng ta luôn có ô thông báo trong mọi trang, nhưng thuộc tính CSS visibility
của nó sẽ được đặt là visible
nếu giá trị của new_messages
khác 0, hoặc là hidden
nếu là 0. Chúng ta cũng sẽ thêm thuộc tính id
cho phần tử <span>
đại diện cho ô thông báo để có thể dễ dàng truy xuất đến phần từ này với bộ chọn $('#message_count')
của jQuery.
Tiếp theo, chúng ta có thể viết một đoạn mã JavaScript ngắn để cập nhật ô thông báo với một số mới:
app/templates/base.html: Ô thông báo đã được điều chỉnh trong thanh định hướng.
1 2 3 4 5 6 7 8 9 10 |
... {% block scripts %} <script> ... function set_message_count(n) { $('#message_count').text(n); $('#message_count').css('visibility', n ? 'visible' : 'hidden'); } </script> {% endblock %} |
Hàm set_message_count()
sẽ cập nhật số tin nhắn vào ô thông báo, đồng thời điểu chỉnh thuộc tính visibility
để giấu ô này đi nếu số tin nhắn là 0 hoặc hiển thị nó trong trường hợp ngược lại.
Gởi thông báo đến người sử dụng
Phần việc còn lại là xây dựng cơ chế để cập nhật số tin nhắn trên trình duyệt của người sử dụng theo định kỳ. Trong mỗi chu kỳ cập nhật, trình duyệt sẽ gọi hàm set_message_count()
để người sử dụng biết rằng quá trình cập nhật đang diễn ra.
Có hai phương pháp để server có thể gởi các thông tin cần cập nhật đến trình duyệt của người sử dụng. Và cả hai phương pháp đều có ưu và khuyết điểm riêng. Vì vậy, việc chọn phương pháp nào tùy thuộc vào từng dự án. Trong phương pháp thứ nhất, trình duyệt sẽ định kỳ hỏi server về các thông tin cần được cập nhật bằng cách gởi một yêu cầu bất đồng bộ. Server sẽ trả lời cho yêu cầu này với một danh sách các thông tin cần được cập nhật. Nhờ các thông tin này, trình duyệt sẽ có thể cập nhật các phần tử trong trang như là ô thông báo. Phương pháp thứ hai cần sử dụng một kiểu kết nối đặc biệt giữa trình duyệt và server, cho phép server chủ động gởi các cập nhật đến trình duyệt khi cần. Lưu ý rằng dù sử dụng phương pháp nào, chúng ta đều nên xem các thông báo như là các thực thể tổng quát để có thể xây dựng cấu trúc này cho các sự kiện khác nhau ngoài thông báo số tin nhắn chưa được đọc.
Ưu điểm lớn nhất của phương pháp thứ nhất là dễ thực hiện. Chúng ta chỉ cần thêm một định tuyến mới vào ứng dụng (ví dụ như /notifications) và trả về danh sách các thông báo theo dạng JSON. Ở trình duyệt, ứng dụng sẽ nhận danh sách này và tiến hành các công việc cần thiết để cập nhật trang hiện hành. Khuyết điểm của phương pháp này là sẽ có độ trễ giữa thời gian sự kiện xảy ra và khi người sử dụng nhận được thông báo về sự kiện bởi vì trình duyệt sẽ gởi yêu cầu cập nhật theo định kỳ (cụ thể là user sẽ không nhận được thông báo ngay khi có tin nhắn mới mà phải chờ đến chu kỳ cập nhật của ứng dụng). Ví dụ như nếu ứng dụng thiết lập cho trình duyệt gởi yêu cầu cập nhật mỗi 10 giây, thông báo có thể đến trễ 10 giây sau khi sự kiện đã xảy ra.
Phương pháp thứ hai yêu cầu một số thay đổi ở mức độ giao thức bởi vì giao thức HTTP không có các phương tiện cần thiết để gởi dữ liệu đến trình duyệt khi không được yêu cầu. Cho đến nay, cách phổ biến nhất để khắc phục trở ngại này là mở rộng mã nguồn ứng dụng để hỗ trợ kết nối theo kiểu WebSocket. Không như HTTP, WebSocket cho phép tạo một liên kết cố định giữa server và trình duyệt. Nhờ đó, server và trình duyệt có thể gởi và nhận dữ liệu tại mọi thời điểm mà không cần được yêu cầu trước. Ưu điểm của phương pháp này là bất cứ khi nào một sự kiện có liên quan đến client xảy ra, server có thể gởi thông báo đến trình duyệt ngay lập tức mà không cần được trình duyệt yêu cầu trước. Khuyết điểm của phương pháp này cách xây dựng phức tạp hơn HTTP bởi vì server cần duy trì kết nối cố định đến mọi trình duyệt và các phần mềm khách khác. Hãy tưởng tượng một server với bốn worker có thể phục vụ cho hàng trăm máy khách sử dụng HTTP cùng lúc vì các kết nối HTTP không tồn tại quá lâu và thường xuyên được xoay vòng. Tuy nhiên, cùng một server như vậy chỉ có thể phục vụ bốn máy khách sử dụng kết nối WebSocket cùng lúc. Trong phần lớn các trường hợp, điều này không đủ đáp ứng nhu cầu sử dụng. Bởi vì hạn chế này nên các ứng dụng với WebSocket thường được thiết kế theo kiểu bất đồng bộ bởi vì phương pháp này hiệu quả hơn khi phải quản lý số lượng worker và kết nối lớn.
Điều tốt là cả hai phương pháp đều cung cấp hàm gọi lại (callback function) để chúng ta có thể sử dụng ở trình duyệt khi nhận được danh sách các cập nhật từ server. Vì vậy, để đơn giản, chúng ta có thể bắt đầu với phương pháp thứ nhất, và sau đó chúng ta có thể thay đổi thành server sử dụng WebSocket nếu cần thiết. Thật ra thì đối với các ứng dụng theo kiểu của chúng ta, phương pháp thứ nhất đã là đủ để đáp ứng. Phương pháp sử dụng WebSocket chỉ cần thiết cho các ứng dụng yêu cầu độ trễ thấp.
Trong trường hợp bạn muốn biết, Twitter cũng sử dụng phương pháp thứ nhất cho các thông báo trong thanh định hướng. Facebook sử dụng một biến thể của nó gọi là long polling để khắc phục một số giới hạn của việc gởi yêu cầu định kỳ với HTTP. StackOverflow và Trello sử dụng WebSocket cho các thông báo trong ứng dụng. Bạn có thể quan sát các hoạt động nền của các Web site bằng cách sử dụng thẻ Network trong công cụ phát triển của trình duyệt.
Sau đây, chúng ta sẽ bắt đầu xây dựng giải pháp theo phương pháp thứ nhất. Đầu tiên, chúng ta cần thêm một mô hình dữ liệu mới để theo dõi số thông báo cho người sử dụng và kèm theo đó là quan hệ với mô hình dữ liệu User:
app/models.py: Mô hình dữ liệu thông báo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import json from time import time ... class User(UserMixin, db.Model): ... notifications = db.relationship('Notification', backref='user', lazy='dynamic') ... class Notification(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(128), index=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id')) timestamp = db.Column(db.Float, index=True, default=time) payload_json = db.Column(db.Text) def get_data(self): return json.loads(str(self.payload_json)) |
Một thông báo sẽ bao gồm tên, user sẽ nhận thông báo, mốc thời gian (timestamp) theo định dạng Unix và dữ liệu (payload). Giá trị mặc định của mốc thời gian sẽ được lấy từ hàm time.time()
. Tùy theo kiểu của thông báo chúng ta sẽ có dữ liệu khác nhau. Vì vậy, chúng ta sẽ sử dụng một chuỗi theo định dạng JSON để chứa dữ liệu vì nó cho phép chúng ta gởi dữ liệu dưới dạng danh sách, từ điển hay một giá trị đơn lẻ như là một con số hay một chuỗi. Chúng ta cũng tạo hàm get_data()
để tăng tính tiện lợi khi cần chuyển đổi giá trị JSON thành dữ liệu (deserialization).
Với mô hình dữ liệu này, chúng ta cần cập nhật và chuyển đổi cơ sở dữ liệu:
1 2 |
(myenv) $ flask db migrate -m "thông báo" (myenv) $ flask db upgrade |
Và cũng để thuận tiện, chúng ta sẽ thêm các mô hình dữ liệu mới là Message
và Notification
vào ngữ cảnh dòng lệnh. Nhờ đó, khi chúng ta bắt đầu một dòng lệnh với lệnh flask shell
, các lớp mô hình dữ liệu sẽ tự động được tham chiếu:
myblog.py: Thêm mô hình dữ liệu mới vào ngữ cảnh dòng lệnh.
1 2 3 4 5 6 7 8 9 |
... from app.models import User, Post, Notification, Message ... @app.shell_context_processor def make_shell_context(): return {'db': db, 'User': User, 'Post': Post, 'Message': Message, 'Notification': Notification} |
Chúng ta cũng sẽ thêm hàm trợ giúp add_notification()
vào mô hình dữ liệu User
để dễ làm việc hơn với các đối tượng từ mô hình này:
app/models.py: Mô hình dữ liệu User.
1 2 3 4 5 6 7 8 |
class User(UserMixin, db.Model): ... def add_notification(self, name, data): self.notifications.filter_by(name=name).delete() n = Notification(name=name, payload_json=json.dumps(data), user=self) db.session.add(n) return n |
Phương thức này sẽ lưu một thông báo vào cơ sở dữ liệu và nếu có một thông báo cùng tên trong cơ sở dữ liệu, nó sẽ xóa thông báo có sẵn trước khi tiến hành lưu thông báo hiện tại. Thông báo chúng ta đang xây dựng sẽ được gọi là unread_message_coun
t. Nếu trong cơ sở dữ liệu có một thông báo với cùng tên gọi nhưng với số tin nhắn chưa đọc khác với số sắp được lưu (ví dụ như số tin nhắn chưa đọc hiện tại là 3, khi user nhận được một tin nhắn mới, số này sẽ được tăng lên 4 và thông báo tương ứng cần phải được thay thế trong cơ sở dữ liệu).
Mỗi khi có thay đổi về số tin nhắn chưa được đọc, chúng ta cần gọi hàm add_notification()
để cập nhật thông báo cho user. Có hai nơi trong ứng dụng có khả năng thay đổi số tin nhắn chưa đọc. Đầu tiên là khi có một tin nhắn mới được tạo ra trong hàm send_message()
:
app/main/routes.py: Cập nhật thông báo cho user.
1 2 3 4 5 6 7 8 9 10 |
@bp.route('/send_message/<recipient>', methods=['GET', 'POST']) @login_required def send_message(recipient): ... if form.validate_on_submit(): ... user.add_notification('unread_message_count', user.new_messages()) db.session.commit() ... ... |
Nơi tiếp theo mà số tin nhắn chưa đọc sẽ được cập nhật là trong trang nhắn tin, khi user mở trang này, theo quy ước của chúng ta, số tin nhắn chưa đọc sẽ trở về 0:
app/main/routes.py: Đọc các tin nhắn.
1 2 3 4 5 6 7 |
@bp.route('/messages') @login_required def messages(): current_user.last_message_read_time = datetime.utcnow() current_user.add_notification('unread_message_count', 0) db.session.commit() ... |
Sau khi đã các thông báo cho user được lưu vào cơ sở dữ liệu, chúng ta có thể thêm một địa chỉ mới và hàm xử lý tương ứng để trình duyệt có thể dùng để nhận các thông báo cho một user đã đăng nhập:
app/main/routes.py: Hàm hiển thị cho các thông báo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
from app.models import Notification ... @bp.route('/notifications') @login_required def notifications(): since = request.args.get('since', 0.0, type=float) notifications = current_user.notifications.filter( Notification.timestamp > since).order_by(Notification.timestamp.asc()) return jsonify([{ 'name': n.name, 'data': n.get_data(), 'timestamp': n.timestamp } for n in notifications]) |
Đây là một hàm tương đối đơn giản và sẽ trả về dữ liệu là mọt danh sách các thông báo cho user theo định dạng JSON. Mỗi thông báo sẽ là một từ điển với ba phần tử: tên thông báo, các dữ liệu của thông báo (payload) như là số tin nhắn, và mốc thời gian (timestamp). Các thông báo sẽ được trả về theo thứ tự thời gian chúng đã được tạo ra, từ cũ nhất đến mới nhất.
Chúng ta không muốn trả về các thông báo trùng lặp. Vì vậy, chúng ta sẽ cung cấp tùy chọn để client có thể chỉ yêu cầu các thông báo từ một thời điểm được chọn. Tùy chọn since
có thể được cung cấp qua một tham số trong địa chỉ (URL) được yêu cầu và có giá trị là mốc thời gian bắt đầu theo chuẩn thời gian của unix và có kiểu là dấu chấm động (floating point). Nếu tùy chọn này được cung cấp, chỉ có các thông báo sau thời điểm này sẽ được trả về.
Bước cuối cùng để hoàn thiện chức năng này là xây dụng mã nguồn ở phía client (trong trường hợp này là trình duyệt) để thực hiện việc gởi yêu cầu nhận thông báo đến server theo định kỳ (polling). Chúng ta sẽ đặt mã này vào trong template gốc để mọi trang trong ứng dụng đều có chức năng này:
app/templates/base.html: Yêu cầu nhận thông báo từ server theo định kỳ.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
... {% block scripts %} <script> ... {% if current_user.is_authenticated %} $(function() { var since = 0; setInterval(function() { $.ajax('{{ url_for('main.notifications') }}?since=' + since).done( function(notifications) { for (var i = 0; i < notifications.length; i++) { if (notifications[i].name == 'unread_message_count') set_message_count(notifications[i].data); since = notifications[i].timestamp; } } ); }, 10000); }); {% endif %} </script> |
Hàm này được đặt bên trong một điều kiện của template bởi vì chúng ta chỉ muốn thực hiện quá trình này cho các user đã đăng nhập. Các user không đăng nhập sẽ không sử dụng được chức năng này.
Chúng ta đã sử dụng hàm jQuery $(function() {…})
trong Phần 20. Theo quy ước của jQuery, đây là cách chúng ta đăng ký một hàm cần được thực thi sau khi một trang Web được tải xong. Trong chức năng này, chúng ta cần khởi tạo một bộ đếm giờ để thực hiện việc kiểm tra tin nhắn định kỳ sau khi trang được tải xong. Chúng ta cũng đã sử dụng hàm setTimeout()
trong JavaScript để thi hành một hàm được truyền vào sau một thời khoảng định trước. Hàm setInterval()
cũng có các tham số như hàm setTimeout()
, nhưng sẽ liên tục gọi hàm đã được truyền vào theo định kỳ thay vì chỉ một lần như trong setTimeout().
Trong trường hợp này, chúng ta sẽ đặt chu kỳ 10 giây (với giá trị tính bằng mili giây) cho mỗi lần gọi. Kết quả là số tin nhắn sẽ được cập nhật khoảng sáu lần một phút.
Hàm được sử dụng với bộ đếm giờ sẽ gởi các yêu cầu Ajax đến server theo định kỳ. Khi nhận được hồi đáp từ server, nó sẽ duyệt qua danh sách các thông báo. Nếu nhận được thông báo với tên gọi unread_message_count
, nó sẽ tiến hành cập nhật số tin nhắn trong ô thông báo trên thanh định hướng bằng cách gọi hàm set_message_count()
đã được định nghĩa ở trên với giá trị là số tin nhắn nhận được trong dữ liệu của thông báo.
Cách xử lý tham số since
trong hàm có thể hơi khó hiểu một chút. Nếu bạn cần được giải thích thì như thế này: Khi bắt đầu, chúng ta khởi tạo giá trị của tham số này là 0. Tham số này sẽ luôn đi kèm trong địa chỉ để yêu cầu thông báo, nhưng chúng ta không thể tạo ra địa chỉ kèm tham số này với hàm url_for()
theo như cách chúng ta đã làm trước đây bởi vì url_for()
chỉ thực hiện một lần tại server, nhưng chúng ta lại cần phải cập nhật since sau mỗi lần gọi. Khi hàm này được gọi lần đầu tiên, yêu cầu sẽ được gởi đến địa chỉ /notifications?since=0, nhưng sau khi chúng ta nhận được thông báo từ server, chúng ta sẽ cập nhật since với mốc thời gian tương ứng của thông báo. Điều này bảo đảm rằng chúng ta sẽ không nhận được các thông báo trùng lặp bởi vì chúng ta luôn yêu cầu các thông báo mới kể từ lần nhận thông báo cuối cùng. Cần lưu ý rằng việc khai báo biến since
bên ngoài hàm setInterval()
rất quan trọng bởi vì chúng ta cần sử dụng biến này ở mức toàn cục chứ không phải cục bộ và nhờ đó, tất cả các yêu cầu từ các trang khác nhau sẽ sử dụng cùng một biến và một giá trị thống nhất.
Cách đơn giản nhất để thử nghiệm chức năng này là sử dụng hai trình duyệt khác nhau và đăng nhập vào Myblog từ mỗi trình duyệt với một user khác nhau. Sau đó, gởi một hoặc nhiều tin nhắn từ user trên trình duyệt đầu tiên đến user trên trình duyệt thứ hai. Thanh định hướng trên trình duyệt thứ hai sẽ hiển thị số tin nhắn bạn đã gởi trong vòng 10 giây. Và nếu bạn bấm vào liên kết đến trang Message, số tin nhắn được hiển thị trong thông báo sẽ trở về 0.
Chúng ta sẽ tạm ngừng ở đây. Hẹn gặp bạn trong phần tiếp theo.