首頁 > 後端開發 > Python教學 > 在伺服器渲染的 Django 應用程式中模擬類似 Rails 的資源控制器

在伺服器渲染的 Django 應用程式中模擬類似 Rails 的資源控制器

王林
發布: 2024-07-18 06:19:00
原創
1203 人瀏覽過

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

我不喜歡 Django 的請求處理或路由方法。該框架的選項太多,意見太少。相反,像 Ruby on Rails 這樣的框架透過其 Action Controller 和資源路由提供了用於請求處理和路由的標準化約定。

這篇文章將擴展 Django REST Framework 的 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 應用程式都會變成採用不同方法的雪花。更糟糕的是,單一應用程式可能會採用多種不同的方法來存取視圖和 URL,而沒有明顯的規律或原因。我見過。我已經經歷過了。

但是 Django 生態系統已經有了類似 Rails 的替代方法。

Django REST Framework 的路由

與 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:

  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中文網其他相關文章!

來源:dev.to
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板