저는 요청 처리나 라우팅에 대한 Django의 접근 방식을 좋아하지 않습니다. 프레임워크에는 옵션이 너무 많고 의견이 너무 적습니다. 반대로 Ruby on Rails와 같은 프레임워크는 Action Controller 및 리소스 라우팅을 통해 요청 처리 및 라우팅에 대한 표준화된 규칙을 제공합니다.
이 게시물에서는 Django REST Framework의 ViewSet 및 SimpleRouter를 확장하여 서버 렌더링 Django 애플리케이션에서 Rails와 유사한 요청 핸들러 클래스 + 리소스 라우팅을 제공합니다. 또한 맞춤형 미들웨어를 통해 PUT, PATCH 및 DELETE 요청에 대한 양식 수준 메서드 스푸핑 기능도 제공합니다.
요청 처리를 위해 Django는 함수 기반 뷰, 일반 클래스 기반 뷰, 모델 클래스 기반 뷰를 제공합니다. Django의 클래스 기반 뷰는 객체 지향 프로그래밍의 최악의 측면을 구현하여 제어 흐름을 난독화하는 동시에 함수 기반 카운터 부분보다 훨씬 더 많은 코드를 요구합니다.
마찬가지로 프레임워크는 URL 경로 구조에 대한 권장 사항이나 규칙을 제공하지 않습니다. 비교를 위해 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 |
프레임워크의 규칙으로 인해 각 Ruby on Rails 앱은 유사하게 구성되어 있으며 새로운 개발자가 빠르게 온보딩할 수 있습니다. 이에 비해 Django의 자유방임 접근 방식은 상당한 양의 자전거 탈피로 귀결됩니다.
뷰 및 URL 구조에 대한 프레임워크 적용 규칙이 없으면 각 Django 앱은 서로 다른 접근 방식을 취하는 눈송이가 됩니다. 더 나쁜 것은 단일 앱이 식별 가능한 운율이나 이유 없이 보기와 URL에 대해 여러 가지 서로 다른 접근 방식을 취할 수 있다는 것입니다. 나는 그것을 보았다. 살았습니다.
하지만 Django 생태계에는 이미 Rails와 유사한 대체 접근 방식이 있습니다.
Django 자체와 달리 Django REST Framework에는 강력한 라우팅 규칙이 있습니다. ViewSet 클래스와 SimpleRouter는 다음 규칙을 적용합니다.
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 |
안타깝게도 이 오직 API 경로에서만 작동합니다. Django 서버 렌더링 애플리케이션에서는 작동하지 않습니다. 이는 기본 브라우저 양식이 GET 및 POST 요청만 구현할 수 있기 때문입니다. Ruby on Rails는 이러한 제한을 해결하기 위해 양식에 숨겨진 입력을 사용합니다.
<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>
POST 요청을 통해 제출하면 Ruby on Rails는 마술처럼 백엔드에서 요청 메서드를 PUT으로 변경합니다. Django에는 그런 기능이 없습니다.
Django REST Framework의 기능을 활용하여 Django에서 Rails와 유사한 요청 처리 및 리소스 라우팅을 구현하고 요청 방법을 재정의하기 위한 자체 미들웨어를 구축할 수 있습니다. 이렇게 하면 Django 템플릿을 사용하는 서버 렌더링 앱에서 유사한 경험을 얻을 수 있습니다.
Django REST Framework의 ViewSet 및 SimpleRouter 클래스는 우리가 에뮬레이트하려는 Rails와 유사한 경험을 대부분 제공하므로 이를 구현의 기초로 사용할 것입니다. 우리가 구축할 라우팅 구조는 다음과 같습니다.
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.
근데 살짝 어색한 핏이에요. Rails 및 Laravel 컨트롤러에는 양식 표시와 데이터베이스에 리소스 커밋을 위한 별도의 엔드포인트가 있어 Django로 달성할 수 있는 것보다 더 많은 "관심사 분리"가 있는 것처럼 보입니다.
ViewSet 모델은 "리소스"라는 개념과도 밀접하게 결합되어 있습니다. TemplateViewSet을 사용하면 페이지가 목록 메소드를 사용해야 하기 때문에 홈페이지나 연락처 페이지에 적합하지 않습니다(TemplateRouter에서 색인으로 이름을 바꿀 수 있음). 이러한 경우에는 자전거 타기 기회를 다시 제공하는 기능 기반 보기를 사용하고 싶을 것입니다.
마지막으로, 자동으로 생성된 URL 경로를 사용하면 django-extensions와 같은 도구를 사용하지 않고는 애플리케이션에 어떤 경로가 있는지 한눈에 파악하기가 더 어렵습니다.
이러한 접근 방식은 Django가 제공하지 않는 기능을 제공합니다. 즉, 자전거를 탈 기회가 적은 강력한 규칙입니다. 그것이 당신의 마음에 든다면 시도해 볼 가치가 있을 것입니다.
위 내용은 서버 렌더링 Django 앱에서 Rails와 유사한 리소스 컨트롤러 에뮬레이션의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!