私はリクエスト処理やルーティングに対する Django のアプローチが好きではありません。このフレームワークには選択肢が多すぎますが、意見が少なすぎます。逆に、Ruby on Rails のようなフレームワークは、アクション コントローラーとリソース ルーティングを介したリクエストの処理とルーティングのための標準化された規則を提供します。
この投稿では、Django REST フレームワークの 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 アプリは異なるアプローチをとるスノーフレークになります。さらに悪いことに、1 つのアプリが、何の韻も理由もなく、ビューと URL に対していくつかの異なるアプローチを採用している可能性があります。見たことがあります。私はそれを生きてきました。
しかし、Django エコシステムにはすでに Rails に似た代替アプローチがあります。
Django 自体とは異なり、Django REST フレームワークには強力なルーティング規則があります。その 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 の機能を利用して、Rails のようなリクエスト処理とリソース ルーティングを Django に実装し、リクエスト メソッドをオーバーライドするための独自のミドルウェアを構築できます。このようにして、Django テンプレートを使用するサーバー レンダリング アプリでも同様のエクスペリエンスを得ることができます。
Django REST フレームワークの 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 中国語 Web サイトの他の関連記事を参照してください。