🐍🥤 Flask за час. Часть 1: создаем адаптивный сайт для GitHub Pages
Изучаем основные принципы работы фреймворка, разрабатывая личный сайт с резюме, портфолио, блогом и контактной формой.
Flask – микрофреймворк: это означает, что в стандартную поставку входит только самое необходимое, а все остальное при необходимости легко подключается. Поэтому приставка «микро» не должна вводить в заблуждение – при желании на Flask можно реализовать серьезный, масштабируемый проект. А для реализации таких небольших веб-приложений, как наш сайт, Flask подходит как нельзя лучше.
Обзор проекта
Готовый сайт находится здесь. У сайта несколько секций:
- Главная
- Резюме
- Портфолио
- Блог
- Контакты
Переключение между секциями создает иллюзию многостраничности, но в «живой» версии сайт – одностраничный. Бэкенд включает в себя модуль Frozen Flask, который превращает приложение в генератор статических сайтов (SSG): все страницы, необходимые для адекватного представления сайта в статической версии, создаются автоматически.
Фронтенд сделан на Bootstrap с несколькими дополнительными JS скриптами – например, записи в блоге фильтруются (без перезагрузки страницы) по тегам с помощью скрипта isotope.js
, при этом теги для фильтра скрипт получает из расширения Flask – FlatPages. Записи в блоге и карточки в портфолио можно перелистывать свайпом, без перезагрузки страницы. Bootstrap обеспечивает адаптивность: сайт одинаково хорошо смотрится на широкоформатном мониторе и на смартфоне.
Первый этап
На этом этапе мы установим Flask вместе со всеми нужными расширениями и зависимостями, напишем первый вариант кода для блога и сделаем два простейших шаблона.
Установка Flask
Сначала нужно создать папку для проекта и активировать виртуальное окружение:
mkdir flask_project cd flask_project mkdir .venv pipenv shell
Папка .venv – служебная: менеджер pipenv автоматически разместит все нужные зависимости там, и они не будут загромождать корневую директорию проекта. Виртуальное окружение активируется командой pipenv shell
, для выхода нужно выполнить exit
.
Установим Flask и все необходимые зависимости. Для этого сохраните этот список в файле requirements.txt:
Click==7.0 Flask==1.1.1 Flask-FlatPages==0.7.1 Frozen-Flask==0.15 itsdangerous==1.1.0 Jinja2==2.10.3 Markdown==3.1.1 MarkupSafe==1.1.1 Pygments==2.4.2 PyYAML==5.1.2 Werkzeug==0.16.0
Поместите файл в директорию проекта и выполните команду:
pipenv install -r requirements.txt
Для быстрого знакомства с принципами работы Flask мы сначала создадим тестовый блог, а затем перейдем к реализации нашего проекта.
Структура Flask проекта
Начнем с создания структуры проекта:
├── mysite.py ├── content │ └── posts │ ├── static └── templates
В папке content/posts будут размещаться Markdown файлы, в templates – шаблоны, в static – CSS стили, изображения и JS-скрипты. Весь код приложения мы напишем в файле mysite.py – сначала импортируем нужные модули, затем определим основные параметры, после пропишем маршруты к шаблонам и запустим сервер. Простейший вариант кода mysite.py выглядит так:
import sys from flask import Flask, render_template from flask_flatpages import FlatPages, pygments_style_defs from flask_frozen import Freezer DEBUG = True FLATPAGES_AUTO_RELOAD = DEBUG FLATPAGES_EXTENSION = '.md' FLATPAGES_ROOT = 'content' POST_DIR = 'posts' app = Flask(__name__) flatpages = FlatPages(app) freezer = Freezer(app) app.config.from_object(__name__) @app.route("/") def index(): posts = [p for p in flatpages if p.path.startswith(POST_DIR)] posts.sort(key=lambda item: item['date'], reverse=True) return render_template('index.html', posts=posts, bigheader=True) @app.route('/posts/<name>/') def post(name): path = '{}/{}'.format(POST_DIR, name) post = flatpages.get_or_404(path) return render_template('post.html', post=post) if __name__ == "__main__": if len(sys.argv) > 1 and sys.argv[1] == "build": freezer.freeze() else: app.run(host='127.0.0.1', port=8000, debug=True)
В это трудно поверить, но основной код блога действительно занимает всего 28 строк. Это возможно благодаря модулям FlatPages и Flask Frozen: первый избавляет от необходимости хранить посты в базе данных, проводит рендеринг Markdown-файлов в html, обеспечивает вывод записей и обращение к их свойствам, что будет заметно при создании шаблонов. Flask Frozen в действии мы увидим чуть позже: этот модуль берет на себя создание статической копии сайта – экспортирует html-файлы и все нужные ассеты (скрипты, CSS, изображения) в папку build.
Добавим в папку posts два-три тестовых поста – в YAML части обязательно должны быть метаданные title, date, description, потому что Jinja будет вызывать их в шаблонах. Markdown посты можно писать в любом редакторе – хоть в Блокноте, хоть в Sublime Text; можно обзавестись и специальным редактором – MarkdownPad для Windows, Mou для macOS.
Теперь создадим два простейших шаблона. Это код для index.html:
{% block content %} <h2>Блог - тестовый запуск</h2> {% for post in posts %} <small>{{ post.date }}</small> <p> <h3> {{ post.title }} </h3> <p> <i>{{ post.description }}</i> </p> <p>{{ post.html[:100]|safe }}</p> <a href="{{ url_for('post', name=post.path.replace('posts/', '')) }}"><span>Читать</span></a> </p> {% endfor %} {% endblock %}
А это шаблон для вывода записи на отдельной странице post.html:
{{ post.date }} {{ post.title }} {{ post.dеscription }} {{ post.html|safe }}
Оба шаблона мы доработаем на следующем этапе, а пока запустим приложение python mysite.py
и посмотрим на результат.
Весь код и тестовый контент для этого этапа есть здесь.
Второй этап
На этом этапе мы сделаем первые шаблоны и подключим файл с настройками.
Шаблонизатор Jinja2
Flask использует шаблонизатор Jinja2. Синтаксис Jinja2 идентичен шаблонизатору Django и напоминает Python. Если вам еще не приходилось работать с Django, на этом этапе достаточно знать, что логика в Jinja2 заключается в такие фигурные скобки {% %}
, а переменные – в такие {{ }}
.
Шаблон Jinja2 представляет собой обычный html-файл, в котором блоки с логикой и переменными размещаются в уже упомянутых скобках. К шаблону можно подключать любые JS-скрипты, иконки, шрифты. Большое количество переменных можно передать в шаблон в виде словаря:
@app.route("/") def index(): variables = {"title":"Это мой сайт", "description":"Разработчик, дизайнер, автор ИТ-курсов" "keywords":"Разработка на Python, курсы по Django" } return render_template('index.html', **variables)
В шаблоне index.html, в свою очередь, эти переменные можно вставить в нужные теги:
<title>{{ title }}</title> <meta content="{{ description }}" name="description"> <meta content="{{ keywords }}" name="keywords">
Если же переменных очень много, имеет смысл вынести словарь в отдельный файл – в дальнейшем мы воспользуемся именно этим вариантом.
Jinja2 поддерживает наследование и включение шаблонов – это позволяет разбить шаблон объемной страницы на несколько секций, которые проще редактировать по отдельности. Наш сайт состоит из одной страницы с несколькими разделами, которые целесообразно вынести в отдельные шаблоны:
- base.html
- index.html
- header.html
- resume.html
- counters.html
- skills.html
- interests.html
- portfolio.html
- card.html
- blog.html
- post.html
- contacts.html
Jinja2 не диктует каких-то жестких правил: при желании можно обойтись всего двумя шаблонами – index.html и post.html. И, конечно, можно не выносить переменные в отдельный файл, а вписать весь текст резюме и портфолио прямо в index.html. Но поддерживать сайт проще, если хранить всю потенциально изменяемую информацию в Markdown-файлах и текстовом файле конфигурации – в этом случае для изменения данных нужно будет внести поправки только один раз: переменные в шаблонах обеспечат обновление текста во всех разделах сайта. Кроме того, ненужные разделы сайта очень просто удалить, если они находятся в отдельных шаблонах.
Первый шаблон, который мы создадим – base.html. Он будет получать переменные из бэкенда. Для передачи данных в шаблон создайте файл settings.txt и сохраните в нем словарь:
{"site_url":"http://localhost:8000", "site_title": "John Doe: Python разработчик и автор контента", "description": "Джон Доу - портфолио, резюме и блог Python разработчика", "keywords": "Веб-разработка на Python, бэкенд на Django и Flask"}
Теперь добавьте импорт json
и загрузку данных из файла в mysite.py:
@app.route("/") def index(): posts = [p for p in flatpages if p.path.startswith(POST_DIR)] posts.sort(key=lambda item: item['date'], reverse=True) with open('settings.txt', encoding='utf8') as config: data = config.read() settings = json.loads(data) return render_template('index.html', posts=posts, bigheader=True, **settings)
Сохраните этот код в templates/base.html:
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="utf-8"> <meta content="width=device-width, initial-scale=1.0" name="viewport"> <title>{{ site_title }}</title> <meta content="{{ description }}" name="description"> <meta content="{{ keywords }}" name="keywords"> <link href="{{ url_for('static', filename='img/favicon.png') }}" rel="icon"> <link href="https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,600,600i,700,700i|Raleway:300,300i,400,400i,500,500i,600,600i,700,700i|Poppins:300,300i,400,400i,500,500i,600,600i,700,700i" rel="stylesheet"> <link href="{{ url_for('static', filename='assets/bootstrap/css/bootstrap.min.css') }}" rel="stylesheet"> <link href="{{ url_for('static', filename='assets/bootstrap-icons/bootstrap-icons.css') }}" rel="stylesheet"> <link href="{{ url_for('static', filename='assets/boxicons/css/boxicons.min.css') }}" rel="stylesheet"> <link href="{{ url_for('static', filename='assets/glightbox/css/glightbox.min.css') }}" rel="stylesheet"> <link href="{{ url_for('static', filename='assets/remixicon/remixicon.css') }}" rel="stylesheet"> <link href="{{ url_for('static', filename='assets/swiper/swiper-bundle.min.css') }}" rel="stylesheet"> <script src="https://kit.fontawesome.com/69e2443572.js" crossorigin="anonymous"></script> <link href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet"> </head> <body> <main id="main"> {% block content %} <!-- вложенные шаблоны --> {% endblock %} </main> <!-- Скрипты --> <script src="{{ url_for('static', filename='assets/purecounter/purecounter.js') }}"></script> <script src="{{ url_for('static', filename='assets/bootstrap/js/bootstrap.bundle.min.js') }}"></script> <script src="{{ url_for('static', filename='assets/glightbox/js/glightbox.min.js') }}"></script> <script src="{{ url_for('static', filename='assets/isotope-layout/isotope.pkgd.min.js') }}"></script> <script src="{{ url_for('static', filename='assets/swiper/swiper-bundle.min.js') }}"></script> <script src="{{ url_for('static', filename='assets/waypoints/noframework.waypoints.js') }}"></script> <script src="{{ url_for('static', filename='js/main.js') }}"></script> </body> </html>
А этот – в templates/index.html:
{% extends "base.html" %} {% block content %} <h1>Здесь будет сайт</h1> {% endblock %}
Сохраните в папке static все эти ассеты. Теперь можно запускать сервер python mysite.py
– скелет сайта готов:
Перейдем к созданию первого шаблона, расширяющего index.html – header.html. Добавьте переменные в файл settings.txt:
"site_url":"http://localhost:8000", "tag1":"Разработчик и автор", "tag2":"курсов по Django", "sect1":"Главная", "sect2":"Резюме", "sect3":"Портфолио", "sect4":"Блог", "sect5":"Контакты", "telegram":"https://t.me/johndoe", "facebook":"ttps://facebook.com/john.doe", "vk":"https://vk.com/john_doe", "email":"mailto:john_doe@gmail.com"
И отредактируйте файл index.html – теперь он будет включать в себя header.html с помощью include
:
{% extends "base.html" %} {% block content %} {% block header %} {% include "header.html" %} {% endblock %} {% endblock %}
Перезагрузите страницу:
Весь код и контент для этого этапа – здесь. Во второй части туториала мы завершим работу над приложением и загрузим статическую копию сайта на GitHub Pages.