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

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

Jul 18, 2024 am 06:19 AM

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

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Stock Market GPT

Stock Market GPT

人工智慧支援投資研究,做出更明智的決策

Clothoff.io

Clothoff.io

AI脫衣器

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

如何使用Python自動化從Excel到Web表單的數據輸入? 如何使用Python自動化從Excel到Web表單的數據輸入? Aug 12, 2025 am 02:39 AM

使用Python自動化將Excel數據填入網頁表單的方法是:先用pandas讀取Excel數據,再用Selenium控制瀏覽器自動填寫並提交表單;具體步驟包括安裝pandas、openpyxl和Selenium庫,下載對應瀏覽器驅動,用pandas讀取data.xlsx文件中的Name、Email、Phone等字段,通過Selenium啟動瀏覽器打開目標網頁,定位表單元素並逐行填入數據,使用WebDriverWait處理動態加載內容,添加異常處理和延遲確保穩定性,最後提交表單並循環處理所有數據行

Python中的類方法是什麼 Python中的類方法是什麼 Aug 21, 2025 am 04:12 AM

ClassmethodsinPythonareboundtotheclassandnottoinstances,allowingthemtobecalledwithoutcreatinganobject.1.Theyaredefinedusingthe@classmethoddecoratorandtakeclsasthefirstparameter,referringtotheclassitself.2.Theycanaccessclassvariablesandarecommonlyused

如何處理不適合內存的Python中的大型數據集? 如何處理不適合內存的Python中的大型數據集? Aug 14, 2025 pm 01:00 PM

當Python中處理超出內存的大型數據集時,不能一次性加載到RAM中,而應採用分塊處理、磁盤存儲或流式處理等策略;可通過Pandas的chunksize參數分塊讀取CSV文件並逐塊處理,使用Dask實現類似Pandas語法的並行化和任務調度以支持大內存數據操作,編寫生成器函數逐行讀取文本文件減少內存佔用,利用Parquet列式存儲格式結合PyArrow高效讀取特定列或行組,使用NumPy的memmap對大型數值數組進行內存映射以按需訪問數據片段,或將數據存入SQLite或DuckDB等輕量級數據

python asyncio隊列示例 python asyncio隊列示例 Aug 21, 2025 am 02:13 AM

asyncio.Queue是用於異步任務間安全通信的隊列工具,1.生產者通過awaitqueue.put(item)添加數據,消費者用awaitqueue.get()獲取數據;2.每處理完一項需調用queue.task_done(),以便queue.join()等待所有任務完成;3.使用None作為結束信號通知消費者停止;4.多個消費者時,需發送多個結束信號或在取消任務前確保所有任務已處理完畢;5.隊列支持設置maxsize限制容量,put和get操作自動掛起不阻塞事件循環,程序最終通過canc

HDF5 數據集名稱與組名稱衝突:解決方案與最佳實踐 HDF5 數據集名稱與組名稱衝突:解決方案與最佳實踐 Aug 23, 2025 pm 01:15 PM

本文針對使用 h5py 庫操作 HDF5 文件時,數據集名稱與組名稱衝突的問題,提供詳細的解決方案和最佳實踐。文章將深入分析衝突產生的原因,並提供代碼示例,展示如何有效地避免和解決此類問題,確保 HDF5 文件的正確讀寫。通過本文,讀者將能夠更好地理解 HDF5 文件結構,並編寫更健壯的 h5py 代碼。

如何使用Python進行股票市場分析和預測? 如何使用Python進行股票市場分析和預測? Aug 11, 2025 pm 06:56 PM

Python可以用於股票市場分析與預測,答案是肯定的,通過使用yfinance等庫獲取數據,利用pandas進行數據清洗和特徵工程,結合matplotlib或seaborn進行可視化分析,再運用ARIMA、隨機森林、XGBoost或LSTM等模型構建預測系統,並通過回測評估性能,最終可藉助Flask或FastAPI部署應用,但需注意市場預測的不確定性、過擬合風險及交易成本影響,成功依賴於數據質量、模型設計和合理預期。

如何使用Python中的RE模塊使用正則表達式? 如何使用Python中的RE模塊使用正則表達式? Aug 22, 2025 am 07:07 AM

正則表達式在Python中通過re模塊實現,用於搜索、匹配和操作字符串。 1.使用re.search()在整個字符串中查找第一個匹配項,re.match()僅在字符串開頭匹配;2.用括號()捕獲匹配的子組,可命名以提高可讀性;3.re.findall()返回所有非重疊匹配的列表,re.finditer()返回匹配對象的迭代器;4.re.sub()替換匹配的文本,支持函數動態替換;5.常用模式包括\d、\w、\s等,可使用re.IGNORECASE、re.MULTILINE、re.DOTALL、re

如何在Python中將文件和目錄從一個位置複製到另一個位置 如何在Python中將文件和目錄從一個位置複製到另一個位置 Aug 11, 2025 pm 06:11 PM

要復製文件和目錄,Python的shutil模塊提供了高效且安全的方法。 1.使用shutil.copy()或shutil.copy2()複製單個文件,後者保留元數據;2.使用shutil.copytree()遞歸複製整個目錄,目標目錄不能預先存在,但可通過dirs_exist_ok=True(Python3.8 )允許目標存在;3.可結合ignore參數和shutil.ignore_patterns()或自定義函數過濾特定文件;4.複製僅目錄結構需用os.walk()和os.makedirs()

See all articles