Einen großartigen Nest.js-Blog erstellen: Kommentare beantworten
Wenhao Wang
Dev Intern · Leapcell

Im vorherigen Artikel haben wir eine Kommentarfunktion zu unserem Blog hinzugefügt, die es den Lesern ermöglicht, ihre Gedanken zu Beiträgen zu teilen.
Diese Kommentare sind jedoch eindimensional. Andere können Ihre Beiträge kommentieren, aber Sie können ihre Kommentare nicht beantworten.
Um den Kommentarbereich interaktiver zu gestalten, werden wir in diesem Artikel eine Kommentar-Antwortfunktion implementieren: Benutzer können auf vorhandene Kommentare antworten, und diese Antworten werden in einem verschachtelten (oder eingerückten) Format angezeigt, um die Antworthierarchie zu zeigen.
Schritt 1: Datenmodell aktualisieren
Um die Antwortfunktion zu implementieren, müssen wir eine Eltern-Kind-Beziehung zwischen den Kommentaren herstellen. Eine Antwort ist im Wesentlichen ein Kommentar, der jedoch einen „Elternkommentar“ hat. Dies erreichen wir, indem wir der Comment
-Entität eine selbstreferenzierende Beziehung hinzufügen.
Datenbanktabelle ändern
Zuerst ändern wir die Struktur der comment
-Tabelle, um ein parentId
-Feld hinzuzufügen, das auf die ID seines Elternkommentars verweist.
Führen Sie die folgende ALTER TABLE
-Anweisung in Ihrer PostgreSQL-Datenbank aus:
ALTER TABLE "comment" ADD COLUMN "parentId" UUID REFERENCES "comment"("id") ON DELETE CASCADE;
- Die
parentId
-Spalte ist optional (erlaubtNULL
), da Top-Level-Kommentare keinen Elternteil haben. REFERENCES "comment"("id")
erstellt einen Fremdschlüssel, derparentId
mit derid
-Spalte derselben Tabelle verknüpft.
Wenn Ihre Datenbank auf Leapcell erstellt wird,
können Sie SQL-Anweisungen einfach über die grafische Oberfläche ausführen. Gehen Sie dazu auf der Website zur Seite „Datenbankverwaltung“, fügen Sie die obige Anweisung in die SQL-Oberfläche ein und führen Sie sie aus.
Comment-Entität aktualisieren
Öffnen Sie die Datei src/comments/comment.entity.ts
und fügen Sie die Eigenschaften parent
und replies
hinzu, um diese hierarchische Beziehung im Code widerzuspiegeln.
// src/comments/comment.entity.ts import { Entity, Column, PrimaryColumn, CreateDateColumn, ManyToOne, OneToMany } from 'typeorm'; import { User } from '../users/user.entity'; import { Post } from '../posts/post.entity'; @Entity() export class Comment { @PrimaryColumn({ type: 'uuid', default: () => 'gen_random_uuid()' }) id: string; @Column('text') content: string; @CreateDateColumn() createdAt: Date; @ManyToOne(() => User, (user) => user.comments) user: User; @ManyToOne(() => Post, (post) => post.comments) post: Post; // --- Neue Felder --- @ManyToOne(() => Comment, (comment) => comment.replies, { nullable: true }) parent: Comment; // Elternkommentar @OneToMany(() => Comment, (comment) => comment.parent) replies: Comment[]; // Liste der Kindkommentare (Antworten) }
Schritt 2: Comment-Service anpassen
Die Service-Schicht muss entsprechend angepasst werden, um beim Erstellen eines neuen Kommentars einen Elternkommentar zuzuordnen und beim Abfragen eine flache Liste von Kommentaren in eine baumähnliche Struktur zu organisieren.
Öffnen Sie src/comments/comments.service.ts
und nehmen Sie folgende Änderungen vor:
// src/comments/comments.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Comment } from './comment.entity'; import { Post } from '../posts/post.entity'; import { User } from '../users/user.entity'; @Injectable() export class CommentsService { constructor( @InjectRepository(Comment) private commentsRepository: Repository<Comment> ) {} // Ändere die Methode findByPostId async findByPostId(postId: string): Promise<Comment[]> { const comments = await this.commentsRepository.find({ where: { post: { id: postId } }, relations: ['user', 'parent'], // Lade Benutzer und Elternteil gleichzeitig order: { createdAt: 'ASC', }, }); return this.structureComments(comments); } // Füge eine neue private Methode hinzu, um die flache Liste in eine Baumstruktur zu konvertieren private structureComments(comments: Comment[]): Comment[] { const commentMap = new Map<string, Comment>(); comments.forEach((comment) => { comment.replies = []; // Initialisiere das replies-Array commentMap.set(comment.id, comment); }); const rootComments: Comment[] = []; comments.forEach((comment) => { if (comment.parent) { const parentComment = commentMap.get(comment.parent.id); if (parentComment) { parentComment.replies.push(comment); } } else { rootComments.push(comment); } }); return rootComments; } // Ändere die Methode create, um ein optionales parentId zu akzeptieren async create(content: string, user: User, post: Post, parentId?: string): Promise<Comment> { const newComment = this.commentsRepository.create({ content, user, post, parent: parentId ? ({ id: parentId } as Comment) : undefined, }); return (await this.commentsRepository.save([newComment]))[0]; } }
Logik-Erklärung:
findByPostId
ruft nun alle Kommentare für einen Beitrag ab (einschließlich Top-Level-Kommentare und aller Antworten).- Die neue Methode
structureComments
ist der Kern der Logik. Sie durchläuft alle Kommentare. Wenn ein Kommentar einparent
hat, wird er in dasreplies
-Array seines Elternkommentars eingefügt; andernfalls ist es ein Top-Level-Kommentar. - Die Methode
create
hat nun einen optionalen ParameterparentId
. Wenn diese ID angegeben wird, wird der neu erstellte Kommentar dem entsprechenden Elternkommentar zugeordnet.
Schritt 3: Controller aktualisieren
Der Controller muss die optionale parentId
aus dem Request-Body empfangen und an den Service übergeben. Diese Änderung ist sehr einfach.
Öffnen Sie src/comments/comments.controller.ts
:
// src/comments/comments.controller.ts import { Controller, Post, Body, Param, Req, Res, UseGuards } from '@nestjs/common'; // ... imports @Controller('posts/:postId/comments') export class CommentsController { constructor(private readonly commentsService: CommentsService) {} @UseGuards(AuthenticatedGuard) @Post() async create( @Param('postId') postId: string, @Body('content') content: string, @Body('parentId') parentId: string, // <-- parentId empfangen @Req() req: Request, @Res() res: Response ) { const { user } = req.session as any; // parentId an den Service übergeben await this.commentsService.create(content, user, { id: postId } as any, parentId); res.redirect(`/posts/${postId}`); } }
Schritt 4: Frontend-Ansicht aktualisieren
Dies ist der Teil mit den meisten Änderungen. Wir müssen die Vorlage post.ejs
aktualisieren, um Kommentare und ihre Antworten rekursiv zu rendern. Außerdem müssen wir etwas JavaScript hinzufügen, um das Antwortformular dynamisch anzuzeigen.
Kommentarvorlage erstellen
Um die rekursive Darstellung von Kommentaren zu erreichen, ist es am besten, eine wiederverwendbare „Partial“-Vorlage für einen Kommentar zu erstellen.
Erstellen Sie eine neue Datei _comment.ejs
im Verzeichnis views
:
<% comments.forEach(comment => { %> <div class="comment-item" style="margin-left: <%= depth * 20 %>px;"> <p class="comment-content"><%= comment.content %></p> <small> Von <strong><%= comment.user.username %></strong> am <%= new Date(comment.createdAt).toLocaleDateString() %> </small> <% if (user) { %> <button class="reply-btn" data-comment-id="<%= comment.id %>">Antworten</button> <% } %> </div> <% if (comment.replies && comment.replies.length > 0) { %> <%- include('_comment', { comments: comment.replies, user: user, post: post, depth: depth + 1 }) %> <% } %> <% }) %>
Diese Vorlage iteriert durch das übergebene comments
-Array und ruft sich für das replies
-Array jedes Kommentars rekursiv selbst auf, während sie die depth
erhöht, um eine stilistische Einrückung zu erzielen.
post.ejs
aktualisieren
Nun ändern wir views/post.ejs
, um diese neue _comment.ejs
-Partial zu verwenden, und fügen ein universelles Antwortformular hinzu.
<section class="comments-section"> <h3>Kommentare</h3> <div class="comment-list"> <% if (comments.length > 0) { %> <%- include('_comment', { comments: comments, user: user, post: post, depth: 0 }) %> <% } else { %> <p>Noch keine Kommentare. Sei der Erste, der kommentiert!</p> <% } %> </div> <% if (user) { %> <form id="comment-form" action="/posts/<%= post.id %>/comments" method="POST" class="comment-form"> <h4>Kommentar hinterlassen</h4> <div class="form-group"> <textarea name="content" rows="4" placeholder="Schreiben Sie hier Ihren Kommentar..." required></textarea> <input type="hidden" name="parentId" id="parentIdInput" value="" /> </div> <button type="submit">Absenden</button> <button type="button" id="cancel-reply-btn" style="display: none;">Antwort abbrechen</button> </form> <% } else { %> <p><a href="/auth/login">Anmelden</a>, um einen Kommentar zu hinterlassen.</p> <% } %> </section> <script> document.addEventListener('DOMContentLoaded', () => { const commentForm = document.getElementById('comment-form'); const parentIdInput = document.getElementById('parentIdInput'); const formTitle = commentForm.querySelector('h4'); const cancelReplyBtn = document.getElementById('cancel-reply-btn'); const commentList = document.querySelector('.comment-list'); commentList.addEventListener('click', (e) => { if (e.target.classList.contains('reply-btn')) { const commentId = e.target.getAttribute('data-comment-id'); const commentItem = e.target.closest('.comment-item'); // Verschiebe das Formular unter den zu beantwortenden Kommentar commentItem.after(commentForm); // Setze parentId und den Formular-Titel parentIdInput.value = commentId; formTitle.innerText = 'Antwort an ' + commentItem.querySelector('strong').innerText; cancelReplyBtn.style.display = 'inline-block'; } }); cancelReplyBtn.addEventListener('click', () => { // Setze den Formular-Status zurück parentIdInput.value = ''; formTitle.innerText = 'Kommentar hinterlassen'; cancelReplyBtn.style.display = 'none'; // Verschiebe das Formular zurück an das Ende des Kommentarbereichs document.querySelector('.comments-section').appendChild(commentForm); }); }); </script> <%- include('_footer') %>
Logik-Erklärung:
Auf der Seite gibt es nur ein einziges Kommentarformular.
Wenn ein Benutzer auf die Schaltfläche „Antworten“ eines Kommentars klickt, wird die ID dieses Kommentars abgerufen und im versteckten parentId
-Eingabefeld des Formulars gesetzt. Das gesamte Formular wird auch verschoben, um direkt unter dem zu beantwortenden Kommentar zu erscheinen.
Das Antwortformular zeigt eine Schaltfläche „Antwort abbrechen“. Das Klicken auf „Antwort abbrechen“ oder das Absenden des Formulars setzt den Formularzustand zurück und verschiebt es an seine ursprüngliche Position.
Ausführen und Testen
Starten Sie Ihre Anwendung neu:
npm run start:dev
Öffnen Sie Ihren Browser und navigieren Sie zu: http://localhost:3000/
Suchen Sie einen Kommentar zu einem Beitrag und klicken Sie auf die Schaltfläche „Antworten“. Sie sehen ein Antwortformular, das unter dem Kommentar erscheint.
Geben Sie Ihren Inhalt ein und senden Sie ihn ab. Nachdem die Seite neu geladen wurde, sehen Sie Ihre Antwort eingerückt unter dem Elternkommentar.
Sie können weiterhin auf Antworten antworten und so mehrere Gesprächsebenen erstellen.
Damit haben Sie jetzt ein vollständiges System zur Beantwortung von Kommentaren.
Frühere Tutorials: