-
Flask 教程 第二十一章:用户通知(2)
我还将在用户模型中添加一个add_notification()
辅助方法,以便更轻松地处理这些对象:
app/models.py:Notification模型。
1 class User(UserMixin, db.Model): 2 # ... 3 4 def add_notification(self, name, data): 5 self.notifications.filter_by(name=name).delete() 6 n = Notification(name=name, payload_json=json.dumps(data), user=self) 7 db.session.add(n) 8 return n
此方法不仅为用户添加通知给数据库,还确保如果具有相同名称的通知已存在,则会首先删除该通知。 我将要使用的通知将被称为unread_message_count
。 如果数据库已经有一个带有这个名称的通知,例如值为3,则当用户收到新消息并且消息计数变为4时,我就会替换旧的通知。
在任何未读消息数改变的地方,我需要调用add_notification()
,以便我更新用户的通知,这样的地方有两处。 首先,在send_message()
视图函数中,当用户收到一个新的私有消息时:
app/main/routes.py:更新用户通知。
1 @bp.route('/send_message/<recipient>', methods=['GET', 'POST']) 2 @login_required 3 def send_message(recipient): 4 # ... 5 if form.validate_on_submit(): 6 # ... 7 user.add_notification('unread_message_count', user.new_messages()) 8 db.session.commit() 9 # ... 10 # ...
第二个地方是用户转到消息页面时,未读计数需要归零:
app/main/routes.py:查看消息视图函数。
1 @bp.route('/messages') 2 @login_required 3 def messages(): 4 current_user.last_message_read_time = datetime.utcnow() 5 current_user.add_notification('unread_message_count', 0) 6 db.session.commit() 7 # ...
既然用户的所有通知都保存在数据库中,那么我可以添加一条新路由,客户端可以使用该路由为登录用户检索通知:
app/main/routes.py:通知视图函数。
1 from app.models import Notification 2 3 # ... 4 5 @bp.route('/notifications') 6 @login_required 7 def notifications(): 8 since = request.args.get('since', 0.0, type=float) 9 notifications = current_user.notifications.filter( 10 Notification.timestamp > since).order_by(Notification.timestamp.asc()) 11 return jsonify([{ 12 'name': n.name, 13 'data': n.get_data(), 14 'timestamp': n.timestamp 15 } for n in notifications])
这是一个相当简单的函数,它返回一个包含用户通知列表的JSON负载。 每个通知都以包含三个元素的字典的形式给出,即通知名称,与通知有关的附加数据(如消息数量)和时间戳。 通知按照从创建时间顺序进行排序。
我不希望客户重复发送通知,所以我给他们提供了一个选项,只请求给定时间戳之后产生的通知。 since
选项可以作为浮点数包含在请求URL的查询字符串中,其中包含开始时间的unix时间戳。 如果包含此参数,则只有在此时间之后发生的通知才会被返回。
完成此功能的最后一部分是在客户端实现实际轮询。 最好的做法是在基础模板中实现,以便所有页面自动继承该行为:
app/templates/base.html:轮询通知。
1 ... 2 {% block scripts %} 3 <script> 4 // ... 5 {% if current_user.is_authenticated %} 6 $(function() { 7 var since = 0; 8 setInterval(function() { 9 $.ajax('{{ url_for('main.notifications') }}?since=' + since).done( 10 function(notifications) { 11 for (var i = 0; i < notifications.length; i++) { 12 if (notifications[i].name == 'unread_message_count') 13 set_message_count(notifications[i].data); 14 since = notifications[i].timestamp; 15 } 16 } 17 ); 18 }, 10000); 19 }); 20 {% endif %} 21 </script>
该函数包含在一个模板条件中,因为我只想在用户登录时轮询新消息。对于没有登录的用户,这个函数将不会被渲染。
你已经在第二十章中看到了jQuery的$(function() { ...})
模式。 这是注册一个函数在页面加载后执行的方式。 对于这个功能,我需要在页面加载时做的是设置一个定时器来获取用户的通知。 你还看到了setTimeout()
JavaScript函数,它在等待特定时间之后运行作为参数给出的函数。 setInterval()
函数使用与setTimeout()
相同的参数,但不是一次性触发定时器,而是定期调用回调函数。 本处,我的间隔设置为10秒(以毫秒为单位),所以我将以每分钟大约六次的频率查看通知是否有更新。
利用定期计时器和Ajax,该函数轮询新通知路由,并在其完成回调中迭代通知列表。 当收到名为unread_message_count
的通知时,通过调用上面定义的函数和通知中给出的计数来调整消息计数徽章。
我处理since
参数的方式可能会令人困惑。 我首先将这个参数初始化为0。 参数总是包含在请求URL中,但是我不能像以前那样使用Flask的url_for()
来生成查询字符串,因为一次请求中url_for()
只在服务器上运行一次,而我需要since
参数动态更新多次。 第一次,这个请求将被发送到/notifications?since=0,但是一旦我收到通知,我就会将since
更新为它的时间戳。 这可以确保我不会收到重复的内容,因为我总是要求收到自我上次看到的通知以来发生的新通知。 同样重要的是要注意,我在interval函数外声明since
变量,因为我不希望它是局部变量,我想要在所有调用中使用相同的变量。
最简单的测试方法是使用两种不同的浏览器A和B。 在两个浏览器上使用不同的用户登录Microblog。 然后从A浏览器向B浏览器上的用户发送一个或多个消息。 B浏览器的导航栏应更新为显示你在10秒钟内发送的消息数量。 而当你点击消息链接时,未读消息数重置为零。