Einen großartigen Nest.js-Blog erstellen: Bilder hochladen
Daniel Hayes
Full-Stack Engineer · Leapcell

Im vorherigen Tutorial haben wir eine Kommentar-Antwortfunktion für unseren Blog implementiert.
Nachdem der Kommentarbereich nun recht vollständig ist, wirken die Artikel selbst etwas schlicht – schließlich unterstützen sie nur einfachen Text.
In den kommenden Tutorials werden wir die Artikel durch Aktivierung der Bilder-Einfügung bereichern.
Das Prinzip hinter dem Einfügen eines Bildes ist folgendes:
- Der Benutzer wählt ein Bild aus und lädt es auf das Backend hoch.
- Das Backend speichert das Bild irgendwo und gibt eine URL zurück, um auf die Bildressource zuzugreifen.
- Das Frontend fügt die Bild-URL in den Artikelinhalt ein.
- Der Artikelinhalt wird schließlich als Webseite gerendert und das Bild wird angezeigt, indem die Daten von der entsprechenden Bild-URL abgerufen werden.
Schritt 1: S3-kompatiblen Objektspeicher vorbereiten
Zuerst benötigen wir einen Ort, um die hochgeladenen Bilder zu speichern. Das Speichern von Bildern direkt auf der Festplatte des Servers ist eine Methode, aber in modernen Anwendungen wird die Verwendung eines Objektspeicherdienstes (wie AWS S3) eher empfohlen, da er einfacher zu warten und kostengünstiger ist.
Zur Vereinfachung werden wir weiterhin Leapcell verwenden, das nicht nur eine Datenbank und Backend-Hosting bietet, sondern auch einen S3-kompatiblen Objektspeicherdienst anbietet.
Melden Sie sich in der Leapcell-Hauptoberfläche an und klicken Sie auf "Object Storage erstellen".
Sie können einen Objektspeicher erstellen, indem Sie einfach einen Namen eingeben.
Auf der Detailseite des Objektspeichers sehen Sie Verbindungsparameter wie Endpoint, Access Key ID und Secret Access Key. Diese werden wir später in unserer Backend-Konfiguration verwenden.
Die Benutzeroberfläche bietet auch eine sehr praktische UI zum direkten Hochladen und Verwalten von Dateien im Browser.
Schritt 2: Die Bild-Upload-API im Backend implementieren
Als Nächstes erstellen wir das Nest.js-Backend, das Datei-Uploads verarbeitet.
Abhängigkeiten installieren
Wir benötigen aws-sdk
(die neue Version ist @aws-sdk/client-s3
), um Dateien in einen S3-kompatiblen Objektspeicherdienst hochzuladen. Zusätzlich verwenden wir das eingebaute Multer von Nest.js, um multipart/form-data
-Anfragen zu verarbeiten.
npm install @aws-sdk/client-s3 npm install -D @types/multer
Uploads-Modul erstellen
Erstellen Sie ein neues Modul für die Funktionalität des Datei-Uploads.
nest generate module uploads nest generate controller uploads nest generate service uploads
uploads.service.ts
schreiben
Dieser Service ist für die Kernlogik der Kommunikation mit S3 zuständig.
// src/uploads/uploads.service.ts import { Injectable } from '@nestjs/common'; import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'; import crypto from 'crypto'; const S3_ENDPOINT = 'https://objstorage.leapcell.io'; const S3_ACCESS_KEY_ID = 'YOUR_ACCESS_KEY_ID'; const S3_SECRET_ACCESS_KEY = 'YOUR_SECRET_ACCESS_KEY'; const S3_BUCKET_NAME = 'my-nest-blog-images'; const S3_PUBLIC_URL = 'https://example.leapcellobj.com/my-nest-blog-images'; @Injectable() export class UploadsService { private readonly s3: S3Client; constructor() { this.s3 = new S3Client({ endpoint: S3_ENDPOINT, region: 'us-east-1', // Bei S3-kompatiblem Speicher ist die Region oft eine Formalität credentials: { accessKeyId: S3_ACCESS_KEY_ID, secretAccessKey: S3_SECRET_ACCESS_KEY, }, }); } async uploadFile(file: Express.Multer.File): Promise<string> { const uniqueFileName = `${crypto.randomUUID()}-${file.originalname}`; const command = new PutObjectCommand({ Bucket: S3_BUCKET_NAME, Key: uniqueFileName, Body: file.buffer, ContentType: file.mimetype, ACL: 'public-read', // Datei als öffentlich lesbar markieren }); try { await this.s3.send(command); // Die öffentliche URL der Datei zurückgeben return `${S3_PUBLIC_URL}/${uniqueFileName}`; } catch (error) { console.error('Fehler beim Hochladen nach S3:', error); throw new Error('Datei-Upload fehlgeschlagen.'); } } }
Zur Vereinfachung der Implementierung sind die S3-Verbindungsparameter direkt im Code geschrieben.
In einem realen Projekt wird empfohlen, sie aus Umgebungsvariablen zu lesen. Sie können das @nestjs/config
-Modul verwenden, um dies zu erreichen.
uploads.controller.ts
schreiben
Dieser Controller ist für die Definition der API-Route zuständig und verwendet FileInterceptor
, um die hochgeladene Datei zu empfangen.
// src/uploads/uploads.controller.ts import { Controller, Post, UploadedFile, UseGuards, UseInterceptors } from '@nestjs/common'; import { FileInterceptor } from '@nestjs/platform-express'; import { UploadsService } from './uploads.service'; import { AuthenticatedGuard } from '../auth/authenticated.guard'; @Controller('uploads') export class UploadsController { constructor(private readonly uploadsService: UploadsService) {} @UseGuards(AuthenticatedGuard) // Nur angemeldete Benutzer können hochladen @Post('image') @UseInterceptors(FileInterceptor('file')) // 'file' ist der Name des Datei-Feldes im Formular async uploadImage(@UploadedFile() file: Express.Multer.File) { const url = await this.uploadsService.uploadFile(file); return { url }; } }
Schließlich importieren Sie UploadsModule
in app.module.ts
, damit die Anwendung dieses neue Modul erkennen kann.
Schritt 3: Frontend-Integration mit der FilePicker-API
Nachdem das Backend bereit ist, modifizieren wir die Frontend-Seite new-post.ejs
, um die Upload-Funktionalität hinzuzufügen.
Es gibt zwei Möglichkeiten, Uploads zu verarbeiten: die neueste FilePicker-API und das traditionelle <input type="file">
.
Traditionelle Methode: <input type="file">
hat eine ausgezeichnete Kompatibilität und wird von allen Browsern unterstützt. Seine API ist jedoch etwas veraltet, unintuitiv und bietet eine schlechte Benutzererfahrung.
Moderne Methode: Die File System Access API ist einfacher zu bedienen, leistungsfähiger und kann eine bessere Benutzererfahrung bieten. Ihre Kompatibilität ist jedoch nicht so gut wie die traditionelle Methode und sie muss in einem sicheren Kontext (HTTPS) ausgeführt werden.
Da unser Blog ein modernes Projekt ist, verwenden wir die FilePicker-API zur Implementierung von Datei-Uploads.
new-post.ejs
aktualisieren
Fügen Sie in views/new-post.ejs
neben dem <textarea>
einen Button "Bild hochladen" hinzu.
<%- include('_header', { title: 'New Post' }) %> <form action="/posts" method="POST" class="post-form"> <div class="form-group"> <label for="title">Title</label> <input type="text" id="title" name="title" required /> </div> <div class="form-group"> <label for="content">Content</label> <div class="toolbar"> <button type="button" id="upload-image-btn">Bild hochladen</button> </div> <textarea id="content" name="content" rows="10" required></textarea> </div> <button type="submit">Absenden</button> </form> <script> document.addEventListener('DOMContentLoaded', () => { const uploadBtn = document.getElementById('upload-image-btn'); const contentTextarea = document.getElementById('content'); uploadBtn.addEventListener('click', async () => { try { const [fileHandle] = await window.showOpenFilePicker({ types: [ { description: 'Bilder', accept: { 'image/*': ['.png', '.jpeg', '.jpg', '.gif', '.webp'] }, }, ], }); const file = await fileHandle.getFile(); uploadFile(file); } catch (error) { // Ein AbortError wird ausgelöst, wenn der Benutzer die Dateiauswahl abbricht, wir ignorieren ihn if (error.name !== 'AbortError') { console.error('FilePicker Error:', error); } } }); function uploadFile(file) { if (!file) return; const formData = new FormData(); formData.append('file', file); // Einfache Ladeanzeige einblenden uploadBtn.disabled = true; uploadBtn.innerText = 'Hochladen...'; fetch('/uploads/image', { method: 'POST', body: formData, // Hinweis: Setzen Sie den Content-Type Header nicht manuell, wenn Sie FormData verwenden }) .then((response) => response.json()) .then((data) => { if (data.url) { // Die zurückgegebene Bild-URL in die Textarea einfügen const markdownImage = ``; insertAtCursor(contentTextarea, markdownImage); } else { alert('Upload fehlgeschlagen. Bitte versuchen Sie es erneut.'); } }) .catch((error) => { console.error('Upload Error:', error); alert('Ein Fehler ist während des Uploads aufgetreten.'); }) .finally(() => { uploadBtn.disabled = false; uploadBtn.innerText = 'Bild hochladen'; }); } // Hilfsfunktion zum Einfügen von Text an der Cursorposition function insertAtCursor(myField, myValue) { if (myField.selectionStart || myField.selectionStart === 0) { var startPos = myField.selectionStart; var endPos = myField.selectionEnd; myField.value = myField.value.substring(0, startPos) + myValue + myField.value.substring(endPos, myField.value.length); myField.selectionStart = startPos + myValue.length; myField.selectionEnd = startPos + myValue.length; } else { myField.value += myValue; } } }); </script> <%- include('_footer') %>
Schritt 4: Artikel mit Bildern rendern
Wir haben erfolgreich den Markdown-Link des Bildes in den Artikel eingefügt, aber er wird immer noch als Textzeichenkette gerendert. Das liegt daran, dass wir das Markdown-Format auf der Detailseite des Artikels, post.ejs
, nach HTML konvertieren müssen.
Eine Markdown-Parsing-Bibliothek installieren
Wir verwenden die Bibliothek marked
, um das Parsen auf dem Backend durchzuführen.
npm install marked
Markdown im Controller parsen
Modifizieren Sie die findOne
-Methode in src/posts/posts.controller.ts
. Bevor Sie die Post-Daten an die Vorlage übergeben, parsen Sie deren Inhalt mit marked
.
// src/posts/posts.controller.ts import { Controller, Get, Render, Param, Request } from '@nestjs/common'; import { PostsService } from './posts.service'; import { CommentsService } from '../comments/comments.service'; import { marked } from 'marked'; // marked importieren @Controller('posts') export class PostsController { constructor(private readonly postsService: PostsService, private readonly commentsService: CommentsService) {} // ... andere Methoden @Get(':id') @Render('post') async post(@Param('id') id: string, @Request() req) { const post = await this.postsService.findOne(id); const comments = await this.commentsService.findByPostId(id); // Markdown-Inhalt parsen if (post) { post.content = marked.parse(post.content) as string; } return { post, user: req.session.user, comments }; } }
Die post.ejs
-Ansicht aktualisieren
Schließlich modifizieren Sie views/post.ejs
, um sicherzustellen, dass es den geparsten HTML-Code korrekt rendert.
Zuvor haben wir <%- post.content.replace(/\n/g, '<br />') %>
verwendet, um Zeilenumbrüche zu behandeln. Da der Inhalt jetzt bereits HTML ist, können wir ihn direkt ausgeben.
<%- include('_header', { title: post.title }) %> <article class="post-detail"> <h1><%= post.title %></h1> <small><%= new Date(post.createdAt).toLocaleDateString() %></small> <div class="post-content"><%- post.content %></div> </article> <a href="/" class="back-link">← Zurück zur Startseite</a> <%- include('_footer') %>
Beachten Sie, dass wir <%-
anstelle von <%=
verwenden. Ersteres gibt das HTML direkt aus, während Letzteres es maskieren würde.
Ausführen und Testen
Starten Sie Ihre Anwendung neu:
npm run start:dev
Öffnen Sie Ihren Browser und gehen Sie zu: http://localhost:3000/
Navigieren Sie zur Seite "Neuer Beitrag" und Sie sehen den Button "Bild hochladen". Wenn Sie darauf klicken, können Sie eine Datei zum Hochladen auswählen.
Wählen Sie ein Bild aus. Nach Abschluss des Uploads wird der Markdown-Link für das Bild automatisch in das Textfeld eingefügt.
Veröffentlichen Sie den Artikel und gehen Sie zur Detailseite des Artikels. Sie werden sehen, dass das Bild erfolgreich gerendert wird. Und als zusätzlichen Bonus unterstützt der Artikelinhalt jetzt Markdown-Syntax!
Herzlichen Glückwunsch, Ihr Blog unterstützt jetzt Bild-Uploads (und Markdown)! Von nun an werden Ihre Blogbeiträge sicher viel aufregender sein.
Frühere Tutorials:
- Einen großartigen Nest.js-Blog erstellen: Kommentare beantworten
- Einen großartigen Nest.js-Blog erstellen: Kommentarsystem
- Einen großartigen Nest.js-Blog erstellen: Autorisierung hinzufügen
- Einen großartigen Nest.js-Blog erstellen: Benutzerverwaltung hinzufügen
- 10 Minuten vom ersten Code bis zum Live-Deployment: Ein super-schneller Nest.js Blog-Kurs