Маршрутизация во Flask, декоратор route
Прежде чем приступим к решению второй из проблем, обозначенных в конце первого урока, отметим для себя следующее. Когда в браузере вводится адрес несуществующей страницы, например, http://127.0.0.1:5000/animals , то в качестве ответа сервер возвращает технический код 404, который говорит о том, что страница не найдена. Также передает свой стандартный html-документ, чтобы отобразить сообщение об этом в браузере.
Далее, когда в нашей программе аргумент декоратора route() будет определяться динамически (то есть в процессе выполнения приложения), мы уже не получим ответ 404 при обращении к несуществующей странице. Вместо этого ошибка будет другого рода, и для ее решения программу придется немного усложнить.
Итак, обычно сайт состоит из множества страниц. Вернемся к нашему представлению page (мы убрали второй аргумент из вызова render_template
, чтобы не отвлекал внимание):
@app.route('/plants') def page(): return render_template('plants.html')
Заметим, что адрес в декораторе и имя файла по большому счету совпадают. Не обязательно, чтобы это было так: адрес может быть одним, а файловая структура проекта и непосредственно имена шаблонов ‒ другими. Однако в нашем случае если URL содержит /plants, отображается шаблон-документ plants.html. В случае /animal рендерим animal.html. Для /evolution отдаем evolution.html.
Чтобы не множить в программе однотипные представления, фреймворк Flask позволяет передавать в тело функции путь, содержащийся в route()
, или его часть. Делается это так:
- В аргументе роута используется переменная, которой фреймворк присваивает часть URL. Чтобы отделить другие части адреса от переменной, последнюю заключают в угловые скобки.
- Эта же переменная передается в качестве аргумента в связанную функцию-представление.
- Как использовать полученное значение, дело самой функции. В нашем случае ‒ значение является частью имени файла-шаблона.
@app.route('/<file_name>') def page(file_name): return render_template(f'{file_name}.html')
Теперь, если у вас есть файлы plants.html, animal.html и evolution.html, все они будут отображаться с помощью одного и того же представления. К сожалению, эта функция будет вызываться при обращении к любому URL типа "http://домен_сайта/слово".
Когда Jinja не находит шаблон, то генерирует исключение TemplateNotFound
. В браузере вы увидите сообщение об этом и трассировку. Одним из способов решения данной проблемы может быть перехват исключения с помощью инструкции try-except
языка Python. Обработать исключение можно по-разному, обычно генерируют ответ 404 с помощью функции abort
модуля werkzeug.exceptions
.
... from jinja2.exceptions import TemplateNotFound from werkzeug.exceptions import abort ... @app.route('/<file_name>') def page(file_name): try: return render_template(f'{file_name}.html') except TemplateNotFound: abort(404)
Werkzeug, как и Jinja, устанавливается вместе с фреймворком Flask и представляет собой библиотеку инструментов для WSGI-приложений. В свою очередь WSGI ‒ это стандарт или протокол, который обеспечивает взаимодействие python-программы и веб-сервера.
Основная причина, по которой не следует оставлять ошибку TemplateNotFound
заключается в том, что если страница была запрошена поисковым роботом, то он может ее проиндексировать (будет считать обычной страницей сайта). А поскольку на ней нет ценной информации, это ухудшит позиции сайта в поисковой выдаче. Поэтому, отдавая роботу 404, сервер сообщает, что индексировать тут нечего.
В ряде случае вместо "Not Found" может потребоваться редирект ‒ перенаправление на другую страницу. В этом случае используется функция redirect
, которую предварительно надо импортировать из модуля flask
. Например,
return redirect('/plants', code=301)
или
return redirect(url_for('index'))
Во втором примере используется функция url_for
(из модуля flask
). Она перенаправляет на адрес, который привязан к функции index()
. В нашем случае ‒ на главную страницу.
301 ‒ это код перманентного (постоянного) редиректа. Например, вы изменили адрес страницы в пределах сайта, а изменить ссылки на нее из внешних источников не можете. Код 302 обозначает временный редирект (допустим, на другой сайт). Этот код используется функцией redirect
по-умолчанию.
Вернемся к декоратору route
. Если в адресе используется переменная, то у нее кроме имени есть еще так называемый конвертер (преобразователь). Его можно не указывать, как в нашем примере.
@app.route('/<file_name>')
В этом случае используется конвертер по-умолчанию, который преобразует значение переменной в строку. В полном варианте это выглядит так:
@app.route('/<string:file_name>')
В обоих случаях переменная может принимать любое строковое значение (числа будут преобразованы в строку), но оно не должно содержать слэшей. Чтобы увидеть разницу, временно избавимся от try-except
и вернемся к простому варианту представления page:
@app.route('/<file_name>') def page(file_name): return render_template(f'{file_name}.html')
Если сейчас в браузере ввести адрес http://127.0.0.1:5000/dsfafa, не имея в каталоге templates файла dsfafa.html, будет выброшено исключение TemplateNotFound
. Потому что page()
была вызвана, а render_template()
не нашла соответствующий шаблон.
Однако если адрес будет, например, таким: http://127.0.0.1:5000/ru/dsfafa, получим ответ 404, так как никакое представление в программе не обрабатывает подобные URL. В таких случаях фреймворк по-умолчанию сообщает серверу, что он должен вернуть "Страница не найдена".
В принципе нас все устраивает. Если несуществующая страница указана через один слэш, генерируем 404 с помощью abort()
. Если адрес сложнее, сам фреймворк сделает всю работу.
Теперь представим, что сайт состоит из разделов, и мы хотим, чтобы адрес содержал в себе информацию, к какому разделу принадлежит открываемая страница. Также, чтобы нам было проще ориентироваться, файловая структура в каталоге templates тоже не будет одноуровневой. Пусть контент о растениях хранится в каталоге botany, о животных ‒ в zoology и т. п.
Таким образом, на сайте будут адреса такого плана: /botany/plants, /botany/algae, /zoology/worms, /zoology/birds и т. п.
Конечно, мы можем создать для каждого раздела свое представление:
@app.route('/botany/<file_name>') def botany_page(file_name): try: return render_template(f'botany/{file_name}.html') except TemplateNotFound: return abort(404) @app.route('/zoology/<file_name>') def zoology_page(file_name): try: return render_template(f'zoology/{file_name}.html') except TemplateNotFound: return abort(404)
Заметим, что в функцию render_template
на самом деле передается не имя файла, а его местонахождение в каталоге templates (по умолчанию). В данном случае папки botany и zoology находятся в нем, а уже в них ‒ файлы-шаблоны.
Если разделов много, а функции почти одинаковые, решение представленное выше не подходит. Тогда почему бы не использовать две переменные. С помощью одной передавать раздел, с помощью другой ‒ имя файла:
@app.route('/<section>/<file_name>') def page(section, file_name): try: return render_template(f'{section}/{file_name}.html') except TemplateNotFound: return abort(404)
Однако в нашем случае и этот вариант избыточен, так как мы никак отдельно не обрабатываем значение переменной section. Хорошо бы вернуться к варианту @app.route('/<file_name>')
, но таким образом, чтобы file_name захватывал адрес целиком, со слэшами внутри.
Выше мы упоминали о конвертерах в route()
. Если тип переменной указывается как path
, то в отличие от варианта по-умолчанию она может содержать слэши.
@app.route('/<path:file_name>') def page(file_name): try: return render_template(f'{file_name}.html') except TemplateNotFound: return abort(404)
Здесь file_name может принимать значения botany/plants, zoology/dfdf/animals и т. п.
Другими типами конвертеров являются int
, float
и uuid
. Так представление, связанное с маршрутом @app.route('/blog/<int:article>')
, будет обрабатывать запросы по адресам /blog/1, /blog/135, но не /blog/13w.