Генерация страниц статичного сайта с помощью Flask-приложения
Это ключевой урок третьего раздела курса.
По большому счету речь пойдет о записи строки, возвращаемой функцией render_template()
, в файл, а не только передачи ее веб-серверу из функции-представления. Такие готовые файлы HTML будут потом загружены на удаленный VPS. Само приложение на базе фреймворка Flask используется лишь для создания и обслуживания сайта.
Протестируем идею на примере главной страницы. Пусть при ее запросе вместе с возвратом результата render_template()
из функции-представления index()
также создается файл index.html:
@app.route('/') def index(): s = render_template('base.html') with open('static/index.html', 'w') as f: f.write(s) return s
Теперь, загрузив/обновив главную страницу в браузере, получим новый файл в каталоге static. Его можно открыть в браузере, но ни стили, ни скрипты работать не будут, так как в коде HTML была использована абсолютная адресация. Запустим отдельный простой веб-сервер из каталога static:
$ cd static/ $ python3 -m http.server
По адресу http://0.0.0.0:8000/ откроется страница index.html.
Очевидно, чтобы получить статичные страницы для всех статей сайта, надо извлечь все записи таблицы статей из базы данных и для каждой вызвать render_template()
с последующей записью результата в файл. Однако перед этим надо подумать, как решить пару небольших проблем.
Во-первых, в шаблонах нашего flask-приложения содержатся ссылки на страницу редактирования статьи и создание новой (а также блок для вывода flask-сообщений). Этого кода HTML в статичных страницах быть не должно. Чтобы удалить его оттуда, будем передавать дополнительный аргумент при вызове render_template()
, а сам html-код поместим в условный оператор jinja.
В файле page.html:
{% if not del_buttons %} <div style="background-color:rgba(57,102,95,0.14);height:30px;padding:5px 10px;"> <a href="{{url_for('edit_page', file_name=address[1:])}}" style="float:left;">Править эту статью</a> <a href="{{url_for('create')}}" style="float:right;">Создать новую</a> </div> {% endif %}
В файле base.html:
{% if not del_buttons %} <style> .error { color: red; text-align: center; } .success { color: green; text-align: center; } </style> {% for category, message in get_flashed_messages(with_categories=true) %} <p class="{{ category }}">{{ message }}</p> {% endfor %} {% endif %}
Внесем изменения в функцию index, выполнив два вызова render_template()
. Один ‒ для записи в файл, другой ‒ для передачи веб-серверу. (На главной странице flask-сообщения выводятся при удалении статьи, когда происходит перенаправление сюда.)
@app.route('/') def index(): with open('static/index.html', 'w') as f: f.write(render_template('base.html', del_buttons=True)) return render_template('base.html')
Если мы не планируем перезаписывать статичный файл главной при каждом обращении к ней, то имеет смысл закомментировать оператор with с его телом.
Вторая проблема ‒ как в Python создавать деревья каталогов. Так, если есть статья с путем /botany/plants, то в нашем каталоге static должен быть каталог botany, а уже в нем будет создан файл plants.html.
Если сайт большой, со сложной файловой структурой, создавать подкаталоги вручную неудобно. Вместо этого воспользуемся классом Path
модуля pathlib
. С помощью его метода mkdir
можно создать всю последовательность каталогов, в самый вложенный из которых будет потом записываться файл.
Каждая статья в базе данных имеет поле path, в которой хранится ее путь в виде строки. Если мы создаем для статьи статичный файл, то имя файла ‒ это подстрока пути, находящаяся после последнего слэша. Чтобы отделить дерево каталогов от имени файла, воспользуемся строковым методом rpartition
. Пояснительный пример:
>>> path = 'zoology/protozoa/amoeba' >>> folders_and_file = path.rpartition('/') >>> folders_and_file ('zoology/protozoa', '/', 'amoeba') >>> folders = folders_and_file[0] >>> folders 'zoology/protozoa'
Далее передадим дерево каталогов в конструктор Path
:
>>> from pathlib import Path >>> path_object = Path(folders)
Вызовем метод mkdir
:
>>> path_object.mkdir(parents=True, exist_ok=True)
Параметр parents
позволяет создавать родительские каталоги, если их нет; exist_ok
‒ не выбрасывать ошибку, если каталоги уже были созданы ранее.
Теперь мы готовы написать функцию make_site, генерирующую сразу все страницы сайта:
def make_site(): with open(f'static/index.html', 'w') as f: f.write(render_template('base.html', del_buttons=True)) c = sqlite3.connect(db_name) c.row_factory = sqlite3.Row articles = c.execute('SELECT * FROM articles') for i in articles: s = render_template('page.html', address='/' + i['path'], title=i['h1'], desc=i['short_desc'], body=Markup(i['body']), del_buttons=True) Path(f"static/{i['path'].rpartition('/')[0]}" ).mkdir(parents=True, exist_ok=True) f = open(f"static/{i['path']}.html", 'w') f.write(s) f.close() c.close()
В ней получаем все статьи из базы данных, каждую передаем в шаблон, полученный оттуда код HTML записываем в файл.
Вызовем make_site()
из функции index (вызов должен быть до оператора return
) и обновим в браузере главную страницу. В каталоге static должны появиться каталоги и страницы. После этого вызов следует закомментировать до следующего случая, когда понадобится снова сгенерировать статичный сайт целиком.
Если сейчас запустить простейший веб-сервер из каталога static (см. выше), можно, переходя по ссылкам, открыть любую страницу сайта, если в конце каждого адреса дописывать .html. Чтобы "скрывать" расширения, программный веб-сервер надо настроить, то есть прописать соответствующие инструкции. Как это можно сделать, показано в следующей статье. О том, как избавиться от расширений на веб-сервере Apache, упомянем, когда речь пойдет о переносе сайта на VPS.
Вернемся к вопросу генерации статичных веб-страниц. По большому счету можно остановиться на том, что уже есть. Мы можем вызывать функцию make_site()
всякий раз, когда на сайте что-то меняется. При этом будут перезаписываться все страницы. Во многих случаях нам так и надо, так как добавление новой статьи, изменение заголовка любой статьи, ее адреса, принадлежности к разделу меняет и меню. А так как оно включено в каждую страницу, требуется перезапись всех страниц.
Однако, когда правится тело статьи, то меню не меняется, и перезаписывать кучу файлов излишне. Кроме того, хорошо бы как-то автоматизировать процесс, чтобы программа сама определяла, когда ей перезаписать один файл, а когда все.
Начнем с простого. Определим все места в программе, куда следует вставить вызов make_site()
, то есть выполнять генерацию всех страниц:
- Когда создается новая страница, то есть в функцию create (в этом случае добавляется ссылка в меню).
- В функцию edit_page в места обновления заголовка, изменения адреса, раздела меню, позиции в нем. Места вставки легко определить: вызов
make_site()
должен идти сразу заmenu_update()
. - Также вызов должен быть в функции delete, так как при удалении страницы ссылка на нее из меню удаляется.
Дополнительно, в функции delete следует предусмотреть удаление файла статьи из каталога static:
import os ... def delete(address): ... menu_update() try: os.remove(f'static/{address}.html') except FileNotFoundError: pass make_site()
Конструкцию try-except также надо вставить туда, где у страницы меняется адрес. Потому что появится документ с новым именем, а старый должен быть удален.
try: os.remove(f'static/{file_name}.html') except FileNotFoundError: pass update('path', 'path') flash('Адрес страницы изменен!', 'success')
Что касается генерации всего одной статичной страницы, то данное действие должно происходить в местах, когда меняется тело статьи или ее краткое описание.
if request.form['article'] != p['body']: updated = update('body', 'article') # здесь должна быть запись в файл одной страницы if request.form['description'] != p['short_desc']: updated = update('short_desc', 'description') # здесь должна быть запись в файл одной страницы
Однако если правится и то, и то, мы два раза перезапишем один и тот же файл. Чтобы не допустить лишних действий, определим переменную changed_one_page, в соответствующих местах присвоим ей True
, и уже перед выходом из функции перезапишем файл:
... if request.method == 'POST': updated = False success = True changed_one_page = False ... if request.form['article'] != p['body']: updated = update('body', 'article') changed_one_page = True if request.form['description'] != p['short_desc']: updated = update('short_desc', 'description') changed_one_page = True ... if success: if changed_one_page: make_one_page(file_name) return redirect(url_for('page', file_name=file_name))
Функция make_one_page:
def make_one_page(path): c = sqlite3.connect(db_name) c.row_factory = sqlite3.Row p = c.execute('SELECT path, h1, short_desc, body \ FROM articles WHERE path=?', (path,)).fetchone() c.close() s = render_template('page.html', address='/' + p['path'], title=p['h1'], desc=p['short_desc'], body=Markup(p['body']), del_buttons=True) Path(f"static/{path.rpartition('/')[0]}").mkdir( parents=True, exist_ok=True) f = open(f'static/{path}.html', 'w') f.write(s) f.close()
Мы можем продолжить оптимизацию (если сложно, можно этого не делать). Все должно работать и так.
Теперь представьте, что по ходу действия (например, в результате изменения текста ссылки в меню) были перезаписаны все страницы сайта. Также мы внесли изменения в тело статьи, чем изменили значение changed_one_page на True
. Но нет необходимости теперь перезаписывать одну страницу. Значит, надо проверять, выполнялась ли до этого функция make_site()
или нет.
Кроме того, и самое страшное, в ряде случаев make_site()
и menu_update()
могут выполняться по несколько раз. Например, на странице редактирования статьи вы изменили ее заголовок и положение в разделе меню. Сначала эта пара функций будет вызвана при обработке изменения заголовка, потом ‒ позиции.
К сожалению, все выглядит так, что в edit_page придется реализовать больше разных условий и зависимостей. Возможно лучше будет найти какой-то другой подход.
Обратим внимание на моменты выхода из функции ‒ операторы return
. Выход с результатом рендера шаблона create.html нас не волнует, так как это значит, что при заполнении формы была допущена некорректность, и страница снова будет открыта на редактирование.
Но при редиректах на page запись статичных страниц и меню может происходить. Почему бы не выполнять эти действия в функции page, а не edit_page. Ведь Flask позволяет передавать в функции-представления дополнительные аргументы.
Удалим код:
if changed_one_page: make_one_page(file_name)
Дополнительно определим переменную changed_all_pages:
changed_one_page = False changed_all_pages = False
В функции edit_page вместо вызовов menu_update()
и make_site()
будем менять значение переменной:
changed_all_pages = True
В редиректы на page добавим дополнительные аргументы:
return redirect(url_for('page', file_name=request.form['path'], write_all=changed_all_pages)) if success: return redirect(url_for('page', file_name=file_name, write_all=changed_all_pages, write_one=changed_one_page ))
В теле page получим значение этих аргументов и в зависимости от них вызовем функции menu_update()
, make_site()
или make_one_page()
:
if p: write_all = request.args.get('write_all') # возвращает строку write_one = request.args.get('write_one') if write_all == 'True': menu_update() make_site() elif write_one == 'True': make_one_page(p) return render_template('page.html', address='/' + p['path'], title=p['h1'], desc=p['short_desc'], body=Markup(p['body']))
В функции make_one_page незачем извлекать данные из базы данных. Они такие же как уже были получены в page. Поэтому мы можем передать запись. Преобразуем вызов из page:
make_one_page(p)
Функция make_one_page:
def make_one_page(p): s = render_template('page.html', address='/' + p['path'], title=p['h1'], desc=p['short_desc'], body=Markup(p['body']), del_buttons=True) Path(f"static/{p['path'].rpartition('/')[0]}").mkdir( parents=True, exist_ok=True) f = open(f'static/{p["path"]}.html', 'w') f.write(s) f.close()