Kommentare

10

Zu ...% übersetzt

In diesem Kapitel wirst du:

  • Anzeigen von existierenden Kommentaren.
  • Hinzufügen eines Kommentarformulars.
  • Lerne wie du erreichst, dass nur Kommentare des aktuellen Beitrags geladen werden.
  • Füge eine Kommentaranzahl-Eigenschaft zu den Beiträgen hinzu.
  • Das Ziel einer Social-Media-News-Site ist es eine aktive Benutzer-Community zu erschaffen. Damit dieses Ziel erreicht werden kann, ist es unerlässlich, dass sich die Benutzer austauschen können. Dazu lass uns in diesem Kapitel eine Kommentarfunktion hinzufügen.

    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

    Wir dürfen nicht vergessen die neue Collection im Server zu veröffentlichen (publish) und im Router zu abonnieren (subscribe):

    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

    Commit 10-1

    Added comments collection, pub/sub and fixtures.

    Damit der Fixture-Code ausgeführt wird, musst du vorher die Datenbank mit: meteor reset entleeren. Denk daran einen neuen Benutzer nach dem Reset anzulegen.

    Im Fixture-Code legen wir als Erstes ein paar Dummy-Benutzer an, fügen sie zur Datenbank hinzu und verwenden die generierten ids um sie anschließend wieder aus der Datenbank abzurufen. Anschließend fügen wir für jeden Benutzer einen Kommentar am ersten Beitrag hinzu. Die Verbindung vom Kommentar zum Beitrag stellen wir mit dem Attribut postId her und die Verbindung zum Benutzer analog mit dem Attribut userId. Weiterhin bekommen unsere Komentare ein Anlagedatum (submitted), ein Attribut für den Kommentartext (body) und den Namen des Authors (ein denormalisiertes Feld).

    Bleibt anzumerken, dass wir unseren Router so erweitert haben, dass nun auf die Initialisierung der posts als auch der comments gewartet wird.

    Displaying comments

    Wir schreiben die Kommentare nun erfolgreich in die Datenbank, aber wir müssen sie auch auf der Diskussions-Seite zur Anzeige bringen. Die notwendigen Schritte um dies zu erreichen, sollten dir inzwischen ziemlich vertraut sein.

    <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

    Im Template postPage fügen wir den Block {{#each comments}} ein. Innerhald des Helpers comments entspricht this damit einem Beitrag. Um die zugehörigen Kommenentare zu finden selektieren wir die Kommentare nach der ID des Beitrags.

    Berücksichtigen wir, was wir über Helpers und Handlebars gelernt haben, gestaltet sich das Rendering eines Kommentars als ziemlich unkompliziert. Wir legen ein neues Verzeichnis namens comments im views Verzeichnis an. Dort werden alle Informationen zu unseren Kommentaren gespeichert.

    <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

    Lass uns einen einfachen Template-Helper definieren, der dazu dient das Anlagedatum in einem menschenlesbaren Format auszugeben (es sei denn, du gehörst zu denen, die UNIX-Timestamps und hexadezimale Farbencodes flüssig lesen und schreiben können.)

    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

    ////

    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

    Commit 10-2

    Display comments on `postPage`.

    Die Fixture-Kommentare solltest du nun zur Anzeige bringen können. Das Ganze sollte in etwa so aussehen:

    Displaying comments
    Displaying comments

    Submitting Comments

    Lass uns einen Weg bereitstellen, der es den Benutzern erlaubt neue Kommentare hinzuzufügen. Dies gestaltet sich ziemlich ähnlich zur Vorgehensweise, die wir angewandt haben, um das Anlegen neue Beiträgen zu ermöglichen.

    Wir beginnen mit dem Hinzufügen eines Kommentarbereichs am Ende eines jeden Beitrags.

    <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

    Und erzeugen dann das Formular-Template zur Anlage eines Kommentars.

    <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
    The comment submit form
    The comment submit form

    Zum Übermitteln unseres Kommentars wird die Methode comment im Manager commentSubmit aufgerufen. Dieser arbeitet analog zu dem Manager 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

    Genau wie wir vorher die serverseitige Meteor-Methode post definiert haben, legen wir nun die Meteor-Methode comment an, wir überprüfen die übermittelten Daten, erzeugen einen Kommentar-Objekt und fügen dieses in die Kommentar-Collection ein.

    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

    Commit 10-3

    Created a form to submit comments.

    Es passieren nicht wirklich komplizierte Dinge: wir prüfen, dass ein Benutzer angemeldet ist, dass der Kommentar einen body hat und dass der Kommentar auf einen existierenden Beitrag verweist.

    Controlling the Comments Subscription

    Momentan veröffentlichen wir alle Kommentare aller Beiträge an alle verbundenen Clients. Das erscheint etwas verschwenderisch. Denn eigentlich verwenden wir ja nur einen kleinen Anteil der Daten zu jedem gegebenen Zeitpunkt. Nachfolgend werden wir unsere Publication und Subscription anpassen, um genau zu kontrollieren welche Kommentare veröffentlicht werden.

    Wenn wir darüber nachdenken, dann ist der einzige Zeitpunkt an dem wir überhaupt auf Kommentare zugreifen müssen, der Moment in dem wir einen individuellen Beitrag anzeigen. Und wir müssen nur die Kommentare laden, die zu diesem Beitrag gehören.

    Im ersten Schritt werden wir die Art und Weise ändern, wie wir Kommentare abonnieren (subscribe). Bisher haben wir die Kommentare auf der Router-Ebene abonniert, was bedeutet, dass wir die gesamten Daten laden, wenn wir den Router initialisieren.

    Aber jetzt wollen wir, dass die Subscription abhängig von einem Pfad-Parameter ist. Dieser Parameter kann sich offensichtlich zu jedem Zeitpunkt ändern. Also müssen wir unsere Subscription von der Router-Ebene auf die Route-Ebene verschieben.

    Daraus ergibt, dass wir unsere Daten nicht mehr beim Initialisieren unserer App laden, sondern wenn die Route aufgerufen wird. Daraus resultieren Wartezeiten beim Browsen innerhalb unsere Anwendung. Was jedoch unvermeidlich ist, wenn wir nicht sämtliche Daten bereits beim Initialiesern unserer App laden wollen.

    So sieht der Code unserer neuen Route-Level-Funktion waitOn aus:

    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

    Dir ist sicher aufgefallen, dass wir this.params._id als Argument an die Subscription übergeben. Also lass uns nun diese Information benutzen um die Menge der Kommentare auf die einzuschränken, die zum aktuellen Beitrag gehören.

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

    Commit 10-4

    Made a simple publication/subscription for comments.

    Es gibt eigentlich nur ein Problem: wenn wir wieder auf die Homepage zurückkehren, behauptet unsere App, dass alle Beiträge null Kommentare haben:

    Our comments are gone!
    Our comments are gone!

    Counting Comments

    Der Grund dafür wird schnell klar: wir haben zu jedem Zeitpunkt die Kommentare von maximal einem unserer Beiträge geladen. Wenn wir also Comments.find({postId: this._id}) im Helper commentsCount im Manager post_item aufrufen, kann Meteor die notwendigen client-seitigen Daten nicht finden und damit auch nicht als Resultat zur Verfügung stellen.

    Der beste Weg damit umzugehen ist die Anzahl der Kommentare zu denormalisieren, also als Attribut des Beitrags zu speichern (keine Sorge, falls Du nicht sicher bist was damit gemeint ist, die nächste Sidebar hilft Dir weiter.) Auch wenn wir, wie wir gleich sehen werden, eine geringfügig höhere Komplexität in Kauf nehmen, so gewinnen wir doch erhebliche Performancevorteile, dadurch, dass wir nicht alle Kommentare veröffentlichen müssen um die Liste der Beiträge anzuzeigen.

    Wir erreichen dies in dem wir das Attribut commentsCount zur Datenstruktur post hinzufügen. Wir beginnen beim Aktualisieren des Fixture-Codes unseres Beitrags (und meteor reset um die Beiträge erneut zu laden – denk daran die Benutzerkennung wiederherzustellen):

    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

    Dann stellen wir sich, dass allen neuen Beiträge mit 0 Kommentaren erstellt werden:

    // pick out the whitelisted keys
    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

    Und dann aktualisieren wir den commentsCount wenn wir einen neuen Kommentar erstellen, indem wir Mongo’s $inc Operator (welcher ein numerisches Feld um den Wert eins erhöht) verwenden:

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

    Schlussendlich können wir den Helper commentsCount aus client/views/posts/post_item.js entfernen, da das Feld nun direkt am Beitrag verfügbar ist.

    Commit 10-5

    Denormalized the number of comments into the post.

    Nun, da die Benutzer miteinander sprechen können, wäre es eine Schande wenn sie neue Kommentare verpassen würden. Tja, jetzt kannst Du vielleicht den Inhalt des nächsten Kapitels erraten: wir werden Benachrichtigungen implementieren.