Émulation de contrôleurs de ressources de type Rails dans une application Django rendue par le serveur

王林
Libérer: 2024-07-18 06:19:00
original
1147 Les gens l'ont consulté

Emulating Rails-like resource controllers in a server-rendered Django app

Je n'aime pas l'approche de Django en matière de gestion ou de routage des requêtes. Le cadre comporte trop d’options et trop peu d’opinions. À l'inverse, des frameworks comme Ruby on Rails fournissent des conventions standardisées pour la gestion et le routage des requêtes via son Action Controller et le routage des ressources.

Cet article étendra ViewSet et SimpleRouter de Django REST Framework pour fournir une classe de gestionnaire de requêtes de type Rails + un routage de ressources dans les applications Django de rendu serveur. Il propose également une usurpation de méthode au niveau du formulaire pour les requêtes PUT, PATCH et DELETE via un middleware personnalisé.

Le problème avec le routage de Django

Pour le traitement des requêtes, Django propose des vues basées sur des fonctions, des vues génériques basées sur des classes et des vues basées sur des classes de modèles. Les vues basées sur les classes de Django incarnent les pires aspects de la programmation orientée objet, obscurcissant le flux de contrôle tout en nécessitant considérablement plus de code que leurs homologues basés sur les fonctions.

De même, le framework ne propose pas de recommandations ou de conventions pour la structure des chemins d'URL. A titre de comparaison, voici la convention pour une ressource Ruby on Rails :

HTTP Verb Path Controller#Action Used for
GET /posts posts#index list of all posts
GET /posts/new posts#new form for creating a new post
POST /posts posts#create create a new post
GET /posts/:id posts#show display a specific post
GET /posts/:id/edit posts#edit form for editing a post
PATCH/PUT /posts/:id posts#update update a specific post
DELETE /posts/:id posts#destroy delete a specific post

En raison des conventions du framework, chaque application Ruby on Rails est structurée de la même manière et les nouveaux développeurs peuvent s'intégrer rapidement. En comparaison, l'approche de laissez-faire de Django aboutit à un important délestage de vélos.

En l'absence de conventions imposées par le framework pour les vues et les structures d'URL, chaque application Django devient un flocon de neige qui adopte une approche différente. Pire encore, une seule application peut adopter plusieurs approches disparates en matière de vues et d'URL, sans rime ni raison perceptible. Je l'ai vu. Je l'ai vécu.

Mais l'écosystème Django propose déjà des approches alternatives similaires à Rails.

Le routage du Framework Django REST

Contrairement à Django lui-même, Django REST Framework a de solides conventions de routage. Sa classe ViewSet et SimpleRouter appliquent les conventions suivantes :

HTTP Verb Path ViewSet.Action Used for
GET /posts/ PostsViewset.list list of all posts
POST /posts/ PostsViewset.create create a new post
GET /posts/:id/ PostsViewset.retrieve return a specific post
PUT /posts/:id/ PostsViewset.update update a specific post
PATCH /posts/:id/ PostsViewset.partial_update update part of a specific post
DELETE /posts/:id/ PostsViewset.destroy delete a specific post

Malheureusement, cela uniquement fonctionne avec les routes API. Cela ne fonctionne pas avec les applications rendues par le serveur Django. En effet, les formulaires natifs du navigateur ne peuvent implémenter que les requêtes GET et POST. Ruby on Rails utilise une entrée cachée dans ses formulaires pour contourner cette limitation :

<form method="POST" action="/books">
  <input name="title" type="text" value="My book" />
  <input type="submit" />

  <!-- Here's the magic part: -->
  <input name="_method" type="hidden" value="put" />
</form>
Copier après la connexion

Lorsqu'il est soumis via une requête POST, Ruby on Rails changera comme par magie la méthode de la requête en PUT dans le backend. Django n'a pas une telle fonctionnalité.

Nous pouvons exploiter les fonctionnalités de Django REST Framework pour implémenter une gestion des requêtes et un routage des ressources de type Rails dans Django, et créer notre propre middleware pour remplacer la méthode de requête. De cette façon, nous pouvons obtenir une expérience similaire dans les applications rendues par le serveur qui utilisent des modèles Django.

Plan d'action

Étant donné que les classes ViewSet et SimpleRouter de Django REST Framework fournissent une grande partie de l'expérience de type Rails que nous souhaitons émuler, nous les utiliserons comme base de notre implémentation. Voici la structure de routage que nous allons construire :

HTTP Verb Path ViewSet.Action Used for
GET /posts/ PostsViewset.list list of all posts
GET /posts/create/ PostsViewset.create form for creating a new post
POST /posts/create/ PostsViewset.create create a new post
GET /posts/:id/ PostsViewset.retrieve return a specific post
GET /posts/:id/update/ PostsViewset.update form for editing a post
PUT /posts/:id/update/ PostsViewset.update update a specific post
DELETE /posts/:id/ PostsViewset.destroy delete a specific post

The routes in bold are ones that differ from what Django REST Framework's SimpleRouter provides out-of-the-box.

To build this Rails-like experience, we must do the following:

  1. Subclass REST Framework's ViewSet and provide it with defaults appropriate for server-rendered Django templates.
  2. Subclass REST Framework's SimpleRouter and create new routes for the create and update methods.
  3. Create a middleware that can change the request verb based on a hidden input named _method.

Prep work

We need to do a little bit of setup before we're ready to implement our routing. First, install Django REST Framework by running the following command in your main project directory:

pip install djangorestframework
Copier après la connexion

Then, add REST Framework to the INSTALLED_APPS list in settings.py:

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",

    # Add this:
    "rest_framework",
]
Copier après la connexion

Next, we need a place to store our subclasses and custom middleware. Create an overrides directory in the main project directory with the following files:

overrides/
├── __init__.py
├── middleware.py
├── routers.py
└── viewsets.py
Copier après la connexion

With that, we're ready to code.

Subclassing ViewSet for template rendering

Place the following code in overrides/viewsets.py:

from rest_framework.authentication import SessionAuthentication
from rest_framework.parsers import FormParser
from rest_framework.renderers import TemplateHTMLRenderer
from rest_framework.viewsets import ViewSet


class TemplateViewSet(ViewSet):
    authentication_classes = [SessionAuthentication]
    parser_classes = [FormParser]
    renderer_classes = [TemplateHTMLRenderer]
Copier après la connexion

Our future ViewSets will be subclassed from this TemplateViewSet, and it will serve the same purpose as a Rails Action Controller. It uses the TemplateHTMLRenderer so that it renders HTML by default, the FormParser to parse form submissions, and SessionAuthentication to authenticate the user. It's nice that Django REST Framework includes these, allowing us to leverage DRF for traditional server-rendered web apps.

Subclassing SimpleRouter

The router class is what will enable us to send requests to the appropriate ViewSet method. By default, REST Framework's simple router uses POST /:resource/ to create a new resource, and PUT /:resource/:id/ to update a resource.

We must modify the create and update routes. Unlike Rails or Laravel, Django has no way to pass form errors to a redirected route. Because of this, a page containing a form to create or update a resource must post the form data to its own URL.

We will use the following routes for creating and updating resources:

  • /:resource/create/ for creating a new resource.
  • /:resource/:id/update/for updating a resource.

Django REST Framework's SimpleRouter has a routes list that associates the routes with the methods of the ViewSet (source code). We will subclass SimpleRouter and override its routes list, moving the create and update methods to their own routes with our desired paths.

Add the following to overrides/routers.py:

from rest_framework.routers import SimpleRouter, Route, DynamicRoute


class TemplateRouter(SimpleRouter):
    routes = [
        Route(
            url=r"^{prefix}{trailing_slash}$",
            mapping={"get": "list"},
            name="{basename}-list",
            detail=False,
            initkwargs={"suffix": "List"},
        ),
        # NEW: move "create" from the route above to its own route.
        Route(
            url=r"^{prefix}/create{trailing_slash}$",
            mapping={"get": "create", "post": "create"},
            name="{basename}-create",
            detail=False,
            initkwargs={},
        ),
        DynamicRoute(
            url=r"^{prefix}/{url_path}{trailing_slash}$",
            name="{basename}-{url_name}",
            detail=False,
            initkwargs={},
        ),
        Route(
            url=r"^{prefix}/{lookup}{trailing_slash}$",
            mapping={"get": "retrieve", "delete": "destroy"},
            name="{basename}-detail",
            detail=True,
            initkwargs={"suffix": "Instance"},
        ),
        # NEW: move "update" from the route above to its own route.
        Route(
            url=r"^{prefix}/{lookup}/update{trailing_slash}$",
            mapping={"get": "update", "put": "update"},
            name="{basename}-update",
            detail=True,
            initkwargs={},
        ),
        DynamicRoute(
            url=r"^{prefix}/{lookup}/{url_path}{trailing_slash}$",
            name="{basename}-{url_name}",
            detail=True,
            initkwargs={},
        ),
    ]
Copier après la connexion

Overriding the HTTP method in middleware

Place the following code in overrides/middleware.py:

from django.conf import settings


class FormMethodOverrideMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        if request.method == "POST":
            desired_method = request.POST.get("_method", "").upper()
            if desired_method in ("PUT", "PATCH", "DELETE"):
                token = request.POST.get("csrfmiddlewaretoken", "")

                # Override request method.
                request.method = desired_method

                # Hack to make CSRF validation pass.
                request.META[settings.CSRF_HEADER_NAME] = token

        return self.get_response(request)
Copier après la connexion

If an incoming request contains a form field named _method with a value of PUT, PATCH, or DELETE, this middleware will override the request's method with its value. This allows forms to emulate other HTTP methods and have their submissions routed to the appropriate request handler.

The interesting bit of this code is the CSRF token hack. Django's middleware only checks for the csrfmiddlewaretoken form field on POST requests. However, it checks for a CSRF token on all requests with methods not defined as "safe" (any request that's not GET, HEAD, OPTIONS, or TRACE).

PUT, PATCH and DELETE requests are available through JavaScript and HTTP clients. Django expects these requests to use a CSRF token header like X-CSRFToken. Because Django will not check the the csrfmiddlewaretoken form field in non-POST requests, we must place the token in the header where the CSRF middleware will look for it.

Now that we've completed our middleware, add it to the MIDDLEWARE list in settings.py:

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",

    # Add this:
    "overrides.middleware.FormMethodOverrideMiddleware"
]
Copier après la connexion

Using the ViewSet and Router

Let's say that we have a blog app within our Django project. Here is what the BlogPostViewSet would look like:

# blog/views.py

from blog.forms import BlogPostForm
from blog.models import BlogPost
from django.shortcuts import get_object_or_404, render, redirect
from overrides.viewsets import TemplateViewSet


class BlogPostViewSet(TemplateViewSet):
    def list(self, request):
        return render(request, "blog/list.html", {
            "posts": BlogPost.objects.all()
        })

    def retrieve(self, request, pk):
        post = get_object_or_404(BlogPost, id=pk)
        return render(request, "blog/retrieve.html", {"post": post})

    def create(self, request):
        if request.method == "POST":
            form = BlogPostForm(request.POST)
            if form.is_valid():
                post = form.save()
                return redirect(f"/posts/{post.id}/")
        else:
            form = BlogPostForm()

        return render(request, "blog/create.html", {"form": form})

    def update(self, request, pk):
        post = BlogPost.objects.get(id=pk)
        if request.method == "PUT":
            form = BlogPostForm(request.POST, instance=post)
            if form.is_valid():
                post = form.save()
                return redirect(f"/posts/{post.id}/")
        else:
            form = BlogPostForm(instance=post)

        return render(request, "blog/update.html", {
            "form": form, "post": post
        })

    def destroy(self, request, pk):
        website = BlogPost.objects.get(id=pk)
        website.delete()
        return redirect(f"/posts/")
Copier après la connexion

Here is how we would add these URLs to the project's urlpatterns list using the TemplateRouter that we created:

# project_name/urls.py

from blog.views import BlogPostViewSet
from django.contrib import admin
from django.urls import path
from overrides.routers import TemplateRouter

urlpatterns = [
    path("admin/", admin.site.urls),
    # other routes...
]

router = TemplateRouter()
router.register(r"posts", BlogPostViewSet, basename="post")

urlpatterns += router.urls
Copier après la connexion

Finally, ensure that the forms within your Django templates have both the CSRF token and hidden _method field. Here's an example from the update post form:

<form method="POST" action="/posts/{{ post.id }}/update/">
  {% csrf_token %}
  {{ form }}
  <input type="hidden" name="_method" value="PUT" />
  <button type="submit">Submit</button>
</form>
Copier après la connexion

And that's it. You now have Rails or Laravel-like controllers in your Django application.

Should you actually consider doing this?

Maybe. The advantage of this approach is that it removes a lot of opportunities for bikeshedding if your app follows REST-like conventions. If you've ever seen Adam Wathan's Cruddy by Design talk, you know that following REST-like conventions can get you pretty far.

Mais c'est un ajustement légèrement gênant. Les contrôleurs Rails et Laravel ont des points de terminaison séparés pour afficher un formulaire ou engager une ressource dans la base de données, ce qui leur donne l'impression d'avoir une « séparation des préoccupations » plus poussée que celle que l'on peut obtenir avec Django.

Le modèle ViewSet est également étroitement lié à l'idée de « ressource ». L'utilisation de TemplateViewSet serait délicate pour une page d'accueil ou une page de contact, car la page serait obligée d'utiliser la méthode de liste (bien que celle-ci puisse être renommée pour indexer sur TemplateRouter). Dans ces cas-là, vous seriez tenté d'utiliser une vue basée sur les fonctions, qui réintroduit la possibilité de débarras de vélos.

Enfin, les chemins d'URL générés automatiquement rendent plus difficile la compréhension des itinéraires de l'application en un coup d'œil sans utiliser des outils tels que les extensions Django.

Cela dit, cette approche offre quelque chose que Django n'offre pas : des conventions fortes avec moins de possibilités de débarras de vélos. Si cela vous intéresse, cela vaut peut-être la peine d'essayer.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

source:dev.to
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal
À propos de nous Clause de non-responsabilité Sitemap
Site Web PHP chinois:Formation PHP en ligne sur le bien-être public,Aidez les apprenants PHP à grandir rapidement!