Rustango docs
← Anleitungen

HTML-Views — serverseitig gerenderte Seiten

Ein HTML-View verwandelt ein Modell in serverseitig gerenderte Webseiten — eine Listenseite, eine Detailseite sowie Erstellungs-/Bearbeitungs-/Löschformulare — aus einer einzigen Deklaration. Es ist das Gegenstück zu ViewSets: Während ein ViewSet JSON für API-Clients ausgibt, gibt ein HTML-View eine gerenderte Seite für einen Browser aus. Beide werden aus demselben #[derive(Model)] gebaut, und du kannst ein Modell beides zugleich ausliefern.

Dies sind das Äquivalent von Rustango zu Djangos generischen klassenbasierten Views (ListView, DetailView, CreateView, UpdateView, DeleteView) oder Laravels Resource-Controllern, die Blade-Views zurückgeben. Sie rendern über Tera-Templates.

HTML-Views in Rustango: Ein Modell speist ListView, DetailView und CreateView/UpdateView/DeleteView, die jeweils ein Tera-Template zu einer serverseitig gerenderten Seite rendern

Neu bei einem Begriff hier? Falls Modell, Template, Router oder serverseitig gerendert unbekannt sind, erklärt das Glossar jeden in einfacher Sprache.

Quelle: rustango::template_views (ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView, RedirectView) — hinter dem template_views-Feature (standardmäßig aktiviert).

Lauffähige Version: Das API-vs-HTML-Beispiel unten ist durch den Framework-Test html_and_api_contrast_sqlite_live.rs festgeschrieben (cargo test -p rustango --features sqlite --test html_and_api_contrast_sqlite_live). Die einzelnen Views werden von template_view.rs und template_views_context_object_name_sqlite_live.rs abgedeckt.

Inhaltsverzeichnis


API-Views vs HTML-Views — welche willst du?

Das ist die erste Entscheidung. Beide verwandeln ein Modell in Endpunkte; sie unterscheiden sich darin, was herauskommt und wer aufruft.

API-ViewViewSetHTML-View — dieser Leitfaden
Modulrustango::viewsetrustango::template_views
Gibt zurückJSON-Dateneine serverseitig gerenderte HTML-Seite
Gebaut fürSPAs, Mobile-Apps, andere ServicesBrowser, serverseitig gerenderte Sites, CRUD im Admin-Stil
Ein „Erstellen"POST JSON → 201 + das neue ObjektPOST eines Formulars → 303-Redirect zu einer Erfolgsseite
Bei fehlerhafter Eingabe400 + eine feldbasierte JSON-Fehlerkarterendert das Formular mit den angezeigten Fehlern neu
Liest eine Liste alseine paginierte JSON-Hülleeine <table>/Schleife in deinem Template
Üblicherweise authentifiziert perTokens / JWT / API-KeysSession-Cookies
Django-AnalogonDRF ModelViewSetgenerische klassenbasierte Views

Du musst nicht global wählen — wähle pro Ressource, und du kannst beide auf demselben Modell einhängen (siehe unten). Faustregeln:

  • Du baust ein JSON-Backend für ein Frontend-Framework oder eine Mobile-App → ViewSet.
  • Du baust eine serverseitig gerenderte Site (der Server gibt HTML-Seiten zurück) → HTML-Views.
  • Du brauchst beides (eine öffentliche API und interne CRUD-Seiten) → hänge beide ein.

Suchst du die JSON-Seite? Sie hat ihre eigene Vertiefung: ViewSets — CRUD-REST-APIs.


Die fünf Modell-Views

Jeder View ist for_model(SCHEMA) plus ein .router(prefix, tera, pool). Sie am selben prefix (sagen wir /posts) einzuhängen, ergibt den klassischen CRUD-URL-Satz:

ViewRendertEingehängte RoutenStandard-Template
ListVieweine paginierte ListeGET <prefix><table>_list.html
DetailVieweine ZeileGET <prefix>/{pk}<table>_detail.html
CreateViewein Formular für einen neuen DatensatzGET/POST <prefix>/new<table>_form.html
UpdateViewein vorausgefülltes BearbeitungsformularGET/POST <prefix>/{pk}/edit<table>_form.html
DeleteVieweine BestätigungsseiteGET/POST <prefix>/{pk}/delete<table>_confirm_delete.html

<table> ist der Tabellenname des Modells, ein Post (Tabelle posts) sucht also nach posts_list.html, posts_detail.html und so weiter. Überschreibe jedes davon mit .template("my_name.html").


ListView

Eine paginierte Listenseite. Du stellst ein Template bereit, das über object_list iteriert; der View übernimmt Paginierung, Sortierung, Filterung und Suche aus den Query-Parametern.

use rustango::template_views::ListView;
use std::sync::Arc;
use tera::Tera;

let app = ListView::for_model(Post::SCHEMA)
    .page_size(20)                       // rows per page (?page=N to navigate)
    .order_by("published_at", true)      // default sort, true = DESC
    .filter_fields(&["status", "author_id"])  // ?status=published
    .search_fields(&["title", "body"])        // ?search=rust
    .router("/posts", Arc::new(tera), pool);

Ein passendes posts_list.html — beachte object_list und die Paginierungsvariablen, die der View für dich einstempelt:

<h1>Posts ({{ total }})</h1>
{% for post in object_list %}
  <article>
    <h2><a href="/posts/{{ post.id }}">{{ post.title }}</a></h2>
    <p>{{ post.body }}</p>
  </article>
{% endfor %}

{% if has_prev %}<a href="?page={{ page - 1 }}">← prev</a>{% endif %}
page {{ page }} / {{ total_pages }}
{% if has_next %}<a href="?page={{ page + 1 }}">next →</a>{% endif %}

?page=, ?status=, ?search= und ?ordering= funktionieren genauso wie bei einer ViewSet-Liste — der Unterschied liegt allein darin, dass das Ergebnis eine gerenderte Seite statt einer JSON-Hülle ist. Verwende .context_object_name("posts"), falls du lieber über posts als über object_list im Template iterierst.


DetailView

Eine Zeile, anhand der URL nachgeschlagen. Standardmäßig passt sie zum Primärschlüssel (/posts/42); richte sie mit .lookup_field("slug") auf eine andere Spalte aus, um schöne URLs zu erhalten (/posts/my-first-post). Eine fehlende Zeile ist ein 404.

use rustango::template_views::DetailView;

let app = DetailView::for_model(Post::SCHEMA)
    .lookup_field("slug")          // GET /posts/{slug} instead of /posts/{id}
    .router("/posts", Arc::new(tera), pool);

Das Template erhält die Zeile als object:

<h1>{{ object.title }}</h1>
<p>{{ object.body }}</p>
<small>by author #{{ object.author_id }}</small>

CreateView, UpdateView, DeleteView

Die Schreibseite. Jeder verarbeitet ein GET (ein Formular / eine Bestätigungsseite rendern) und ein POST (die Arbeit erledigen, dann weiterleiten). Die Weiterleitung-nach-POST ist das Standardmuster Post/Redirect/Get — es verhindert, dass ein Browser-Refresh erneut absendet.

CreateViewGET /posts/new rendert ein leeres Formular; POST /posts/new fügt die Zeile ein und 303t zu success_url:

use rustango::template_views::CreateView;

let app = CreateView::for_model(Post::SCHEMA)
    .success_url("/posts")         // where to send the browser after a save
    .router("/posts", Arc::new(tera), pool);

Das Formular-Template (posts_form.html) wird mit UpdateView geteilt. is_update unterscheidet die beiden, und errors trägt etwaige Validierungsmeldungen zurück:

<form method="post">
  <input name="title" value="{{ object.title | default(value='') }}">
  <textarea name="body">{{ object.body | default(value='') }}</textarea>
  {% for field, msgs in errors %}
    <p class="error">{{ field }}: {{ msgs | join(sep=', ') }}</p>
  {% endfor %}
  <button>{% if is_update %}Save{% else %}Create{% endif %}</button>
</form>

Validierung. Schema-Regeln (Typ, max_length, NOT NULL…) werden automatisch erzwungen. Füge mit einem Closure-Validator eigene hinzu — bei Err wird das Formular mit den Meldungen und einem 422-Status neu gerendert statt gespeichert:

use rustango::forms::FormErrors;

CreateView::for_model(Post::SCHEMA)
    .validator(|data| {
        let mut errs = FormErrors::default();
        if data.get("title").map_or(true, |t| t.len() < 5) {
            errs.add("title", "must be at least 5 characters");
        }
        if errs.is_empty() { Ok(()) } else { Err(errs) }
    })
    .success_url("/posts")
    .router("/posts", Arc::new(tera), pool);

Du kannst auch die Validatoren einer #[derive(Form)]-Struktur mit .form::<F>() wiederverwenden (vorerst nur Validierung — siehe die API-Dokumentation).

UpdateViewGET /posts/{pk}/edit rendert dasselbe Formular, vorausgefüllt aus der Zeile (object ist befüllt, is_update ist true); POST aktualisiert und 303t.

use rustango::template_views::UpdateView;

UpdateView::for_model(Post::SCHEMA)
    .success_url("/posts")
    .router("/posts", Arc::new(tera), pool);

DeleteViewGET /posts/{pk}/delete rendert eine Bestätigungsseite (posts_confirm_delete.html, mit object); POST löscht und 303t.

use rustango::template_views::DeleteView;

DeleteView::for_model(Post::SCHEMA)
    .success_url("/posts")
    .router("/posts", Arc::new(tera), pool);

Hänge alle fünf am selben Präfix ein und du hast vollständiges HTML-CRUD:

let app = axum::Router::new()
    .merge(ListView::for_model(Post::SCHEMA).router("/posts", tera.clone(), pool.clone()))
    .merge(DetailView::for_model(Post::SCHEMA).router("/posts", tera.clone(), pool.clone()))
    .merge(CreateView::for_model(Post::SCHEMA).success_url("/posts").router("/posts", tera.clone(), pool.clone()))
    .merge(UpdateView::for_model(Post::SCHEMA).success_url("/posts").router("/posts", tera.clone(), pool.clone()))
    .merge(DeleteView::for_model(Post::SCHEMA).success_url("/posts").router("/posts", tera, pool));

Der Tera-Kontext

Jeder View stempelt einen konsistenten Kontext ein, damit Templates sauber zwischen ihnen portieren:

ViewIm Template verfügbare Variablen
ListViewobject_list (die Zeilen der Seite), page, page_size, total, total_pages, has_next, has_prev
DetailViewobject (die Zeile)
CreateView / UpdateViewobject (leer beim Erstellen, vorausgefüllt beim Aktualisieren), is_update (bool), errors, values
DeleteViewobject (die zu bestätigende Zeile)

Zeilen werden als schlichte, nach Spaltennamen indizierte Maps bereitgestellt ({{ post.title }}), wobei SQL-NULL als null gerendert wird. Verwende .context_object_name("posts" / "post"), um neben object_list / object einen freundlicheren Alias hinzuzufügen.


TemplateView und RedirectView

Zwei modellfreie Helfer für die Seiten, die jede Site hat:

TemplateView — rendert ein statisches Template mit einem festen Kontext (eine „Über uns"-Seite, eine Landingpage). Kein Modell, keine Datenbank:

use rustango::template_views::TemplateView;

let app = TemplateView::new("about.html")
    .context_value("title", "About us")
    .router("/about", Arc::new(tera));

RedirectView — eine permanente oder temporäre Weiterleitung an einer URL (für verschobene Seiten):

use rustango::template_views::RedirectView;

let app = RedirectView::to("/posts").router("/old-posts");

Single-Tenant vs Multi-Tenant

Jeder Modell-View bringt zwei Router-Konstruktoren mit — derselbe Builder, wähle den, der dazu passt, wie deine App Datenbankverbindungen verwaltet:

  • .router(prefix, tera, pool) — Single-Tenant; erfasst zur Einhängezeit einen Pool. Das nutzen die Beispiele oben.
  • .tenant_router(prefix, tera) — Multi-Tenant; löst eine Verbindung pro Request aus dem Tenant-Extractor auf. Verfügbar mit den Features template_views + tenancy. Templates portieren unverändert zwischen beiden.

Das spiegelt die ViewSet-Aufteilung (router / router_pool vs tenant_router).


Ein Modell auf beide Arten ausliefern

Du bist nicht auf eine Eingangstür beschränkt. Hänge eine JSON-API und HTML-Seiten über dasselbe Modell und denselben Pool ein — eine öffentliche API für Clients, serverseitig gerenderte Seiten für Menschen:

use rustango::viewset::ViewSet;
use rustango::template_views::{ListView, DetailView};

let app = axum::Router::new()
    // JSON for API clients:
    .merge(ViewSet::for_model(Post::SCHEMA).router_pool("/api/posts", pool.clone()))
    // HTML pages for browsers:
    .merge(ListView::for_model(Post::SCHEMA).router("/posts", tera.clone(), pool.clone()))
    .merge(DetailView::for_model(Post::SCHEMA).router("/posts", tera, pool));

Jetzt gibt GET /api/posts die paginierte JSON-Hülle zurück und GET /posts gibt eine gerenderte HTML-Liste zurück — dieselben Zeilen, derselbe Pool, zwei Formen. Genau dieses Setup ist es, was der zugrunde liegende Test behauptet.


Siehe auch