サーバーレンダリングされた Django アプリで Rails のようなリソース コントローラーをエミュレートする

王林
リリース: 2024-07-18 06:19:00
オリジナル
1148 人が閲覧しました

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

私はリクエスト処理やルーティングに対する Django のアプローチが好きではありません。このフレームワークには選択肢が多すぎますが、意見が少なすぎます。逆に、Ruby on Rails のようなフレームワークは、アクション コントローラーとリソース ルーティングを介したリクエストの処理とルーティングのための標準化された規則を提供します。

この投稿では、Django REST フレームワークの ViewSet と SimpleRouter を拡張して、サーバー レンダー Django アプリケーションで Rails のようなリクエスト ハンドラー クラスとリソース ルーティングを提供します。また、カスタム ミドルウェアを介した PUT、PATCH、DELETE リクエストに対するフォームレベルのメソッド スプーフィング機能も備えています。

Django のルーティングの問題

リクエスト処理のために、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 REST フレームワークのルーティング

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:

  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
ログイン後にコピー

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.

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]
ログイン後にコピー

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={},
        ),
    ]
ログイン後にコピー

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)
ログイン後にコピー

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"
]
ログイン後にコピー

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/")
ログイン後にコピー

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.

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.

しかし、少しぎこちないフィット感です。 Rails と Laravel コントローラーには、フォームの表示とデータベースへのリソースのコミットのための別個のエンドポイントがあり、Django で達成できるよりもさらに「関心事の分離」が行われているように見えます。

ViewSet モデルは、「リソース」の概念とも密接に結びついています。 TemplateViewSet を使用すると、ページではリスト メソッドの使用が強制されるため、ホームページや連絡先ページには不向きです (ただし、これは TemplateRouter のインデックスに名前を変更できます)。このような場合、機能ベースのビューを使用したくなるでしょう。これにより、バイクシェディングの機会が再び導入されます。

最後に、自動生成された URL パスにより、django-extensions などのツールを使用しない限り、アプリケーションにどのようなルートがあるかを一目で理解することが難しくなります。

そうは言っても、このアプローチは Django にはないものを提供します。それは、バイクシェディングの機会が少ない強力な規約です。これに魅力を感じたら、試してみる価値があるかもしれません。

以上がサーバーレンダリングされた Django アプリで Rails のようなリソース コントローラーをエミュレートするの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

ソース:dev.to
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート
私たちについて 免責事項 Sitemap
PHP中国語ウェブサイト:福祉オンライン PHP トレーニング,PHP 学習者の迅速な成長を支援します!