我不喜欢 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中文网其他相关文章!