forked from DiscoverMeteor/DiscoverMeteor_Ru
-
Notifications
You must be signed in to change notification settings - Fork 0
/
11-notifications.md.erb
279 lines (219 loc) · 15 KB
/
11-notifications.md.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
---
title: Уведомления
slug: notifications
date: 0011/01/01
number: 11
contents: Создадите коллекцию для уведомлений, чтобы оповещать пользователей о действиях остальных.|Научитесь делать доступными только те уведомления, которые имеют отношение к текущему пользователю.|Узнаете больше о публикациях и подписках.
paragraphs: 25
---
Теперь, когда пользователи могут оставлять комментарии к постам друг друга, было бы здорово сообщать им о начале диалога.
Для этого мы будем уведомлять автора поста о том, что был добавлен новый комментарий, и предоставлять ссылку, по которой комментарий можно посмотреть.
Это как раз тот тип функциональности, при котором Meteor демонстрирует свои сильные стороны: благодаря real-time природе Meteor такие уведомления будут отображаться _мгновенно_. Нам не нужно ждать, пока пользователь обновит страницу, или что-либо проверять - мы можем просто добавлять новые уведомления без необходимости писать для этого какой-нибудь специальный код.
### Создаем уведомления
Мы будем создавать уведомление каждый раз, когда кто-то оставляет комментарий к вашему посту. В будущем уведомления могут быть также использованы и при многих других сценариях, но сейчас этого будет достаточно, чтобы держать пользователя в курсе происходящего.
Создадим коллекцию `Notifications`, а также функцию `createCommentNotification`, которая будет добавлять соответствующее уведомление для каждого нового комментария к вашим постам.
Так как мы будем обновлять уведомления с клиентской стороны, нужно удостовериться в надежности вызова 'allow'. Мы проверим это следующим образом:
- Пользователь, вызывающий `update`, является владельцем обновляемого уведомления.
- Пользователь пытается обновить только одно поле.
- Этим единственным полем является свойство `read`.
~~~js
Notifications = new Mongo.Collection('notifications');
Notifications.allow({
update: function(userId, doc, fieldNames) {
return ownsDocument(userId, doc) &&
fieldNames.length === 1 && fieldNames[0] === 'read';
}
});
createCommentNotification = function(comment) {
var post = Posts.findOne(comment.postId);
if (comment.userId !== post.userId) {
Notifications.insert({
userId: post.userId,
postId: post._id,
commentId: comment._id,
commenterName: comment.author,
read: false
});
}
};
~~~
<%= caption "lib/collections/notifications.js" %>
Подобно постам и комментариям, коллекция `Notifications` будет доступна и на клиенте, и на сервере. Так как нам нужна будет возможность обновлять уведомления после того, как пользователь их увидит, мы разрешаем выполнение операции `update`, убедившись, как обычно, что пользователь может редактировать только свои собственные данные.
Мы также написали несложную функцию, которая во время добавления комментария проверяет пост, определяет, кому о нем нужно сообщить, и создает новые уведомления.
У нас уже есть серверный метод для создания комментариев, поэтому остается всего лишь добавить в него вызов нашей функции. Мы заменяем `return Comments.insert(comment);` на `comment._id = Comments.insert(comment)`, чтобы сохранить `_id` нового комментария в переменной, а затем вызываем функцию `createCommentNotification`:
~~~js
Comments = new Mongo.Collection('comments');
Meteor.methods({
comment: function(commentAttributes) {
//...
comment = _.extend(commentAttributes, {
userId: user._id,
author: user.username,
submitted: new Date()
});
// обновляем количество комментариев для поста
Posts.update(comment.postId, {$inc: {commentsCount: 1}});
// создаем комментарий и сохраняем id
comment._id = Comments.insert(comment);
// создаем уведомление, информируя пользователя о новом комментарии
createCommentNotification(comment);
return comment._id;
}
});
~~~
<%= caption "lib/collections/comments.js" %>
<%= highlight "17~123" %>
Затем публикуем уведомления:
~~~js
Meteor.publish('posts', function() {
return Posts.find();
});
Meteor.publish('comments', function(postId) {
check(postId, String);
return Comments.find({postId: postId});
});
Meteor.publish('notifications', function() {
return Notifications.find();
});
~~~
<%= caption "server/publications.js" %>
<%= highlight "10~12" %>
И подписываемся на клиенте:
~~~js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() {
return [Meteor.subscribe('posts'), Meteor.subscribe('notifications')]
}
});
~~~
<%= caption "lib/router.js" %>
<%= highlight "6" %>
<%= commit "11-1", "Added basic notifications collection." %>
### Отображаем уведомления
Теперь мы можем добавить список уведомлений к заголовку.
~~~html
<template name="header">
<nav class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navigation">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{pathFor 'postsList'}}">Microscope</a>
</div>
<div class="collapse navbar-collapse" id="navigation">
<ul class="nav navbar-nav">
{{#if currentUser}}
<li>
<a href="{{pathFor 'postSubmit'}}">Submit Post</a>
</li>
<li class="dropdown">
{{> notifications}}
</li>
{{/if}}
</ul>
<ul class="nav navbar-nav navbar-right">
{{> loginButtons}}
</ul>
</div>
</div>
</nav>
</template>
~~~
<%= caption "client/templates/includes/header.html" %>
<%= highlight "15~22" %>
Создадим шаблоны `notifications` и `notificationItem` (они будут делить между собой файл `notifications.html`):
~~~html
<template name="notifications">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
Notifications
{{#if notificationCount}}
<span class="badge badge-inverse">{{notificationCount}}</span>
{{/if}}
<b class="caret"></b>
</a>
<ul class="notification dropdown-menu">
{{#if notificationCount}}
{{#each notifications}}
{{> notificationItem}}
{{/each}}
{{else}}
<li><span>No Notifications</span></li>
{{/if}}
</ul>
</template>
<template name="notificationItem">
<li>
<a href="{{notificationPostPath}}">
<strong>{{commenterName}}</strong> commented on your post
</a>
</li>
</template>
~~~
<%= caption "client/templates/notifications/notifications.html" %>
Как мы видим, план состоит в том, чтобы каждое уведомление содержало ссылку на пост с соответствующим комментарием и имя пользователя, который оставил этот комментарий.
Далее нам нужно убедиться, что мы выбираем правильный список уведомлений в хелпере, а также помечаем уведомления как "read" ("прочитанные"), когда пользователь проходит по указанной в них ссылке.
~~~js
Template.notifications.helpers({
notifications: function() {
return Notifications.find({userId: Meteor.userId(), read: false});
},
notificationCount: function(){
return Notifications.find({userId: Meteor.userId(), read: false}).count();
}
});
Template.notification.helpers({
notificationPostPath: function() {
return Router.routes.postPage.path({_id: this.postId});
}
});
Template.notification.events({
'click a': function() {
Notifications.update(this._id, {$set: {read: true}});
}
});
~~~
<%= caption "client/templates/notifications/notifications.js" %>
<%= commit "11-2", "Display notifications in the header." %>
Вы могли бы подумать, что уведомления не так уж сильно отличаются от ошибок: они действительно очень схожи по структуре. Тем не менее у них есть одно ключевое различие: в случае с уведомлениями мы создали настоящую синхронизованную между клиентом и сервером коллекцию. Это значит, что наши уведомления *постоянны* и при использовании одной и той же учетной записи будут оставаться неизменными при обновлении браузера и для разных устройств.
Попробуйте открыть второй браузер (к примеру, Firefox), создать новую учетную запись и добавить комментарий к посту, который вы создали, используя свою основную учетную запись (ту, что осталась открытой в Chrome). Вы должны видеть что-то вроде этого:
<%= screenshot "11-1", "Displaying notifications." %>
### Контролируем доступ к уведомлениям
Итак, с функционированием наших уведомлений все в порядке. Но есть одна небольшая проблема: они находятся в публичном доступе.
Если ваш второй браузер все еще открыт, попробуйте выполнить в его консоли следующий код:
~~~js
❯ Notifications.find().count();
1
~~~
<%= caption "Browser console" %>
Новый пользователь (тот, что *добавил комментарий*) не должен иметь никаких уведомлений. То уведомление, которое они оба могут видеть в коллекции `Notifications`, на самом деле принадлежит нашему *первоначальному* пользователю.
Даже если не учитывать потенциальные проблемы с конфиденциальностью, мы просто не можем себе позволить загружать полную коллекцию уведомлений в браузер каждого пользователя. Для относительно крупного сайта это может привести к перегрузке памяти, доступной браузеру, и серьезным проблемам с производительностью.
Эта проблема может быть решена при помощи **публикаций**. Мы можем использовать их, чтобы уточнить, какую часть нашей коллекции мы хотим отправить каждому браузеру.
С этой целью мы должны возвращать из публикации курсор, отличный от `Notifications.find()`. А конкретнее, нам нужен курсор, соответствующий уведомлениям только для текущего пользователя.
Сделать это достаточно просто, учитывая, что функция `publish` имеет доступ к `_id` текущего пользователя через `this.userId`:
~~~js
Meteor.publish('notifications', function() {
return Notifications.find({userId: this.userId, read: false});
});
~~~
<%= caption "server/publications.js" %>
<%= commit "11-3", "Only sync notifications that are relevant to the user." %>
Если мы снова проверим два наших браузера, мы должны увидеть две разных коллекции с уведомлениями:
~~~js
❯ Notifications.find().count();
1
~~~
<%= caption "Browser console (user 1)" %>
~~~js
❯ Notifications.find().count();
0
~~~
<%= caption "Browser console (user 2)" %>
На самом деле, список уведомлений должен изменяться даже тогда, когда вы входите в систему или выходите из нее. Это происходит из-за того, что публикации автоматически обновляются при смене учетной записи.
Наше приложение становится все более функциональным, и в силу того, что много новых пользователей регистрируются и начинают постить ссылки, возникает риск бесконечной домашней страницы. Мы решим эту проблему в следующей главе, добавив нумерацию страниц.