Это переиздание из моего основного блога tryexceptpass.

Я провожу большую часть дня, каждый день, по колено в коде. Оптимизация, построение, исправление и продумывание рабочих процессов могут быть утомительными. Это означает, что последнее, чем я хочу заниматься по возвращении домой, - это заниматься дополнительным программированием. Но мне также нравится узнавать что-то новое и делиться своим опытом, чтобы они могли помочь другим. Я делаю это через сообщения на этом сайте.

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

Существует множество сервисов, которые помогают объединить эти функции. Некоторые платные, а некоторые «бесплатные», но выбрать их не так-то просто. С бесплатными решениями вы можете либо разместить систему самостоятельно, чего вы хотели избежать, либо использовать популярный сервис, который сделает это за вас, включая рекламу (например, disqus).

Мое личное мнение состоит в том, что если кто-то рекламирует на моем сайте, то это буду я - или, по крайней мере, с помощью механизма, в котором последнее слово остается за мной - в противном случае нет никакого контроля над тем, что видят пользователи. Мне не нужна система, которая собирает все виды данных от моих пользователей. Мне нужен такой, для которого даже не нужны учетные записи пользователей. Мне не хотелось бы видеть какую-нибудь случайную рекламу отеля на Ибице, пока я веду технический блог, просто потому, что вчера вечером я гуглил о каникулах на Средиземном море после того, как улеглась усталость.

Решение, не требующее особого обслуживания

Предполагая, что мы хотим создать систему, которая требует некоторой серверной части данных, такой как раздел комментариев, нам понадобится место для хранения записей и способ их получения. Базы данных не могут быть частью решения, потому что мы не хотим ими управлять, и нам действительно не нужно ничего особенного, даже индексирование. Все, что мне нужно, это простой список записей с 3 или 4 полями на запись. Подойдет простой файл, но мы хотим отслеживать историю и иметь какой-то механизм резервного копирования. Последнюю часть мы можем получить из системы контроля версий, такой как git.

В наши дни существует множество способов хранения структурированных данных. Самый универсальный со встроенными библиотеками практически для всех языков - это JSON. Другие, такие как YAML - я действительно начал по этому пути - и TOML может упростить добавление в файл, но универсальность JSON в сети делает его более актуальным в этой ситуации.

Хранение комментариев в файле JSON не ново и не изменилось. Что это за предлагаемое решение, не требующее особого обслуживания? Вставьте его в GitHub, используйте JavaScript для чтения и добавьте немного AWS Lambda, чтобы внести в него изменения. Да, вам все равно придется написать небольшой код, но это несложно, и как только вы его начнете, он практически не требует обслуживания.

Настройка GitHub

Сначала вам нужно выбрать, хранить ли вашу информацию в частном или публичном репозитории. Я выбрал частное репо, потому что у меня есть для него учетная запись, и мне нужна была возможность скрыть любые базовые файлы от равнодушных пользователей интернета. Однако описанный ниже метод использует страницы GitHub, которые подходят для обоих.

GitHub Pages - это функция, которая позволяет обычному репозиторию размещать статические файлы в Интернете. Файлы расположены в определенной ветке, обычно gh-pages. Это отлично подходит для статических генераторов, потому что вы отслеживаете файлы конфигурации и содержимого в ветке master, а затем запускаете сборку, которая фиксирует сгенерированный HTML в ветке gh-pages. Таким образом автоматически сгенерированные файлы хранятся отдельно от фактического содержимого. Через несколько минут после фиксации обновленный веб-сайт будет доступен по URL-адресу, назначенному GitHub (если вы не используете DNS).

Настроить это очень просто: * Определите или создайте ветку, в которой вы будете размещать файлы (по умолчанию gh-pages). * Зайдите в настройки репозитория GitHub и прокрутите вниз до раздела GitHub Pages. * Выберите ветку в разделе Source * Настройте DNS в разделе Custom domain, если необходимо.

GitHub теперь готов разместить ваши файлы. Проверьте это, зафиксировав index.html страницу с текстом «привет, мир» и загрузив ее в свой браузер с URL-адресом, который вам дал GitHub.

Чтение комментариев

А пока предположим, что у нас уже есть comments.json файл со следующей структурой, добавленной в ветку gh-pages:

[
    {
        "user": "ABCDEFG",
        "comment": "12345"
    }
]

Вы можете отредактировать генератор своего сайта, чтобы он включал код JavaScript, который выполняет HTTP-запрос GET к URL-адресу, на котором размещен этот файл. Есть сотни способов сделать это, большинство из которых использует сторонние библиотеки (например, jQuery). Если вы не хотите еще больше усложнять дерево зависимостей, вот несколько примеров JavaScript, который делает это с помощью базового объекта XmlHttpRequest:

divcomments = document.getElementById("#div_for_adding_comments");
xhr.open('GET', "https://your_user.github.io/ur_repo/comments.json")
xhr.onreadystatechange = function(data, err) {
    if (xhr.readyState === XMLHttpRequest.DONE) {
        if (xhr.status > 200) {
            // There are no comments
            divcomments.innerHTML = '<p>Be the first to comment on this article.</p>';
        }
        else {
            // Parse the comments into an object that we can iterate
            var data = JSON.parse(xhr.response);
            // Iterate through the comments and add them to the div
            for (var i=0; i < data.length; i++) {
                divcomments.innerHTML = divcomments.innerHTML + 
                '<blockquote><p>' + data[i]['comment'] + 
                '</p></blockquote>';
            }
        }
    }
}

Обратите внимание, что все ответы я разбираю как JSON. Это дает нам полный объект JavaScript для обновления элемента HTML, уже находящегося на странице.

Однако у нашего плана есть изъян. Поле comment в нашей структуре данных представляет собой обычный текст. Это означает, что если кто-то использует в своем сообщении специальные символы, это может привести к разрыву всего раздела. Обычно вам нужно экранировать строку, прежде чем помещать ее в поле, но наиболее гибким решением является кодирование base64. В JavaScript есть для этого встроенные функции: atob() и btoa().

Добавление комментариев с помощью AWS Lambda

Я давно искал причину использовать AWS Lambda. Это сервис Amazon, который позволяет связать выполнение функции с event. Функции могут быть написаны на нескольких языках, одним из которых является python 3.6, и они выполняются внутри контейнеров докеров. События - это внутренние триггеры из различных систем AWS, включая сервис API Gateway. Это означает, что вы, по сути, сопоставляете функцию с URL-адресом, предоставленным вам службой.

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

Чтобы начать работу, достаточно просто зайти на сайт Lambda и создать новую функцию на основе примера hello world в Python по умолчанию. * Щелкните Create Function. * Выберите Blueprints. * Найдите hello-world-python3. * Дайте ему имя и введите изменения кода. * Функция-обработчик получает два параметра: event и context. event - это словарь с полем body, содержащим тело HTTP-запроса. Если вы используете HTML-форму для отправки комментариев, именно здесь вы найдете закодированные данные. * Анализ event['body'] достаточно прост, используя urllib для декодирования значений:

from urllib.parse import unquote_plus
params = [param.split('=') for param in event['body'].split('&')]
params = {k: unquote_plus(v) for k, v in params}

Лямбда-функции подключаются к системе Amazon CloudWatch, поэтому вы сможете просматривать вывод любых print() операторов, войдя в службу CloudWatch.

Теперь давайте вызовем GitHub REST API, чтобы получить текущий список комментариев, но сделаем это без использования сторонних библиотек. Это сохраняет функцию минимальной, в противном случае вы выполняете другой процесс, чтобы добавить эти библиотеки в контейнер, в котором выполняется функция. Lambda также взимает плату за гигабайт-часы использования. Чем больше контейнер, тем выше будет счет за сдачу. Это означает, что мы застряли на старых добрых urllib.urlopen и urllib.Request.

Документация по API, который управляет файлами в репозитории GitHub, доступна здесь. Вы можете использовать базовую аутентификацию HTTP с личным токеном доступа, полученным в настройках пользователя GitHub, для входа в систему. Просто добавить токен в HTTP-заголовки запроса и указать его на правильную ветку и файл:

from urllib import urlopen, Request
from base64 import b64encode, b64decode
headers = {
    'content-type':'application/json',
    'authorization': f"Basic {str(b64encode(bytes(
        'username:access_token', 'utf-8'
    )), 'utf-8')}"
}
resp = urlopen(Request(f"https://api.github.com/repos/{github_user}/{github_repo}/contents/{filename}.json?ref=gh-pages"))

Ответ API содержит файл в кодировке base64 в поле content, поэтому вам нужно b64decode(resp['content'], 'utf-8'), чтобы получить свои комментарии, а затем использовать json.loads(), чтобы вставить его в словарь, который легко изменить. Он также дает нам SHA-хэш последнего коммита в этой ветке, который является информацией, необходимой для следующего шага: изменения файла.

Фиксация изменения также проста, за исключением того, что нам нужно передать метаданные, описывающие изменения, вместе с новым содержимым файла в кодировке base64 (полностью).

data = {
    'message': 'A commit message to figure out what happened',
    'branch': 'gh-pages',
    'sha': sha_value_from_previous_GET_response,
    'content': str(b64encode(bytes(
        json.dumps(updated_comments), 'utf-8'
    )), 'utf-8')
}

Это кодирование и декодирование немного некрасиво. Мы сериализуем обновленные комментарии обратно в JSON. Но поскольку нам нужно закодировать их в base64, чтобы передать в GitHub, мы вызываем b64encode(). Эта функция принимает байты, а не простые строки, поэтому мы должны преобразовать сериализованное содержимое в байты. Вдобавок к этому результат b64encode() также состоит из байтов, что означает, что нам нужно преобразовать их обратно в строку utf-8, прежде чем вставлять их в словарь, который мы будем отправлять вместе с нашим HTTP-запросом. Фух!

HTTP-запрос к GitHub - это PUT, и аутентификация такая же, как и раньше, но urllib принимает данные в байтовых строках (да, это снова!). Вызов теперь выглядит так:

urlopen(Request(
    f"https://api.github.com/repos/{github_user}/{github_repo}/contents/{filename}.json",
    headers=headers,
    data=bytes(json.dumps(data), 'utf-8'),
    method='PUT'
))

Вот и все. После завершения запроса ветка gh-pages обновится новым коммитом, содержащим сообщение, которое вы только что передали, и новый файл станет общедоступным. Это не самое быстрое решение, но пользователь увидит, что его комментарий добавлен в список, и у вас будет история фиксации всех изменений с течением времени. Вы также можете добавить интеграцию электронной почты с GitHub, чтобы получать уведомление о фиксации.

Запуск через API Gateway

Как упоминалось ранее, Lambda может запускать ряд вещей в экосистеме AWS, некоторые из которых являются внутренними сервисами, другие - внешними. Чтобы сделать вашу функцию доступной через общедоступный Интернет, вы должны подключить ее к API Gateway. В первый раз это может немного сбить с толку, так что вот некоторая информация по этому поводу.

После создания API со службой шлюза добавьте URL-путь и метод, по которому вы хотите маршрутизировать функцию. Можно использовать /, я решил сделать это на конкретном ресурсе типа /comments. Для этого щелкните раскрывающийся список Actions и выберите Create resource, затем введите имя и путь, которые вы выбрали.

Я рекомендую также установить флажок Enable API Gateway CORS, который предварительно настроит ответ на запрос OPTIONS. Если вы решите использовать чистый JavaScript для вызова API (а не отправки форм), вы можете указать заголовки CORS, которые браузеры будут искать в предполетных запросах, отправляемых перед фактическими запросами API. Выполните редактирование, щелкнув Integration Response после выбора OPTIONS созданного метода.

У нас есть конечная точка шлюза и путь к ресурсу, теперь мы добавляем метод, вызывающий нашу лямбда-функцию, в базовый ресурс: POST. Сделайте это, выбрав путь к ресурсу, щелкнув раскрывающееся меню Actions и выбрав Create Method. Выберите метод POST и используйте Lambda Function для поля Integration Type, затем выберите регион AWS, в котором существует функция, и введите имя функции в текстовое поле Lambda Function.

Панель управления шлюза API предоставляет URL-адрес для этой лямбда-функции. Используйте его как значение для поля action в HTML-форме вашего веб-сайта.

Теперь рабочий процесс готов. Вот небольшой обзор: * Загрузите веб-страницу с новым разделом комментариев. * JavaScript выполняет HTTP GET для вывода списка существующих комментариев из общедоступного файла, размещенного на страницах GitHub. * При отправке формы для добавления нового комментария выполняется HTTP POST на URL-адрес AWS API Gateway, который запускает функцию Lambda. * Функция Lambda использует GitHub REST API для получения последнего списка комментариев, вставляет новый комментарий и передает обновленный список в GitHub.

Заключительные примечания

Полное решение должно учитывать, что один плоский файл для всего веб-сайта будет слишком большим. Если только вы не похожи на меня и у вас мало трафика. Чтобы разбить вещи, я рекомендую идентифицировать статьи или страницы, на которых вы хотите добавить отдельные разделы, с помощью некоторой формы uuid, хэша или простого числового идентификатора, который хранится где-то на странице. Генераторы статических сайтов обычно предоставляют функции, которые делают это за вас. Используйте этот идентификатор в качестве имени файла, которое вы фиксируете в ветке gh-pages. В результате получается список, которым легко управлять как для редактирования, так и для чтения.

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

Когда потребитель отправляет HTML-форму, шлюз API передает HTTP-запрос в функцию Lambda, которая затем возвращает ответ. Это означает, что теперь пользователь ушел с сайта, с которого был сделан запрос. Чтобы остаться на сайте или переместиться в другое место с фактическим содержанием, вы можете добавить скрытое поле redirect в HTML-форму и попросить API Gateway вернуть этот URL-адрес в HTTP-перенаправлении обратно на страницу, отправившую запрос.

Для этого вам нужно добавить сопоставление в раздел Method Response для метода POST в API Gateway, чтобы он возвращал фактический 302 с заголовком Location, когда функция Lambda отвечает примерно так:

return {
    "statusCode": 302,
    "headers": {
        "Location": params['redirect']
    }
}

И последнее замечание, если вы не заботитесь о конфиденциальности, это то, что вы можете обойти Lambda и выполнить все изменения в JavaScript. Просто имейте в виду, что любые учетные данные, используемые для входа в GitHub, будут видны в коде JavaScript. Однако в зависимости от приложения это может не быть проблемой.

Хотя этот рабочий процесс немного сложен, его несложно создать и легко поддерживать. Я надеюсь, что он пригодится вам в будущем, во всяком случае, это хорошее упражнение по рабочим процессам AWS Lambda и API-интерфейсам GitHub REST.

Если вам понравилась эта статья и вы хотите быть в курсе того, над чем я работаю, порекомендуйте ее, посетите tryexecptpass.org для получения дополнительных тем и подпишитесь на меня в Twitter.