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é.
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.
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>
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.
É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:
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
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", ]
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
With that, we're ready to code.
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]
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.
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:
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={}, ), ]
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)
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" ]
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/")
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
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>
And that's it. You now have Rails or Laravel-like controllers in your Django application.
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!