Начало работы с middleware в Django

Spread the love

Оригинальная статья: PAWEŁ FERTYKGetting started with Django middleware

Django поставляется с множеством полезных функций. Одним из них является механизм middleware (переводится как промежуточное программное обеспечение). В этом посте я кратко объясню, как работает middleware и как начать писать свой собственный.

Исходный код, включенный в этот пост, доступен на GitHub.

Общая концепция

Middleware позволяет обрабатывать запросы из браузера, прежде чем они достигнут представления Django, а также ответы от представлений до того, как они возвращаются в браузер. Django ведет список middleware для каждого проекта. Вы можете найти его в settings.py под названием MIDDLEWARE. Каждый новый проект Django уже имеет несколько middleware, добавленных в этот список, и в большинстве случаев вам не следует ничего удалять из этого списка. Однако, вы можете, добавить свой собственный.

Middleware применяется в том же порядке, в каком оно добавлено в список в настройках Django. Когда браузер отправляет запрос, он обрабатывается так:

Browser -> M_1 -> M_2 -> ... -> M_N -> View

Представление получает запрос, выполняет некоторые операции и возвращает ответ. На пути к браузеру ответ снова проходить через каждое middleware, но в обратном порядке:

Browser <- M_1 <- M_2 <- ... <- M_N <- View

Это очень краткое объяснение. Более подробное описание можно найти в документации Django.

Простой пример

Мы начнем с простого middleware, которое измеряет время, необходимое для обработки запроса. Все примеры в этом посте используют Django 3.0.5 и Python 3.6.9.

Настрйка проекта

Сначала создайте проект Django с одним приложением. Игнорируйте миграции, примеры из этого поста не будут использовать базу данных. Создайте файл под названием middleware.py в своем приложении: именно туда мы поместим большую часть кода.

django-admin startproject django_middleware
cd django_middleware
python manage.py startapp intro
touch intro/middleware.py

Ваш проект должен выглядеть так:

django_middleware/
├── django_middleware
│   ├── asgi.py
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── intro
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── middleware.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
└── manage.py

Не забудьте зарегистрировать свое приложение в django_middleware/settings.py:

INSTALLED_APPS = [
    'intro',
    ...
]

Теперь вы можете запустить проект:

python manage.py runserver

Создание Django middleware

Согласно документации Django, существует 2 способа создания middleware: как функция и как класс. Мы будем использовать первый метод, но последний пример покажет вам, как создать класс.

Общая структура middleware в Django выглядит следующим образом (пример скопирован из документации Django):

def simple_middleware(get_response):
    # Единовременная настройка и инициализация.
    def middleware(request):
        # Код должен быть выполнен для каждого запроса
        # до view
        response = get_response(request)
        # Код должен быть выполнен ответа после view
        return response
    return middleware

Функция simple_middleware вызывается один раз, когда Django инициализирует middleware и добавляет его в список всех middleware, используемых в проекте. Функция middleware вызывается для каждого запроса к серверу. Все до строки response = get_response (request) вызывается, когда запрос переходит из браузера на сервер. Все после этой строки вызывается, когда ответ отправляется с сервера обратно в браузер.

Что делает строка respone = get_response (request)? Если кратко, она вызывает следующее middleware в списке. Если это последний middleware, вызывается представление: оно получает запрос, выполняет некоторые операции и генерирует ответ. Этот ответ затем возвращается последнему middleware в списке, который, в свою очередь, отправляет его предыдущему, пока не закончится все middleware и ответ не будет отправлен в браузер.

В нашем примере мы хотим проверить, сколько времени занимает весь процесс обработки запроса. Отредактируйте файл intro/middleware.py следующим образом:

import time


def timing(get_response):
    def middleware(request):
        t1 = time.time()
        response = get_response(request)
        t2 = time.time()
        print("TOTAL TIME:", (t2 - t1))
        return response
    return middleware

В этом примере мы измеряем время в секундах (time.time()) до и после запроса и выводим разницу.

Следующим шагом является установка middleware, чтобы Django знал, что мы собираемся его использовать. Все, что нам нужно сделать, это добавить его в django_middleware/settings.py:

MIDDLEWARE = [
    'intro.middleware.timing',
    ...
]

Примечание: в этом примере intro – это имя нашего приложения Django, middleware – это имя файла Python, содержащего наш код, а timing – это имя функции middleware в этом файле.

Теперь мы готовы это проверить. Откройте браузер и перейдите к localhost:8000. В браузере вы должны увидеть страницу проекта Django по умолчанию (ту, что содержит ракету). В командной строке (где вы назвали python manage.py runserver) вы должны увидеть что-то похожее на это:

TOTAL TIME: 0.0013387203216552734
[04/Apr/2020 17:15:34] "GET / HTTP/1.1" 200 16351

Изменение запроса

Наше middleware работает достаточно хорошо, печатая информацию в командной строке. Но мы можем пойти еще дальше: как насчет добавления чего-либо в запрос, чтобы наши представления могли использовать его позже? Поскольку мы занимаемся синхронизацией времени, как насчет добавления даты и времени выполнения запроса?

Эта модификация будет довольно легкой. Отредактируйте файл intro/middleware.py следующим образом:

import time
import datetime


def timing(get_response):
    def middleware(request):
        request.current_time = datetime.datetime.now()
        t1 = time.time()
        response = get_response(request)
        t2 = time.time()
        print("TOTAL TIME:", (t2 - t1))
        return response
    return middleware

Мы добавили 2 строки: import datetime и request.current_time = datetime.datetime.now(). Вместе они добавят текущее время к нашему запросу. Теперь нам нужно представление, чтобы отобразить это время. Отредактируйте intro/views.py:

from django.http import HttpResponse


def showtime(request):
    return HttpResponse('Request time is: {}'.format(request.current_time))

Для такого простого примера нам не нужен шаблон, мы можем создать объект HttpResponse непосредственно в нашем коде.

Теперь нам нужен URL для нашей view. Создайте файл intro/urls.py и отредактируйте его:

from django.urls import path
from .views import showtime

urlpatterns = [
    path('', showtime),
]

Не забудьте отредактировать и django_middleware/urls.py:

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('', include('intro.urls')),
    path('admin/', admin.site.urls),
]

Давайте проверим это. Откройте localhost:8000 в вашем браузере. Вы должны увидеть что-то вроде этого:

Request time shown in browser

Обновите страницу несколько раз, чтобы убедиться, что вы получите разные результаты (время должно обновляться для каждого запроса).

Что-то более полезное: обработка исключений

Пришло время для более интересного примера. Рассмотрим ситуацию из реальной жизни: вы пишете программу, и она не работает. Это случается с лучшими из нас, не волнуйтесь. Что вы обычно тогда делаете? Вы ищите ответы на Stack Overflow? Как насчет создания middleware, которое будет выполнять этот поиск за нас?

Django middleware может включать функцию, которая будет вызываться каждый раз, когда возникает исключение. Эта функция называется process_exception, и она принимает 2 аргумента: request, вызвавший исключение, и само исключение.

Если наше middleware определено как функция, тогда мы можем реализовать process_exception следующим образом:

def simple_middleware(get_response):
    def middleware(request):
        return get_response(request)

    def process_exception(request, exception):
        # Do something useful with the exception
        pass

    middleware.process_exception = process_exception
    return middleware


Допустим в нашем случае мы хотим отправить наше исключение в Stack Overflow и получить ссылки на самые актуальные вопросы.

Краткое введение в API Stack Overflow

Если вы раньше не использовали API, не волнуйтесь. Основная идея такова: точно так же, как вы отправляете вопрос в Интернет с помощью веб-браузера, API – это еще один способ для вас отправлять вопросы, но только автоматически через код.

Stack Exchange достаточно любезен, чтобы разместить API для запросов к своим сайтам. Базовый URL-адрес: https://api.stackexchange.com/2.2/search, после чего вы можете добавить параметры поиска. И так, если вы хотите проверить 3 лучших результата (отсортированных по голосам) от Stack Overflow, помеченных как «python» и имеющих дело с Django, вы можете отправить запрос следующим образом: https://api.stackexchange.com/2.2/search?site=stackoverflow&pagesize=3&sort=votes&order=desc&tagged=python&intitle=django . Проверьте это в вашем браузере. Вы должны увидеть что-то вроде этого:

Stack Exchange API results

В Python для отправки такого запроса мы будем использовать модуль под названием requests

Stack Overflow middleware

Давайте создадим новое middleware с именем stackoverflow:

import requests
from django.http import HttpResponse

# Previous imports and timing middleware should remain unchanged


def stackoverflow(get_response):
    def middleware(request):
        # Этот метод ничего не делает, все, что мы хотим, 
        # это обработка исключений
        return get_response(request)

    def process_exception(request, exception):
        url = 'https://api.stackexchange.com/2.2/search'
        params = {
            'site': 'stackoverflow',
            'order': 'desc',
            'sort': 'votes',
            'pagesize': 3,
            'tagged': 'python;django',
            'intitle': str(exception),
        }
        response = requests.get(url, params=params)
        html = ''
        for question in response.json()['items']:
            html += '<h2><a href="{link}">{title}</a></h2>'.format(**question)
        return HttpResponse(html)

    middleware.process_exception = process_exception

    return middleware


Каждый раз, когда представление вызывает исключение, будет вызываться наш метод process_exception. Мы используем модуль requests для вызова Stack Exchange API. Большинство параметров говорят сами за себя. Они такие же, как мы использовали в примере с браузером, но вместо того, чтобы помещать их все в URL вручную, мы позволяем модулю запросов сделать это за нас. Мы просто изменили теги (для поиска Python и Django) и используем наше исключение в виде строки (str (exception)) для поиска заголовка доступных вопросов. После получения ответа от Stack Overflow мы собираем HTML-код, содержащий ссылку на каждый соответствующий вопрос. Надеюсь, мы сможем найти ответ на нашу проблему там. Наконец, этот HTML возвращается в браузер.

Обратите внимание, что ответ от переполнения стека не является обычной веб-страницей, а представляет собой набор информации в формате JSON. Вот почему мы вызываем response.json(), чтобы получить наши результаты.

Конечно, нам нужно установить это новое middleware:

MIDDLEWARE = [
    'intro.middleware.stackoverflow',
    'intro.middleware.timing',
    ...
]

Единственная проблема, которую мы имеем сейчас, заключается в том, что наш код отлично работает. Нам нужно немного его сломать, если мы хотим, чтобы наше новое middleware получило некоторые исключения для обработки. Отредактируйте intro/views.py:

def showtime(request):
    raise Exception('Django middleware')
    # return HttpResponse('Request time is: {}'.format(request.current_time))


Имейте в виду, что метод process_exception будет вызываться только для реальных исключений. Возвращение HttpResponseServerError или любого другого кода ошибки не считается.

Пришло время проверить это. Откройте localhost:8000 в вашем браузере. Вы должны увидеть что-то вроде этого:

Stack Overflow answers our call

middleware, которое мы только что создали, немного сложнее, чем начальные примеры. По мере роста вашего кода, может быть, лучше управлять middleware как классами, а не функциями. Наше middleware Stack Overflow как класс будет выглядеть так:

class StackOverflow():
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        return self.get_response(request)

    def process_exception(self, request, exception):
        url = 'https://api.stackexchange.com/2.2/search'
        params = {
            'site': 'stackoverflow',
            'order': 'desc',
            'sort': 'votes',
            'pagesize': 3,
            'tagged': 'python;django',
            'intitle': str(exception),
        }
        response = requests.get(url, params=params)
        html = ''
        for question in response.json()['items']:
            html += '<h2><a href="{link}">{title}</a></h2>'.format(**question)
        return HttpResponse(html)


Большая часть кода выглядит аналогично, но для класса нам нужно сохранить обратный вызов get_response в нашем экземпляре и использовать его для каждого вызова метода __call__. Если вы предпочитаете эту версию, не забудьте изменить настройки:

MIDDLEWARE = [
    'intro.middleware.StackOverflow',
    ...
]

Заключение

Это были очень простые примеры, но middleware может использоваться для многих других целей, таких как проверка токена авторизации, поиск подходящего пользователя и присоединение этого пользователя к запросу. Я уверен, что вы можете найти много идей самостоятельно, и, надеюсь, этот пост поможет вам начать. Если вы считаете, что чего-то не хватает или заметили ошибку, пожалуйста, дайте мне знать! (блог автора оригинальной статьи http://pfertyk.me/2020/04/getting-started-with-django-middleware/)

Была ли вам полезна эта статья?
[18 / 4.6]

Spread the love
Подписаться
Уведомление о
guest
7 Комментарий
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Катя
4 лет назад

Спасибо за полезную статью, как раз изучаю middleware.

Андрей
Андрей
3 лет назад

Единственный ресурс который позволяет разобраться в мелочах. Материал излагается очень просто и доступно. Такого нигде не видел. Спасибо огромное.

Petr
Petr
3 лет назад

Обратите внимание, что ответ от переполнения стека не является обычной веб-страницей, –> Обратите внимание, что ответ от StackOverflow не является обычной веб-страницей,

Анонимно
Анонимно
3 лет назад

Спасибо!

Анонимно
Анонимно
3 лет назад

Цитата: “то вы обычно тогда делаете? Вы проверяете переполнение стека ответов?” =) Так я и делаю… А где у вас переполнение стека ответов? 🙂 Если что – тут ошибочно переведено StackOverflow.

Evgeny
Evgeny
3 лет назад

Огромное спасибо, практические примеры – огонь) Лучшие!

mlavetso
mlavetso
3 лет назад

Как по мне пример с Класом более понятен и лаконичен, да и в новой Джанге лучше искользовать класовый middleware который уже наследуеться от нового generic-middleware, чтоб была доступна поддержка асинхронности