Router

Was ist Routing?

Router definiert, welche Middleware und welcher Handler eine HTTP-Anfrage verarbeitet. Dies ist die grundlegendste und zentrale Funktionalität in Salvo.

Intern besteht ein Router im Wesentlichen aus einer Reihe von Filtern. Wenn eine Anfrage eintrifft, testet der Router sich selbst und seine Nachfolger der Reihe nach von oben nach unten, um zu prüfen, ob sie die Anfrage abgleichen können. Wenn eine Übereinstimmung erfolgreich ist, wird die Middleware auf der gesamten Kette, die durch den Router und seine Nachfolger-Router gebildet wird, sequenziell ausgeführt. Wenn während der Verarbeitung der Response-Status auf einen Fehler (4XX, 5XX) oder eine Umleitung (3XX) gesetzt wird, werden nachfolgende Middleware und Handler übersprungen. Sie können auch manuell ctrl.skip_rest() aufrufen, um die verbleibende Middleware und den Handler zu überspringen.

Während des Abgleichvorgangs existiert ein URL-Pfadinformationsobjekt, das als ein Objekt betrachtet werden kann, das während des Abgleichs vollständig von Filtern verbraucht werden muss. Wenn alle Filter in einem bestimmten Router erfolgreich übereinstimmen und diese URL-Pfadinformationen vollständig verbraucht wurden, gilt dies als "erfolgreicher Abgleich".

Beispiel:

Router::with_path("articles").get(list_articles).post(create_article);

Ist tatsächlich äquivalent zu:

Router::new()
    // PathFilter kann Anfragepfade filtern. Es stimmt nur erfolgreich überein, wenn der Anfragepfad das Segment "articles" enthält.
    // Andernfalls schlägt der Abgleich fehl. Zum Beispiel: /articles/123 stimmt erfolgreich überein, während /articles_list/123
    // "articles" enthält, aber wegen des nachgestellten "_list" nicht übereinstimmt.
    .filter(PathFilter::new("articles"))

    // Wenn die Wurzel erfolgreich übereinstimmt und die Anfragemethode GET ist, kann der innere Kindrouter erfolgreich übereinstimmen,
    // und die Anfrage wird von list_articles verarbeitet.
    .push(Router::new().filter(filters::get()).handle(list_articles))

    // Wenn die Wurzel erfolgreich übereinstimmt und die Anfragemethode POST ist, kann der innere Kindrouter erfolgreich übereinstimmen,
    // und die Anfrage wird von create_article verarbeitet.
    .push(Router::new().filter(filters::post()).handle(create_article));

Wenn auf GET /articles/ zugegriffen wird, gilt dies als erfolgreicher Abgleich und list_articles wird ausgeführt. Wenn jedoch auf GET /articles/123 zugegriffen wird, schlägt der Routenabgleich fehl und gibt einen 404-Fehler zurück, weil Router::with_path("articles") nur den /articles-Teil der URL-Pfadinformationen verbraucht, den /123-Teil jedoch unverbraucht lässt, daher gilt der Abgleich als fehlgeschlagen. Um einen erfolgreichen Abgleich zu erreichen, kann die Route geändert werden zu:

Router::with_path("articles/{**}").get(list_articles).post(create_article);

Hier gleicht {**} jeden verbleibenden Pfad ab, sodass GET /articles/123 übereinstimmen und list_articles ausgeführt werden kann.

Flache Definition

Wir können Routen in einem flachen Stil definieren:

Router::with_path("writers").get(list_writers).post(create_writer);
Router::with_path("writers/{id}").get(show_writer).patch(edit_writer).delete(delete_writer);
Router::with_path("writers/{id}/articles").get(list_writer_articles);

Baumartige Definition

Wir können Routen auch in einer baumartigen Struktur definieren, was der empfohlene Ansatz ist:

Router::with_path("writers")
    .get(list_writers)
    .post(create_writer)
    .push(
        Router::with_path("{id}")
            .get(show_writer)
            .patch(edit_writer)
            .delete(delete_writer)
            .push(Router::with_path("articles").get(list_writer_articles)),
    );

Diese Form der Definition macht Router-Definitionen hierarchisch, klar und einfach für komplexe Projekte.

Viele Methoden in Router geben nach dem Aufruf Self zurück, was das Schreiben von verkettetem Code erleichtert. Manchmal müssen Sie basierend auf bestimmten Bedingungen entscheiden, wie geroutet werden soll. Das Routing-System bietet auch die then-Funktion, die einfach zu verwenden ist:

Router::new()
    .push(
        Router::with_path("articles")
            .get(list_articles)
            .push(Router::with_path("{id}").get(show_article))
            .then(|router|{
                if admin_mode() {
                    router.post(create_article).push(
                        Router::with_path("{id}").patch(update_article).delete(delete_writer)
                    )
                } else {
                    router
                }
            }),
    );

Dieses Beispiel bedeutet, dass Routen zum Erstellen, Bearbeiten und Löschen von Artikeln nur hinzugefügt werden, wenn der Server im admin_mode ist.

Abrufen von Parametern aus Routen

Im obigen Code definiert {id} einen Parameter. Wir können seinen Wert über die Request-Instanz abrufen:

#[handler]
async fn show_writer(req: &mut Request) {
    let id = req.param::<i64>("id").unwrap();
}

{id} stimmt mit einem Segment im Pfad überein. Normalerweise ist die id eines Artikels nur eine Zahl. In diesem Fall können wir einen regulären Ausdruck verwenden, um die Abgleichsregel für id einzuschränken, wie r"{id|\d+}".

Für numerische Typen gibt es eine noch einfachere Methode mit <id:num>. Spezifische Notationen sind:

  • {id:num} stimmt mit beliebig vielen Ziffernzeichen überein.
  • {id:num[10]} stimmt genau mit einer bestimmten Anzahl von Ziffernzeichen überein; hier bedeutet 10, dass genau 10 Ziffern übereinstimmen.
  • {id:num(..10)} stimmt mit 1 bis 9 Ziffernzeichen überein.
  • {id:num(3..10)} stimmt mit 3 bis 9 Ziffernzeichen überein.
  • {id:num(..=10)} stimmt mit 1 bis 10 Ziffernzeichen überein.
  • {id:num(3..=10)} stimmt mit 3 bis 10 Ziffernzeichen überein.
  • {id:num(10..)} stimmt mit mindestens 10 Ziffernzeichen überein.

Sie können auch alle verbleibenden Pfadsegmente mit {**}, {*+} oder {*?} abgleichen. Für eine bessere Code-Lesbarkeit können Sie geeignete Namen hinzufügen, um die Pfadsemantik klarer zu machen, z.B. {**file_path}.

  • {**}: Stellt einen Wildcard-Abgleich dar, bei dem der übereinstimmende Teil eine leere Zeichenkette sein kann. Zum Beispiel stimmt der Pfad /files/{**rest_path} mit /files, /files/abc.txt, /files/dir/abc.txt überein.
  • {*+}: Stellt einen Wildcard-Abgleich dar, bei dem der übereinstimmende Teil existieren muss und keine leere Zeichenkette sein kann. Zum Beispiel stimmt der Pfad /files/{*+rest_path} NICHT mit /files überein, aber mit /files/abc.txt, /files/dir/abc.txt.
  • {*?}: Stellt einen Wildcard-Abgleich dar, bei dem der übereinstimmende Teil eine leere Zeichenkette sein kann, aber nur ein Pfadsegment enthalten darf. Zum Beispiel stimmt der Pfad /files/{*?rest_path} NICHT mit /files/dir/abc.txt überein, aber mit /files, /files/abc.txt.

Mehrere Ausdrücke können kombiniert werden, um dasselbe Pfadsegment abzugleichen, z.B. /articles/article_{id:num}/, /images/{name}.{ext}.

Hinzufügen von Middleware

Middleware kann über die hoop-Funktion auf einem Router hinzugefügt werden:

Router::new()
    .hoop(check_authed)
    .path("writers")
    .get(list_writers)
    .post(create_writer)
    .push(
        Router::with_path("{id}")
            .get(show_writer)
            .patch(edit_writer)
            .delete(delete_writer)
            .push(Router::with_path("articles").get(list_writer_articles)),
    );

In diesem Beispiel verwendet der Wurzelrouter check_authed, um zu überprüfen, ob der aktuelle Benutzer angemeldet ist. Alle Nachfolger-Router sind von dieser Middleware betroffen.

Wenn Benutzer nur writer-Informationen und Artikel durchsuchen, bevorzugen wir möglicherweise, dass sie ohne Anmeldung browsen können. Wir können die Routen wie folgt definieren:

Router::new()
    .push(
        Router::new()
            .hoop(check_authed)
            .path("writers")
            .post(create_writer)
            .push(Router::with_path("{id}").patch(edit_writer).delete(delete_writer)),
    )
    .push(
        Router::with_path("writers").get(list_writers).push(
            Router::with_path("{id}")
                .get(show_writer)
                .push(Router::with_path("articles").get(list_writer_articles)),
        ),
    );

Obwohl zwei Router dieselbe Pfaddefinition path("articles") haben, können sie trotzdem zum selben Elternrouter hinzugefügt werden.

Filter

Router verwendet intern Filter, um zu bestimmen, ob eine Route übereinstimmt. Filter unterstützen grundlegende logische Operationen mit or oder and. Ein Router kann mehrere Filter enthalten; wenn alle Filter erfolgreich übereinstimmen, stimmt die Route erfolgreich überein.

Website-Pfadinformationen sind eine Baumstruktur, aber diese Baumstruktur ist nicht äquivalent zur Baumstruktur, die Router organisiert. Ein Website-Pfad kann mehreren Router-Knoten entsprechen. Zum Beispiel erfordern einige Inhalte unter dem Pfad articles/ möglicherweise eine Anmeldung zum Anzeigen, während andere dies nicht tun. Wir können Teilpfade, die eine Anmeldeüberprüfung erfordern, unter einem Router mit Anmeldeüberprüfungs-Middleware organisieren und solche, die keine Überprüfung erfordern, unter einem anderen Router ohne sie:

Router::new()
    .push(
        Router::with_path("articles")
            .get(list_articles)
            .push(Router::new().path("{id}").get(show_article)),
    )
    .push(
        Router::with_path("articles")
            .hoop(auth_check)
            .post(list_articles)
            .push(Router::new().path("{id}").patch(edit_article).delete(delete_article)),
    );

Router verwenden Filter, um Anfragen zu filtern und sie an die entsprechende Middleware und Handler zur Verarbeitung zu senden.

path und method sind zwei der am häufigsten verwendeten Filter. path wird verwendet, um Pfadinformationen abzugleichen; method wird verwendet, um die Methode der Anfrage abzugleichen, z.B. GET, POST, PATCH usw.

Wir können Router-Filter mit and, or verbinden:

Router::with_filter(filters::path("hello").and(filters::get()));

Pfadfilter

Filter basierend auf Anfragepfaden sind die am häufigsten verwendeten. Pfadfilter können Parameter definieren, zum Beispiel:

Router::with_path("articles/{id}").get(show_article);
Router::with_path("files/{**rest_path}").get(serve_file)

In einem Handler können sie über die get_param-Funktion des Request-Objekts abgerufen werden:

#[handler]
pub async fn show_article(req: &mut Request) {
    let article_id = req.param::<i64>("id");
}

#[handler]
pub async fn serve_file(req: &mut Request) {
    let rest_path = req.param::<i64>("rest_path");
}

Methodenfilter

Filtert Anfragen basierend auf der Method der HTTP-Anfrage, zum Beispiel:

Router::new().get(show_article).patch(update_article).delete(delete_article);

Hier sind get, patch, delete alles Methodenfilter. Dies ist tatsächlich äquivalent zu:

use salvo::routing::filter;

let mut root_router = Router::new();
let show_router = Router::with_filter(filters::get()).handle(show_article);
let update_router = Router::with_filter(filters::patch()).handle(update_article);
let delete_router = Router::with_filter(filters::get()).handle(delete_article);
Router::new().push(show_router).push(update_router).push(delete_router);

Benutzerdefinierte Wisp

Für bestimmte häufig auftretende Abgleichsausdrücke können wir über PathFilter::register_wisp_regex oder PathFilter::register_wisp_builder einen kurzen Namen zuweisen. Zum Beispiel erscheint das GUID-Format oft in Pfaden. Der normale Weg ist, es jedes Mal so zu schreiben, wenn ein Abgleich benötigt wird:

Router::with_path("/articles/<id:/[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}/>");
Router::with_path("/users/<id:/[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}/>");

Dieses komplexe Regex jedes Mal zu schreiben, ist fehleranfällig und macht den Code weniger lesbar. Stattdessen können Sie dies tun:

use salvo::routing::filter::PathFilter;

#[tokio::main]
async fn main() {
    let guid = regex::Regex::new("[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}").unwrap();
    PathFilter::register_wisp_regex("guid", guid);
    Router::new()
        .push(Router::with_path("/articles/{id:guid}").get(show_article))
        .push(Router::with_path("/users/{id:guid}").get(show_user));
}

Sie müssen es nur einmal registrieren. Danach können Sie direkt einfache Notation wie {id:guid} verwenden, um GUIDs abzugleichen, was das Code-Schreiben vereinfacht.

Kommend von einem Controller-basierten Web-Framework, wie versteht man Router?

Die Hauptunterschiede zwischen Routing-entworfenen Web-Frameworks (wie Salvo) und traditionellen MVC oder Controller-entworfenen Frameworks sind:

  • Flexibilität: Routing-Design ermöglicht eine flexiblere Definition von Anfrageverarbeitungsflüssen und ermöglicht eine präzise Kontrolle über die Logik für jeden Pfad. Zum Beispiel können Sie in Salvo direkt Handler-Funktionen für bestimmte Pfade definieren:

    Router::with_path("articles").get(list_articles).post(create_article);

    Im Controller-Design müssen Sie typischerweise zuerst eine Controller-Klasse definieren und dann mehrere Methoden innerhalb der Klasse definieren, um verschiedene Anfragen zu behandeln:

    @Controller
    public class ArticleController {
        @GetMapping("/articles")
        public List<Article> listArticles() { /* ... */ }
        
        @PostMapping("/articles")
        public Article createArticle(@RequestBody Article article) { /* ... */ }
    }
  • Middleware-Integration: Routing-Frameworks bieten normalerweise eine prägnantere Möglichkeit, Middleware zu integrieren, und ermöglichen es, Middleware auf bestimmte Routen anzuwenden. Salvos Middleware kann präzise auf bestimmte Routen angewendet werden:

    Router::new()
        .push(
            Router::with_path("admin/articles")
                .hoop(admin_auth_middleware)  // Authentifizierungs-Middleware nur auf Admin-Routen anwenden
                .get(list_all_articles)
                .post(create_article),
        )
        .push(
            Router::with_path("articles")  // Öffentliche Routen benötigen keine Authentifizierung
                .get(list_public_articles),
        );
  • Code-Organisation: Routing-Design neigt dazu, Code basierend auf Funktionalität oder API-Endpunkten zu organisieren, anstatt auf der Model-View-Controller-Schichtung von MVC. Routing-Design ermutigt, Code gemäß API-Endpunkt-Funktionalität zu organisieren:

    // user_routes.rs - Benutzerbezogene Routen und Verarbeitungslogik