Пишем сайт/PWA и выкатываем в прод с Github Actions

Пишем сайт/PWA и выкатываем в прод с Github Actions

Привет! Хочу сохранить для себя некоторые аспекты разработки простенького одностраничного web приложения с функционалом PWA, а также нюансы его деплоя. Опишу некоторые проблемы с которыми я столкнулся, и их решения.

Речь пойдет о сервисе nosins.ru. Сервис позволяет получить индульгенцию - сертификат об отпущении грехов (не имеет юридической силы, если что), если вы православный, католик, пастафарианец или атеист =)

В качестве стека я выбрал React и бекенд на Node.js (Koa). Раскатывается всё это дело с помощью Github Actions.

Основная идея

Сайт представляет из себя форму, которая POST-запросом отправляет пользователя на страницу оплаты яндекс денег. Эту форму можно сгенерировать на сайте ЯДенег и некоторые поля поправить уже на лету, после ввода данных пользователем.

Всё это здорово и просто! Как теперь отследить платёж?
На сервере мы используем yandex-money-sdk и с некой периодичностью проверяем поступления с определённым полем label. Как только есть новые поступления, парсим их и получаем из комментария e-mail адрес клиента и его имя.

Далее мы используем html-pdf библиотеку, чтобы из ejs шаблона сгенерировать pdf файл сертификата. Затем при помощи nodemailer мы отправляем файл клиенту и удаляем его с диска.

Чтобы сделать из этого сайта PWA, добавляем manifest.json с настройками темы и путями к иконкам, и Service Worker (с возможность кеширования контента).

Идея простая, погнали к аспектам разработки и проблемам.

Аспекты

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

Настройки проекта

Во-первых, открыл для себя .env файл и dotenv пакет, который позволяет вынести все критичные переменные, которые не должны попасть в репозиторий (например токены, логины, пароли и т.п.), и задавать их через переменные окружения.
При сборке webpackом их можно прокинуть в приложение через webpack.DefinePlugin.

Далее добавил в проект editorconfig (который задаёт настройки для IDE) и editorconfig-checker как самый простой тест при деплое, проверяющий соответствие кода этим настройкам

"test": "editorconfig-checker --exclude '.git|node_modules|.DS_Store'"

Сборка

Стандарная конфигурация webpack для development с webpack-dev-server и production — когда статику раздает Koa сервер.

PWA

Сделать из обычного сайта PWA проще простого. Надо сгенерить набор иконок, в manifest.json прописать к ним путь, указать цветовые настройки, и еще по мелочи. Также нужно подключить Service Worker.

Иконки удобно сгенерить на favicon-generator. Загружаете туда 512х512 - он вам выплёвывает архив со всеми нужными иконками для андроида, ios и windows + готовый манифест.

Пример manifest.json

{
    "name": "Indulgention-online: Pokaisa",
    "short_name": "Indulgention",
    "icons": [{
      "src": "\/icons\/android-icon-36x36.png",
      "sizes": "36x36",
      "type": "image\/png",
      "density": "0.75",
      "purpose": "maskable"
    },
    {
      "src": "\/icons\/android-icon-48x48.png",
      "sizes": "48x48",
      "type": "image\/png",
      "density": "1.0",
      "purpose": "maskable"
    },
    {
      "src": "\/icons\/android-icon-72x72.png",
      "sizes": "72x72",
      "type": "image\/png",
      "density": "1.5",
      "purpose": "maskable"
    },
    {
      "src": "\/icons\/android-icon-96x96.png",
      "sizes": "96x96",
      "type": "image\/png",
      "density": "2.0",
      "purpose": "maskable"
    },
    {
      "src": "\/icons\/android-icon-144x144.png",
      "sizes": "144x144",
      "type": "image\/png",
      "density": "3.0",
      "purpose": "maskable"
    },
    {
      "src": "\/icons\/android-icon-192x192.png",
      "sizes": "192x192",
      "type": "image\/png",
      "density": "4.0",
      "purpose": "maskable"
    }],
    "start_url": "/",
    "display": "standalone",
    "background_color": "#8E628C",
    "theme_color": "#8E628C"
}

Кстати, наконец узнал как делать круглые иконки для андроида — за это отвечает парметр "purpose": "maskable". Подробнее об этом тут.

Service Worker занимается в основном кешированием статики и подключается в стартовой точке (index.js). Это позволяет грузить сайт без подключения к интернету. Подробнее в туториале на ютуб.

Сам манифест подключается в html в head

<link rel="manifest" href="manifest.json">

После открытии сайта на адроиде вам выпадет уведомление, что сайт можно установить на главную страницу.

photo_2021-02-20-14.59.43
photo_2021-02-20-14.59.39
photo_2021-02-20-14.59.35

Деплой

Долго подступался к деплою собственных приложений, и все проекты до этого деплоил просто через clone с github. Некрасиво, неудобно. Благо появился сервис Github Actions. Он позволяет деплоить приложения за пару минут, прогоняя тесты и выполняя команды на сервере.

Спасибо Вадиму Макееву за его вводную лекцию. Для того, чтобы деплоить, вам понадобится настроить ssh ключи для своего сервера и добавить yml конфигурации в свой репозиторий для деплоя. В конфигурациях можно использовать уже созданные actions через конфигурацию uses. Для меня очень удобным оказался appleboy/ssh-action, позволяющий выполнять команды на сервере по ssh.

Отправка писем

Для этого нужно было настроить gmail почту (в интерфейсе почты), чтобы можно было отправлять письма по smtp. При этом нужно было указывать логин и пароль при создании подключения nodemailer'а.

Регистрация домена

Обычно покупаю дешманский домен на webhost1.ru и настраиваю dns записи, чтобы они указывали на ip моего сервера. Для этого сначала нужно настроить NS записи, чтобы указать, какой dns будет резолвить ваш домен. В NS записи можно указать либо ip хостинга(vps), либо ip домена.

Настройка nginx и tls

Для того чтобы не прослыть лохом в интернете, нужно, чтобы твой сайт работал по https. Самоподписной сертификат можно получить просто с помощью Let's Encrypt и certbot.

Устанавливаем на сервер certbot и выполняем
sudo certbot certonly --standalone --preferred-challenges http -d yoursite.com

После этого в вашей папке /etc/letsencrypt/live/ появляются нужные сертификаты, которые нужно указать в конфигурации nginx.

Nginx нам нужен для двух вещей. Во-первых, чтобы наш http сервер мог работать по https, не меняя реализацию самого сервера. Во-вторых, для возможности запуска нескольких приложений на одном сервере — запросы с разных сайтов будут приходить на порт 80, а nginx в зависимости от того, откуда пришел запрос, будет редиректить на порт соответствующего приложения.


server {
    listen 80;
    listen [::]:80;
    server_name nosins.ru www.nosins.ru;
    
    location / {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $http_host;
        proxy_pass http://your_server_ip:port; //подставьте нужное)
	      rewrite ^ https://$host$request_uri? permanent;    
    }

    client_max_body_size 50m;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name nosins.ru www.nosins.ru;
    client_max_body_size 12m;

    ssl_session_timeout 5m;
    ssl_protocols TLSv1.1 TLSv1.2;
    #ssl_dhparam /etc/ssl/certs/dhparam.pem;
    ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;

    ssl_certificate /etc/letsencrypt/live/www.nosins.ru/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/www.nosins.ru/privkey.pem;

    location / {
        proxy_pass http://http://your_server_ip:app_port; //подставьте нужное)
    }
}

Если это linux, не забудьте настроить firewall ufw чтобы разрешить нужные порты.

Мета теги

Для того, чтобы сайт подтягивал изображение и описание в соц сетях, нужно добавить мета теги.
Основные мета теги — image, description, title

Вот пример для протокола Open Graph

<meta property="og:type" content="website">
<meta property="og:site_name" content="Индульгенция онлайн">
<meta property="og:title" content="Индульгенция онлайн">
<meta property="og:description" content="Очистись от грехов и спаси свою душу от геенны огненной">
<meta property="og:image" content="https://www.nosins.ru/icons/indul-meta.jpg">

После добавления их в head, ссылка на сайт выглядит вот так
--------------2021-02-20---15.14.46

Что ж, вроде самую сладкую часть рассказал, теперь перейдем к трудностям, которые встретились на моём пути.

Проблемы

Сборка при деплое

Чтобы не гонять большие объемы данных при деплое, я решил собирать и фронт и бек при помощи webpack, и деплоить только эти билды. Но оказалось, что после билда бекенда начинают валиться ошибки. Причина была в html-pdf библиотеке, которая не могла найти путь к phanthom-js после билда. Решалось это всё переустановкой html-pdf после сборки. Поэтому решено было билдить фронт в github actions, а серверную часть целиком копировать из исходников. Однако дополнительно нужно было скопировать package.json, чтобы поставить зависимости для серверной части. После долгого чтения мануалов по rsync собрал команду:

rsync -v package.json -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" --archive --compress --no-recursive . user@IPSERVER:~/path

Она копирует только 1 файл package.json и не тащит и не перетирает ничего лишнего.

Переменные окружения при деплое

Для процесса сборки на шаге uses: actions/setup-node@v1 переменные можно прописать в параметре

env:
   ENV_VAR1: ${{ secrets.ENV_VAR1 }}

где secrets задается в настройках репозитория.

На шаге запуска всё оказалось немного сложнее.

Поковырявшись в инструкциях appleboy/ssh-action@master, нашёл, что они прописываются так

env:
   ENV_VAR1: ${{ secrets.ENV_VAR1 }}
   
with:
    envs: ENV_VAR1...

Почтовый сервис не отправляет сообщения

Если вы хотите отсылать однотипные письма через почту, вы скорее всего попадёте в спам, да и вообще письма не будут уходить (вас заблочит сам сервис отправителя).

Для решения этой проблемы я завёл бесплатный почтовый домен nosin на yandex business, чтобы был официальный @nosins.ru.
При настройке почты вам нужно будет добавить TXT записи в настройках домена сайта: @, mail.domainkey, dkim.domainkey (что туда прописать - вам подскажет сам yandex).
При отправке через nodemailer указывайте настоящий адрес отправителя. Тогда вероятность того, что вас заблочат, будет минимальной (ну не минимальной, но меньше).

Неверная настройка записей для домена

Как обычно, нормально не разобравшись с этой темой, натыкал настройку домена по каким-то примерам (а первичная настройка применяется через 2 суток), и пришёл к тому, что что-то работает не верно.

В совокупности с настройкой nginx понял я это не сразу.
В итоге проблема была в неверной настройке A-записи, где вместо @ я поставил www.nosins.ru, что в результате давало запись www.nosins.www.nosins.ru.

Раздача статики + дополнительные роуты на Koa

Если вы раздаете статику по / на Koa сервере и у вас есть какие-нибудь роуты React приложения, то Koa сервер может выдавать 404 при переходе напрямую по ссылке, отличной от /. У меня например есть роут, куда я перенаправляю пользователя после успешной оплаты - /success.

Для того, чтобы Koa отдал ту же статику, нужно добавить middleware:

static_pages.use(async (ctx, next) => {
  if(ctx.path === '/success') {
    ctx.path = '/';
  }
  await next();
})

Возможно, Koa не лучший пример, чтобы раздавать статику, либо я его неправильно приготовил.

В общем, таков процесс разработки и деплоя простого приложения, если в двух словах. Даже самая простая идея может встретить кучу мелких затыков. Главное не сдаваться =)

Буду рад, если вы поделитесь в комментариях своими фидбеком и мыслями, а также опытом по конфигурации проектов, их сборке и выкатке.

Спасибо за внимание! Подписывайтесь на мой телеграм блог про разработку Sleepless Tech, там я делюсь опытом по разработке своих проектов.