Комментарии

10

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

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

  • Показываем комментарии.
  • Добавляем форму для нового комментария.
  • Узнаем как загрузить только комментарии текущего поста.
  • Добавляем параметр с количеством комментариев к посту.
  • Целью каждого социального сайта является создание сообщества пользователей. Это было бы невозможно, если у людей не было бы способа общаться друг с другом. В этой главе мы добавим в наше приложение возможность оставлять комментарии к постам.

    Для начала создадим новую коллекцию для хранения комментариев. Также сразу добавим в нее тестовые комментарии (fixtures).

    Comments = new Meteor.Collection('comments');
    
    collections/comments.js
    // Fixture data
    if (Posts.find().count() === 0) {
      var now = new Date().getTime();
    
      // create two users
      var tomId = Meteor.users.insert({
        profile: { name: 'Tom Coleman' }
      });
      var tom = Meteor.users.findOne(tomId);
      var sachaId = Meteor.users.insert({
        profile: { name: 'Sacha Greif' }
      });
      var sacha = Meteor.users.findOne(sachaId);
    
      var telescopeId = Posts.insert({
        title: 'Introducing Telescope',
        userId: sacha._id,
        author: sacha.profile.name,
        url: 'http://sachagreif.com/introducing-telescope/',
        submitted: now - 7 * 3600 * 1000
      });
    
      Comments.insert({
        postId: telescopeId,
        userId: tom._id,
        author: tom.profile.name,
        submitted: now - 5 * 3600 * 1000,
        body: 'Interesting project Sacha, can I get involved?'
      });
    
      Comments.insert({
        postId: telescopeId,
        userId: sacha._id,
        author: sacha.profile.name,
        submitted: now - 3 * 3600 * 1000,
        body: 'You sure can Tom!'
      });
    
      Posts.insert({
        title: 'Meteor',
        userId: tom._id,
        author: tom.profile.name,
        url: 'http://meteor.com',
        submitted: now - 10 * 3600 * 1000
      });
    
      Posts.insert({
        title: 'The Meteor Book',
        userId: tom._id,
        author: tom.profile.name,
        url: 'http://themeteorbook.com',
        submitted: now - 12 * 3600 * 1000
      });
    }
    
    server/fixtures.js

    Добавим механизм публикации и подписки на нашу коллекцию:

    Meteor.publish('posts', function() {
      return Posts.find();
    });
    
    Meteor.publish('comments', function() {
      return Comments.find();
    });
    
    server/publications.js
    Router.configure({
      layoutTemplate: 'layout',
      loadingTemplate: 'loading',
      waitOn: function() {
        return [Meteor.subscribe('posts'), Meteor.subscribe('comments')];
      }
    });
    
    lib/router.js

    Коммит 10-1

    Добавлена коллекция комментариев, публикация и подписка н…

    Обратите внимание - чтобы тестовые комментарии оказались в базе данных вам нужно вызвать команду meteor reset. Эта команда сбросит все записи в Mongo и перезапишет в нее все тестовые данные (fixtures). Учетные записи пользователей также будут стерты, так что придется зарегистрироваться заново.

    При создании тестовых записей произошли следующие вещи. Сначала мы создали двух тестовых пользователей - Tom Coleman и Sacha Greif. Затем мы использовали параметр id чтобы найти их в базе данных. От имени каждого пользователя был создан комментарий под первым постом, в комментарии был добавлен id поста (объект комментария содержит параметр postId) и id автора (параметр userId). Мы также добавили дату создания комментария, текст самого комментария и денормализованное поле author.

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

    Показываем комментарии

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

    <template name="postPage">
      {{> postItem}}
    
      <ul class="comments">
        {{#each comments}}
          {{> comment}}
        {{/each}}
      </ul>
    </template>
    
    client/views/posts/post_page.html
    Template.postPage.helpers({
      comments: function() {
        return Comments.find({postId: this._id});
      }
    });
    
    client/views/posts/post_page.js

    Мы добавили блок {{#each comments}} в шаблон поста, так чтобы this указывал на пост в хелпере comments. Чтобы найти соответствующие посту комментарии, мы проверяем их по атрибуту текущего поста postId.

    Учитывая все, что мы узнали о хелперах и шаблонах handlebars, отрисовывание комментария происходит привычным способом. Добавим новую папку comments внутри папки views для всех шаблонов, связанных с комментариями:

    <template name="comment">
      <li>
        <h4>
          <span class="author">{{author}}</span>
          <span class="date">on {{submittedText}}</span>
        </h4>
        <p>{{body}}</p>
      </li>
    </template>
    
    client/views/comments/comment.html

    Создадим небольшой шаблон-помощник для форматирования даты создания комментария в читаемый вид (если, конечно, вы не из тех редких людей кто чувствует себя комфортно с форматами дат UNIX и шестнадцатиричными кодами цветов).

    Template.comment.helpers({
      submittedText: function() {
        return new Date(this.submitted).toString();
      }
    });
    
    client/views/comments/comment.js

    Затем мы покажем цифру с количеством комментариев для каждого поста:

    <template name="postItem">
      <div class="post">
        <div class="post-content">
          <h3><a href="{{url}}">{{title}}</a><span>{{domain}}</span></h3>
          <p>
            submitted by {{author}},
            <a href="{{pathFor 'postPage'}}">{{commentsCount}} comments</a>
            {{#if ownPost}}<a href="{{pathFor 'postEdit'}}">Edit</a>{{/if}}
          </p>
        </div>
        <a href="{{pathFor 'postPage'}}" class="discuss btn">Discuss</a>
      </div>
    </template>
    
    client/views/posts/post_item.html

    И добавим хелпер commentsCount для нашего менеджера postItem:

    Template.postItem.helpers({
      ownPost: function() {
        return this.userId == Meteor.userId();
      },
      domain: function() {
        var a = document.createElement('a');
        a.href = this.url;
        return a.hostname;
      },
      commentsCount: function() {
        return Comments.find({postId: this._id}).count();
      }
    });
    
    client/views/posts/post_item.js

    Коммит 10-2

    Показываем комментарии на странице с постом

    Теперь наши тестовые комментарии должны появиться на странице.

    Показываем комментарии
    Показываем комментарии

    Создание новых комментариев

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

    Для начала мы добавим форму нового комментария под каждым постом:

    <template name="postPage">
      {{> postItem}}
    
      <ul class="comments">
        {{#each comments}}
          {{> comment}}
        {{/each}}
      </ul>
    
      {{#if currentUser}}
        {{> commentSubmit}}
      {{else}}
        <p>Please log in to leave a comment.</p>
      {{/if}}
    </template>
    
    client/views/posts/post_page.html

    Затем мы создадим шаблон с формой нового комментария:

    <template name="commentSubmit">
      <form name="comment" class="comment-form">
        <div class="control-group">
            <div class="controls">
                <label for="body">Comment on this post</label>
                <textarea name="body"></textarea>
            </div>
        </div>
        <div class="control-group">
            <div class="controls">
                <button type="submit" class="btn">Add Comment</button>
            </div>
        </div>
      </form>
    </template>
    
    client/views/comments/comment_submit.html
    Форма для создания нового комментария
    Форма для создания нового комментария

    Для отправки комментариев мы вызовем Метод comment в менеджере commentSubmit, который работает почти также, как и менеджер postSubmit:

    Template.commentSubmit.events({
      'submit form': function(e, template) {
        e.preventDefault();
    
        var $body = $(e.target).find('[name=body]');
        var comment = {
          body: $body.val(),
          postId: template.data._id
        };
    
        Meteor.call('comment', comment, function(error, commentId) {
          if (error){
            throwError(error.reason);
          } else {
            $body.val('');
          }
        });
      }
    });
    
    client/views/comments/comment_submit.js

    Точно так же, как ранее мы создали Метод Meteor post на сервере, мы создадим еще один Метод под названием comment для создания новых комментариев, проверки комментариев на соответствие заданным нами правилам, и наконец сохранения комментариев в коллекцию базы данных.

    Comments = new Meteor.Collection('comments');
    
    Meteor.methods({
      comment: function(commentAttributes) {
        var user = Meteor.user();
        var post = Posts.findOne(commentAttributes.postId);
        // ensure the user is logged in
        if (!user)
          throw new Meteor.Error(401, "You need to login to make comments");
    
        if (!commentAttributes.body)
          throw new Meteor.Error(422, 'Please write some content');
    
        if (!post)
          throw new Meteor.Error(422, 'You must comment on a post');
    
        comment = _.extend(_.pick(commentAttributes, 'postId', 'body'), {
          userId: user._id,
          author: user.username,
          submitted: new Date().getTime()
        });
    
        return Comments.insert(comment);
      }
    });
    
    collections/comments.js

    Коммит 10-3

    Форма для отправки комментариев готова.

    Ничего особенного - мы просто проверяем что пользователь залогинен, что у комментария есть текст, и что он относится к конкретному посту.

    Управляем подпиской на комментарии

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

    Если подумать, нам нужна подписка на комментарии только когда пользователь открыл страницу конкретного поста - и даже тогда нам нужны комментарии только для этого поста.

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

    Но теперь мы хотели бы, чтобы наша подписка зависела от параметра в адресе страницы - и этот параметр менялся бы в зависимости от страницы. Поэтому мы переместим код подписки с уровня маршрутизатора на уровень маршрута.

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

    Так будет выглядеть новая функция waitOn на уровне маршрута:

    Router.map(function() {
    
      //...
    
      this.route('postPage', {
        path: '/posts/:_id',
        waitOn: function() {
          return Meteor.subscribe('comments', this.params._id);
        },
        data: function() { return Posts.findOne(this.params._id); }
      });
    
      //...
    
    });
    
    lib/router.js

    Вы заметите что мы передаем this.params._id как аргумент для подписки. Используем этот параметр, чтобы ограничить публикуемые комментарии только теми, что относятся к данному посту:

    Meteor.publish('posts', function() {
      return Posts.find();
    });
    
    Meteor.publish('comments', function(postId) {
      return Comments.find({postId: postId});
    });
    
    server/publications.js

    Коммит 10-4

    Создан простой механизм публикации и подписки для коммент…

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

    У вас нет комментариев!
    У вас нет комментариев!

    Считаем комментарии

    Причина такого поведения проста - мы всегда загружаем комментарии только одного поста. Когда же мы вызываем Comments.find({postId: this._id}) в хелпере commentsCount в менеджере post_item, Meteor не может найти необходимые данные на клиенте чтобы правильно посчитать количество комментариев.

    Лучший решением будет денормализовать количество комментариев прямо в пост (если вы не уверены что это означает, не волнуйтесь - следующая вспомогательная глава все прояснит). Несмотря на то, что наш код стал немного сложнее, это отлично компенсируется скоростью работы приложения когда не нужно публиковать все комментарии все время.

    Добавим свойство commentsCount в структуру объекта post. Также, обновим структуру тестовых постов (и вызовем meteor reset чтобы их перезагрузить. Не забудьте заново зарегистрировать пользователя):

    var telescopeId = Posts.insert({
      title: 'Introducing Telescope',
      ..
      commentsCount: 2
    });
    
    Posts.insert({
      title: 'Meteor',
      ...
      commentsCount: 0
    });
    
    Posts.insert({
      title: 'The Meteor Book',
      ...
      commentsCount: 0
    });
    
    server/fixtures.js

    Затем мы удостоверимся, что у всех новых постов 0 комментариев:

    // выбираем нужные поля
    var post = _.extend(_.pick(postAttributes, 'url', 'title', 'message'), {
      userId: user._id,
      author: user.username,
      submitted: new Date().getTime(),
      commentsCount: 0
    });
    
    var postId = Posts.insert(post);
    
    collections/posts.js

    Дальше мы просто обновляем значение свойства commentsCount когда создается новый комментарий. У Mongo как раз есть подходящий оператор $inc для инкрементного увеличения значения параметра:

    // update the post with the number of comments
    Posts.update(comment.postId, {$inc: {commentsCount: 1}});
    
    return Comments.insert(comment);
    
    collections/comments.js

    И еще теперь мы можем удалить хелпер commentsCount из client/views/posts/post_item.js, так как это поле теперь доступно прямо в объекте поста.

    Коммит 10-5

    Денормализуем количество комментариев в объект поста.

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