Уведомления

11

На данный момент переведено

В этой главе вы:

  • Создадите коллекцию для уведомлений, чтобы оповещать пользователей о действиях остальных.
  • Научитесь делать доступными только те уведомления, которые имеют отношение к текущему пользователю.
  • Узнаете больше о публикациях и подписках.
  • Теперь, когда пользователи могут оставлять комментарии к постам друг друга, было бы здорово сообщать им о начале диалога.

    Для этого мы будем уведомлять автора поста о том, что был добавлен новый комментарий, и предоставлять ссылку, по которой комментарий можно посмотреть.

    Это как раз тот тип функциональности, при котором Meteor демонстрирует свои сильные стороны: благодаря real-time природе Meteor такие уведомления будут отображаться мгновенно. Нам не нужно ждать, пока пользователь обновит страницу, или что-либо проверять - мы можем просто добавлять новые уведомления без необходимости писать для этого какой-нибудь специальный код.

    Создаем уведомления

    Мы будем создавать уведомление каждый раз, когда кто-то оставляет комментарий к вашему посту. В будущем уведомления могут быть также использованы и при многих других сценариях, но сейчас этого будет достаточно, чтобы держать пользователя в курсе происходящего.

    Создадим коллекцию Notifications, а также функцию createCommentNotification, которая будет добавлять соответствующее уведомление для каждого нового комментария к вашим постам.

    Так как мы будем обновлять уведомления с клиентской стороны, нужно удостовериться в надежности вызова ‘allow’. Мы проверим это следующим образом:

    • Пользователь, вызывающий update, является владельцем обновляемого уведомления.
    • Пользователь пытается обновить только одно поле.
    • Этим единственным полем является свойство read.
    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
        });
      }
    };
    
    lib/collections/notifications.js

    Подобно постам и комментариям, коллекция Notifications будет доступна и на клиенте, и на сервере. Так как нам нужна будет возможность обновлять уведомления после того, как пользователь их увидит, мы разрешаем выполнение операции update, убедившись, как обычно, что пользователь может редактировать только свои собственные данные.

    Мы также написали несложную функцию, которая во время добавления комментария проверяет пост, определяет, кому о нем нужно сообщить, и создает новые уведомления.

    У нас уже есть серверный метод для создания комментариев, поэтому остается всего лишь добавить в него вызов нашей функции. Мы заменяем return Comments.insert(comment); на comment._id = Comments.insert(comment), чтобы сохранить _id нового комментария в переменной, а затем вызываем функцию createCommentNotification:

    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;
      }
    });
    
    lib/collections/comments.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();
    });
    
    server/publications.js

    И подписываемся на клиенте:

    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      notFoundTemplate: 'notFound',
      waitOn: function() {
        return [Meteor.subscribe('posts'), Meteor.subscribe('notifications')]
      }
    });
    
    lib/router.js

    Коммит 11-1

    Added basic notifications collection.

    Отображаем уведомления

    Теперь мы можем добавить список уведомлений к заголовку.

    <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>
    
    client/templates/includes/header.html

    Создадим шаблоны notifications и notificationItem (они будут делить между собой файл notifications.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>
    
    client/templates/notifications/notifications.html

    Как мы видим, план состоит в том, чтобы каждое уведомление содержало ссылку на пост с соответствующим комментарием и имя пользователя, который оставил этот комментарий.

    Далее нам нужно убедиться, что мы выбираем правильный список уведомлений в хелпере, а также помечаем уведомления как “read” (“прочитанные”), когда пользователь проходит по указанной в них ссылке.

    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}});
      }
    });
    
    client/templates/notifications/notifications.js

    Коммит 11-2

    Display notifications in the header.

    Вы могли бы подумать, что уведомления не так уж сильно отличаются от ошибок: они действительно очень схожи по структуре. Тем не менее у них есть одно ключевое различие: в случае с уведомлениями мы создали настоящую синхронизованную между клиентом и сервером коллекцию. Это значит, что наши уведомления постоянны и при использовании одной и той же учетной записи будут оставаться неизменными при обновлении браузера и для разных устройств.

    Попробуйте открыть второй браузер (к примеру, Firefox), создать новую учетную запись и добавить комментарий к посту, который вы создали, используя свою основную учетную запись (ту, что осталась открытой в Chrome). Вы должны видеть что-то вроде этого:

    Displaying notifications.
    Displaying notifications.

    Контролируем доступ к уведомлениям

    Итак, с функционированием наших уведомлений все в порядке. Но есть одна небольшая проблема: они находятся в публичном доступе.

    Если ваш второй браузер все еще открыт, попробуйте выполнить в его консоли следующий код:

     Notifications.find().count();
    1
    
    Browser console

    Новый пользователь (тот, что добавил комментарий) не должен иметь никаких уведомлений. То уведомление, которое они оба могут видеть в коллекции Notifications, на самом деле принадлежит нашему первоначальному пользователю.

    Даже если не учитывать потенциальные проблемы с конфиденциальностью, мы просто не можем себе позволить загружать полную коллекцию уведомлений в браузер каждого пользователя. Для относительно крупного сайта это может привести к перегрузке памяти, доступной браузеру, и серьезным проблемам с производительностью.

    Эта проблема может быть решена при помощи публикаций. Мы можем использовать их, чтобы уточнить, какую часть нашей коллекции мы хотим отправить каждому браузеру.

    С этой целью мы должны возвращать из публикации курсор, отличный от Notifications.find(). А конкретнее, нам нужен курсор, соответствующий уведомлениям только для текущего пользователя.

    Сделать это достаточно просто, учитывая, что функция publish имеет доступ к _id текущего пользователя через this.userId:

    Meteor.publish('notifications', function() {
      return Notifications.find({userId: this.userId, read: false});
    });
    
    server/publications.js

    Коммит 11-3

    Only sync notifications that are relevant to the user.

    Если мы снова проверим два наших браузера, мы должны увидеть две разных коллекции с уведомлениями:

     Notifications.find().count();
    1
    
    Browser console (user 1)
     Notifications.find().count();
    0
    
    Browser console (user 2)

    На самом деле, список уведомлений должен изменяться даже тогда, когда вы входите в систему или выходите из нее. Это происходит из-за того, что публикации автоматически обновляются при смене учетной записи.

    Наше приложение становится все более функциональным, и в силу того, что много новых пользователей регистрируются и начинают постить ссылки, возникает риск бесконечной домашней страницы. Мы решим эту проблему в следующей главе, добавив нумерацию страниц.