Rumah > pembangunan bahagian belakang > Tutorial Python > Meniru pengawal sumber seperti Rails dalam apl Django yang diberikan pelayan

Meniru pengawal sumber seperti Rails dalam apl Django yang diberikan pelayan

王林
Lepaskan: 2024-07-18 06:19:00
asal
1201 orang telah melayarinya

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

Saya tidak suka pendekatan Django untuk meminta pengendalian atau penghalaan. Rangka kerja mempunyai terlalu banyak pilihan dan terlalu sedikit pendapat. Sebaliknya, rangka kerja seperti Ruby on Rails menyediakan konvensyen piawai untuk pengendalian permintaan dan penghalaan melalui Pengawal Tindakan dan penghalaan sumbernya.

Siaran ini akan memanjangkan Django REST Framework's ViewSet dan SimpleRouter untuk menyediakan kelas pengendali permintaan + penghalaan sumber seperti Rails dalam aplikasi Django render pelayan. Ia juga menampilkan pemalsuan kaedah peringkat bentuk untuk permintaan PUT, PATCH dan DELETE melalui middleware tersuai.

Masalah dengan penghalaan Django

Untuk pengendalian permintaan, Django menawarkan pandangan berasaskan fungsi, pandangan berasaskan kelas generik dan pandangan berasaskan kelas model. Pandangan berasaskan kelas Django merangkumi aspek terburuk pengaturcaraan berorientasikan objek, mengelirukan aliran kawalan sambil memerlukan lebih banyak kod daripada bahagian kaunter berasaskan fungsinya.

Begitu juga, rangka kerja tidak menawarkan pengesyoran atau konvensyen untuk struktur laluan URL. Sebagai perbandingan, berikut ialah konvensyen untuk sumber 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

Disebabkan konvensyen rangka kerja, setiap apl Ruby on Rails distrukturkan dengan cara yang sama dan pembangun baharu boleh masuk dengan cepat. Sebagai perbandingan, pendekatan laissez-faire Django memuncak dengan penumpahan basikal yang ketara.

Dengan ketiadaan konvensyen yang dikuatkuasakan rangka kerja untuk paparan dan struktur URL, setiap apl Django menjadi kepingan salji yang mengambil pendekatan berbeza. Lebih teruk lagi, satu apl mungkin mengambil beberapa pendekatan yang berbeza untuk paparan dan URL tanpa rima atau alasan yang boleh dilihat. Saya telah melihatnya. Saya telah menjalaninya.

Tetapi ekosistem Django sudah mempunyai pendekatan alternatif yang serupa dengan Rails.

Penghalaan Rangka Kerja Django REST

Tidak seperti Django sendiri, Rangka Kerja Django REST mempunyai konvensyen penghalaan yang kukuh. Kelas ViewSet dan SimpleRouter menguatkuasakan konvensyen berikut:

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

Malangnya, ini hanya berfungsi dengan laluan API. Ia tidak berfungsi dengan aplikasi yang diberikan pelayan Django. Ini kerana borang penyemak imbas asli hanya boleh melaksanakan permintaan GET dan POST. Ruby on Rails menggunakan input tersembunyi dalam bentuknya untuk mengatasi had ini:

<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>
Salin selepas log masuk

Apabila diserahkan melalui permintaan POST, Ruby on Rails secara ajaib akan menukar kaedah permintaan kepada PUT di bahagian belakang. Django tidak mempunyai ciri sedemikian.

Kami boleh memanfaatkan ciri Django REST Framework untuk melaksanakan pengendalian permintaan seperti Rails dan penghalaan sumber dalam Django, dan membina perisian tengah kami sendiri untuk mengatasi kaedah permintaan. Dengan cara ini kita boleh mendapatkan pengalaman yang serupa dalam apl yang diberikan pelayan yang menggunakan templat Django.

Pelan tindakan

Oleh kerana kelas ViewSet dan SimpleRouter Django REST Framework menyediakan banyak pengalaman seperti Rails yang ingin kami contohi, kami akan menggunakannya sebagai asas pelaksanaan kami. Berikut ialah struktur penghalaan yang akan kami bina:

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
Salin selepas log masuk

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",
]
Salin selepas log masuk

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
Salin selepas log masuk

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]
Salin selepas log masuk

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={},
        ),
    ]
Salin selepas log masuk

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)
Salin selepas log masuk

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"
]
Salin selepas log masuk

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/")
Salin selepas log masuk

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
Salin selepas log masuk

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>
Salin selepas log masuk

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.

Tetapi ia agak janggal. Pengawal Rails dan Laravel mempunyai titik akhir yang berasingan untuk memaparkan borang lwn menyerahkan sumber kepada pangkalan data, memberikan mereka rupa mempunyai "pemisahan kebimbangan" lebih lanjut daripada yang boleh dicapai dengan Django.

Model ViewSet juga digabungkan dengan idea "sumber". Menggunakan TemplateViewSet adalah kesesuaian yang janggal untuk halaman utama atau halaman kenalan kerana halaman itu akan dipaksa untuk menggunakan kaedah senarai (walaupun ini boleh dinamakan semula kepada indeks pada TemplateRouter). Dalam kes ini, anda akan tergoda untuk menggunakan paparan berasaskan fungsi, yang memperkenalkan semula peluang untuk menumpahkan basikal.

Akhir sekali, laluan URL yang dijana secara automatik menjadikannya lebih sukar untuk memahami laluan apa yang ada pada aplikasi sepintas lalu tanpa menggunakan alatan seperti sambungan django.

Semua yang dikatakan, pendekatan ini menawarkan sesuatu yang tidak Django: konvensyen yang kukuh dengan peluang yang lebih sedikit untuk menumpahkan basikal. Jika itu menarik kepada anda, maka ini mungkin berbaloi.

Atas ialah kandungan terperinci Meniru pengawal sumber seperti Rails dalam apl Django yang diberikan pelayan. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!

sumber:dev.to
Kenyataan Laman Web ini
Kandungan artikel ini disumbangkan secara sukarela oleh netizen, dan hak cipta adalah milik pengarang asal. Laman web ini tidak memikul tanggungjawab undang-undang yang sepadan. Jika anda menemui sebarang kandungan yang disyaki plagiarisme atau pelanggaran, sila hubungi admin@php.cn
Tutorial Popular
Lagi>
Muat turun terkini
Lagi>
kesan web
Kod sumber laman web
Bahan laman web
Templat hujung hadapan