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