У каждой страницы в интернете есть уникальный веб-адрес, URL (это сокращение от английского Uniform Resource Locator, «унифицированный указатель ресурса»).URL Яндекса — https://yandex.ru/
. URL Яндекс.Практикума — https://praktikum.yandex.ru/
. На сайтах обычно есть множество страниц, и у каждой из них есть собственный URL. Например, https://yandex.ru/images/
на Яндексе или https://praktikum.yandex.ru/backend-developer/
на Практикуме. Адреса всех страниц начинаются с названия протокола (http
или https
) и имени домена. Например, адреса всех страниц Яндекса начинаются с https://yandex.ru/
, где имя домена — строка yandex.ru
.Когда разговор идёт о страницах определённого сайта, подразумевается, что говорят о страницах в пределах одного домена, так что не обязательно каждый раз упоминать доменное имя. URL, записанные без указания протокола и головного домена (backend-developer/
вместо https://praktikum.yandex.ru/backend-developer/
), называют относительными. Относительным адресом главной страницы любого сайта будет пустая строка. Это немного неожиданно, но логично: ведь в адресе главной страницы после имени домена ничего не пишется. Полные адреса (https://praktikum.yandex.ru/backend-developer/
) называются абсолютными.
Обработка запросов в Django
При создании проекта разработчик сам придумывает адреса, которые будут доступны на сайте. Эти адреса хранятся в коде; при запросе к проекту Django перебирает список адресов проекта, сравнивая их с запрошенным.Если запрошенный URL есть в списке, то вызывается связанная с этим адресом функция, которая обрабатывает запрос и делает всё необходимое: генерирует и отправляет пользователю запрошенную страницу, записывает данные в БД или делает что-то ещё хорошее.Если же запрошенный URL не обнаружился в списке заготовленных адресов, пользователю отправляется страница с сообщением об ошибке: 404, Page Not Found, «вы ошиблись адресом!».
Проектирование адресов
В качестве адресов могут выступать любые строки, но обычно используют английские слова в нижнем регистре, разделённые знаками -
или _
. Будет здорово, если по адресу пользователь сможет понять, какая информация содержится на странице.Заканчивайте адреса символом /
. Это соответствует соглашению о наименовании адресов Django. Списки адресов в Django хранят в переменной urlpatterns
в файлах urls.py. Основной файл urls.py лежит в главной папке проекта.Адреса в списке указывают в виде относительных URL.В проекте «Анфиса» будут такие адреса:
- Главная страница:
''
, пустая строка. - Список сортов мороженого:
'ice_cream/'
. - Подробная информация о мороженом номер 1:
'ice_cream/1/'
. - Подробная информация о мороженом номер 2:
'ice_cream/2/'
. - И так далее, для всех сортов мороженого в базе данных.
Однако перечислить в этом списке все страницы с сортами мороженого не получится: администратор проекта может добавлять или удалять такие страницы, в результате придётся каждый раз вручную править список адресов.Эта проблема решается просто: вместо конкретных URL можно применять маски адресов, в которых часть адреса заменена переменной (имя переменной указывается в треугольных скобках <>
).
...
'ice_cream/<pk>/',
# Такой адрес совпадёт с адресами
# ice_cream/1/
# ice_cream/48/
# и даже с адресом ice_cream/plombir/
...
Например, при запросе к URL ice_cream/859/
в переменную pk
будет записано значение 859
. Затем в функции-обработчике эту переменную можно использовать для получения запрошенного мороженого из базы данных.Таких переменных в шаблоне адреса может быть и несколько. Например, страница публикации определённого пользователя в блоге может содержать две переменные — username
автора и id
поста: users/<username>/<post_id>/
.
Связь urls с обработчиком
После того как Django найдёт совпадение запрошенного URL с шаблоном адреса из списка, должен быть вызван обработчик — функция или класс, в которых после обработки запроса будет подготовлен ответ.Начиная с версии Django 2 для связи URL и обработчика применяется функция path()
. Она принимает обязательные параметры path('route', view)
:
- route — шаблон обрабатываемого адреса, образец, с которым сравнивается полученный запрос;
- view — функция-обработчик: если запрошенный URL совпадает с route, вызов будет перенаправлен в указанную view-функцию (view-функции в Django хранят в файле views.py).
В проекте может быть много обрабатываемых путей, в них описаны все доступные адреса проекта. Все path()
хранят в файле urls.py в виде списка (обычно его называют urlpatterns
).
# Файл urls.py
...
urlpatterns = [
...
# Главная страница:
# если получен запрос без относительного адреса
# (то есть это запрос к имени домена, например anfisa.com),
# будет вызвана функция index() из файла views.py
path('', views.index),
# Страница со списком сортов мороженого:
# если получен запрос ice_cream/,
# будет вызвана функция ice_cream_list() из файла views.py
path('ice_cream/', views.ice_cream_list),
# Отдельная страница с информацией о сорте мороженого
# если получен запрос ice_cream/<любой_набор_символов>,
# будет вызвана функция ice_cream_detail() из файла views.py
path('ice_cream/<pk>/', views.ice_cream_detail),
]
Функциям-обработчикам нужно давать понятные имена. Из названия функции должно быть понятно, какой путь она обрабатывает.При получении запроса Django проходит по списку urlpatterns
сверху вниз, пока не найдёт совпадение запрошенного адреса с шаблоном адреса. При обнаружении совпадения будет вызвана соответствующая view-функция.
Разделение адресов по приложениям. Функция include()
Обычно Django-проект состоит из нескольких приложений, и каждое приложение обрабатывает свою часть запросов.Например, в проекте «Анфиса для друзей» помимо ice_cream могло бы быть ещё несколько приложений; это могут быть приложения news (отвечает за ленту новостей о мороженом), blog (дневник разработчиков проекта) и forum (площадка для общения друзей Анфисы).Адреса проекта лучше спроектировать так, чтобы они соответствовали приложениям:
- адреса
ice_cream/...
будут обрабатываться в приложении ice_cream, - адреса
news/...
— в приложении news.
Аналогично и с остальными приложениями.Хранить все шаблоны адресов всех приложений в одном файле будет неудобно: в какой-то момент список шаблонов разрастётся и станет нечитаемым. Хорошей стратегией будет разделить список urlpatterns
на части, в соответствии с приложениями, и хранить эти части в директориях приложений.Django даёт такую возможность: в каждом приложении можно создать собственный файл urls.py, в котором будут перечислены адреса, обрабатываемые именно этим приложением, а в корневом urls.py будут ссылки на эти файлы.Для создания таких ссылок используется функция include() (англ. «включить», «встроить»).Запросы к главной странице проекта «Анфиса для друзей» будем обрабатывать в urls.py приложения ice_cream:
# anfisa/urls.py (главный файл url проекта)
# По умолчанию в проект Django подключена система администрирования
from django.contrib import admin
# Функция include позволит использовать path() из других файлов.
# Импортируем!
from django.urls import include, path
urlpatterns = [
# Дорогой Джанго, если запрошена главная страница (''),
# перейди в файл urls приложения ice_cream и проверь там все пути
path('', include('ice_cream.urls')),
# Встроенная админка в Django подключена по этому адресу «из коробки»
path('admin/', admin.site.urls),
]
А файл urls.py приложения ice_cream будет выглядеть так:
# ice_cream/urls.py
from django.urls import path
from . import views
urlpatterns = [
# Главная страница
path('', views.index),
# Страница со списком сортов мороженого
path('ice_cream/', views.ice_cream_list),
# Отдельная страница с информацией о сорте мороженого
path('ice_cream/<pk>/', views.ice_cream_detail),
]
В результате таких действий проект будет готов работать с адресами ''
, 'ice_cream/'
и 'ice_cream/<pk>/'
.
Конвертеры пути
В адресе ice_cream/<pk>/
в аргументе pk
ожидается число, но пользователь может по ошибке или из любопытства вписать туда любую строчку: например, отправить запрос ice_cream/eskimo/
вместо ice_cream/15/
. Это нарушит нам весь фэншуй.Для профилактики такого несознательного поведения используют конвертеры пути (англ. path converters): перед именем переменной указывается тип ожидаемых данных: ice_cream/<int:pk>/
.Если в аргументе будут данные, не соответствующие конвертеру, Django будет считать, что шаблон адреса не совпадает с запросом, и продолжит поиск совпадений по urlpatterns
.Теперь ни один пользователь не сможет накормить обработчик неудобоваримой переменной.Вот список наиболее востребованных конвертеров:
str
— ожидает любую непустую строку, включая разделитель пути'/'
. Если в параметрах пути конвертер не указан явно, то именно конвертерstr
будет применён по умолчанию: шаблон адресаuser/<username>/
идентичен шаблонуuser/<str:username>/
.- Конвертер
int
ожидает в качестве значения ноль или любое целое положительное число. Синтаксис:ice_cream/<int:pk>/
. - Конвертер
slug
ожидает строку из букв и цифр, входящих в стандарт ASCII, а также символов-
и_
.Обычно слаг используют для создания человекочитаемых URL. При сравнении двух URL, в одном из которых аргумент — число, а в другом — slug, станет видна разница: по адресуgroup/cats/
безусловно ясно, что страница про котиков. А из адресаgroup/1/
не понятно вообще ничего, сплошь туманная неопределённость.Адреса с применением slug любят и люди, и поисковые роботы.
Можно создать и собственный конвертер пути. Например, конвертер, который будет принимать только четырёхзначное число и игнорировать любые другие параметры.
Конфликты адресов: кто выше — тот и главный
Django обрабатывает список urlpatterns
сверху вниз и вызывает обработчик из первого же path()
, в котором шаблон адреса совпадёт с запросом. Поэтому могут возникать коллизии.Например, при описанной ниже структуре списка view-функция just_page()
никогда не будет вызвана: первый шаблон будет перехватывать все запросы с числовыми аргументами, в том числе с аргументом 1
:
urlpatterns = [
path('page/<int:pk>/', views.material_by_id),
path('page/1/', views.just_page),
]
При планировании структуры адресов эту особенность стоит учитывать. Многих конфликтов можно избежать, если следовать правилу «адреса с частными случаями ставим выше, более общие шаблоны — ниже»: адрес page/1/
— это частный случай шаблона page/<int:pk>/
, значит, page/1/
надо поставить ближе к началу списка.