Сегодня завершающая часть трилогии (предыдущие: Часть 1, Часть 2) о создании мультисервисного web-приложения на базе технологии Docker. В этой статье мы окончательно «сложим пазлы» в единую картину работающего приложения.
Уточним, что нам осталось сделать согласно постановки задачи:
- Реализовать WebSocket сервер на http://localhost/ws; (используем Python-Sanic).
- Написать простейший чат http://localhost/ (используя VueJs) который будет авторизироваться через http://localhost/api, реализованный в Части 2. Получим некий token, при помощи которого можно будет подключиться к чату на базе WebSocket-сервера из п. 1.
Этап 1. WebSocket server на Sanic
Небольшое отступление.Асинхронный фреймворк Sanic позволяет реализовать WS-сервер на базе созданного еще во
Итак:
# Из корня проекта выполняем: mkdir ws
Добавляем файл ws/Dockerfile
FROM python:3.6.7-slim-stretch WORKDIR /app RUN apt-get update RUN apt-get -y install gcc COPY requirements.txt /tmp RUN pip install -r /tmp/requirements.txt VOLUME [ "/app" ] EXPOSE 8000 CMD ["python", "run.py"]
Здесь мы «просим» docker создать нам контейнер, взяв за основу образ с python:3.6.7. Нужно доустановить в него некоторые системные библиотеки и необходимые для работы приложения python-пакеты из ws/requirements.txt:
# содержимое ws/requirements.txt sanic asyncio_redis
Кроме того, мы указали, что запуск приложения будет осуществляться на 8000 порту внутри самого контейнера, а команда реализация сервера находиться внутри run.py:
import os from time import time from sanic import Sanic import ujson import asyncio_redis from websockets.exceptions import ConnectionClosed app = Sanic('websocket') conn = {} CONN_CACHE_TIME = 10 # sec @app.listener('before_server_start') async def start(app, loop): app.redis = await asyncio_redis.Pool.create(host='redis', poolsize=10) @app.listener('after_server_stop') async def stop(app, loop): app.redis.close() async def check_token(request): token = request.args['token'] async def checkTokenAlive(ws, token): if time() - conn.get(ws, 0) > CONN_CACHE_TIME: token_exists = await app.redis.exists(token) if token_exists: conn[ws]=time() else: return False return True @app.websocket('/') async def feed(request, ws): token = request.args['token'].pop() if token: isAlive = await checkTokenAlive(ws, token) while isAlive: try: data = await ws.recv() if data: if data=="/out": await ws.close() await ws.send(f'I\'ve received: {data}') except ConnectionClosed: pass isAlive = await checkTokenAlive(ws, token) await ws.close() if __name__ == "__main__": debug_mode = os.getenv('API_MODE', '') == 'dev' app.run( host='0.0.0.0', port=8000, debug=debug_mode, access_log=debug_mode )
В сервере предусмотрена периодическая (10 секунд) проверка token на «наличие» в Redis. Напомню, данный токен — это то, что получит наше SPA в случае успешной авторизации на localhost/api/v1.0/user/auth (реализация в Часть 2). Дополнительно реализована инструкция «/out» для самого чата, которая при отправке с клиента, закрывает текущее WebSocket-соединение.
Дополняем docker-compose.yml новым сервисом:
services: ws: container_name: test_ws build: context: ./ws tty: true restart: always volumes: - "./ws:/app" links: - "redis" networks: - internal env_file: - .env
Корректируем конфигурацию nginx сервера таким образом, чтобы все запросы, которые приходят на localhost/ws, проксировались к контейнеру «ws»:
services: location /ws { rewrite /ws$ / break; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_pass http://ws:8000; }
Так как мы пишем SPA-приложение, работающее в браузере, нам необходимо получить самый что ни есть «классический» JavaScript, который гарантированно будет выполняться на подавляющем большинстве браузеров, установленных у пользователей. В силу очевидных причин, развитие JavaScript как языка программирования (ES5, ES6 и т. д.) сильно ушло вперед по сравнению с тем, что могут предложить существующие браузеры. По этой причине для того, чтобы воспользоваться всей мощью языка на клиенте, нам необходимо «преобразовать» наш «современный» синтаксис в («старый») понятный для браузера код.
Для этой цели как нельзя лучше подходит пакетный статический анализатор кода Webpack, к которому в качестве плагинов можно подключить конвертеры: Browserify, TypeScript, Sass, Less, css-minify, js-minify и многие другие, облегчающие web-разработку клиента. В общем случае — Webpack представляет собой демон (работающий процесс), который в зависимости от конфигурации отслеживает изменение кода и «налету» преобразовывает «удобный и современный» js-код в аналогичный по функционалу, но подходящий для работы в большинстве браузеров. В общем случае текст кода, который мы пишем, последовательно преобразовывается подключаемыми к демону плагинами и сохраняется в виде одного/двух файлов в директории /dist.
Настройка Webpack (установка и конфигурирование плагинов) — весьма муторное занятие, но существуют «утилиты-помощники», позволяющие выполнить эту затратную по времени операцию. Так как мы пишем фронтенд на VueJs, то в качестве такого «помощника» воспользуемся утилитой vue-cli, которая, кроме разворачивания Webpack, создаст базовое тестовое приложение на Vue, которое мы изменим под свои цели.
Этап 2. Создаем «SPA demo» при помощи vue-cli
Итак, нам нужно:
- запустить контейнер с Node (версии LTS 10.5);
- сгенерировать при помощи vue-cli demo-приложение;
- запустить его в контейнере;
- настроить маршрутизацию запросов сервера nginx.
Выполним из корня нашего проекта команды:
# Переменной GID присваиваем идентификатор группы хост-машины echo "GID=$(id -g)" >> .env # Переменной GID присваиваем идентификатор текущего пользователя echo "UID=$(id -u)" >> .env
Cоздаем файл app/Dockerfile:
# За основу выбераем последний стабильный (LTS) образ версии Node FROM node:10.15.0-alpine WORKDIR /app VOLUME ["/app"] # инсталируем vue-cli согласно https://cli.vuejs.org/guide/installation.html RUN npm install -g @vue/cli
В docker-compose.yml добавляем:
services: app: build: ./app tty: true user: "${UID}:${GID}" container_name: test_app volumes: - "./app:/app" networks: - internal env_file: - .env
Хочу обратить внимание на
Далее выполняем:
# из корня нашего проекта, перестраиваем и перезапускаем контейнеры make upb # или, если нравится традиционный способ, то: docker-compose up -d --force-recreate --build
Мы запустили контейнер с Node версии 10.15.0, c предварительно установленным vue-cli.
Теперь:
# подключаемся к sh-консоли работающего контейнера c Node (в docker-compose его имя test_app) docker exec -it test_app /bin/sh # в консоли контейнера переходим в корневой каталог cd / # создаем demo приложение при помощи уже установленной во время создания контейнера vue-cli vue create app # Мастер, сообщаем что директория не пуста.. Выбираем "Merge" # Затем в меню "Manually select features" выбираем пакеты которые бы мы хотели установить для нашего приложения # Выбираем нужные пакеты, я оставил: # ◉ Babel # ◉ Router # ◉ CSS Pre-processors # ◉ Linter / Formatter # Далеее, на все вопросы установщика, можем соглашаться по умолчанию. # В конце установки пакетов, мастер выдает сообщение # $ cd app # $ npm run serve
Выполнив последние 2 команды, мы увидим, что webpack по умолчанию запустился на 8080 порту.
Отключаемся от контейнера (Ctrl+С) и видим, что на хост-машине (ls -la app/ ) в папке фронтенда контейнер сгенерил «кучу» файлов, которые благодаря механизму Volumes, теперь являются общими для хост-машины и для контейнера. Самое интересное здесь то, что благодаря GID и UID сгенеренные из контейнера файлы принадлежат текущему юзеру host-машины, хотя внутри контейнера юзер имеет другое имя. Более исчерпывающую информацию о пользователях/группах в контейнерах можно почерпнуть здесь.
Так как у нас уже есть готовый рабочий код, все, что нам осталось сделать, — это подправить наш app/Dockerfile таким образом, чтобы контейнер при запуске выполнял команду запуска демона, то есть npm run serve.
# окончательный вид app/Dockerfile FROM node:10.15.0-alpine RUN apk add --no-cache bash RUN npm install -g @vue/cli WORKDIR /app VOLUME ["/app"] RUN npm install EXPOSE 8080 CMD ["npm", "run", "serve"]
Этап 2.1. Настраиваем nginx для vue-приложения
В конец конфигурационного файла nginx/server.conf вставим:
location / { rewrite /(.*) /$1 break; proxy_redirect off; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_pass http://app:8080; }
Теперь перезапускаем все созданные контейнеры:
make upb
и заходим на http://localhost. Должны увидеть demo-страницу vue.
Важно! Demo-приложение, которое генерит vue-cli, по умолчанию для mode dev подключает интегрированный плагин vue-hot-reload-api, который через WebSocket-соединение «узнает» от демона webpack об изменениях на сервере и автоматически перезагружает страницу приложения в браузере, подтягивая новые данные. По этой причине, в конфигурации nginx, заложена поддержка проксирования WebSocket-заголовков.
# Если нужно видеть вывод консоли работающего WebPack, # цепляемся к контейнеру командой docker attach test_app
Этап 2.2. Пишем клиентское приложение
Немного поговорим о том, как будет работать наш SPA-клиент:
- При заходе на localhost проверяем, есть ли в localStorage значение для ключа «token». Если есть, пробуем установить WebSocket-соединение с ws://localhost/ws?token=xxx с сервером. Если сервер закрыл соединение (token отсутствует в redis), сбрасываем приложение на страницу /login.
- На странице /login находится форма авторизации, которая отправляет данные на api (http://localhost/api/v1.0/user/auth), и в случае успеха сохраняет token в localStorage с последующим редиректом на главную.
- В случае неудачной авторизации, отрисовываем повторно форму авторизации с отображением ошибки валидации.
К сожалению, объем кода SPA-приложения довольно большой, что неминуемо приведет к тому, что объем статьи будет огромный. Все изменения кода в этой статье, я выполнил в отдельной ветке на GitHub. Их можно посмотреть в виде pull request, которые я выполнил по сравнению с состоянием кода по окончании
Итоговый результат
Если вы дочитали до этого места, то, пожалуй, именно сейчас как раз тот момент, когда вы можете полностью увидеть реализованный вариант рабочего приложения, состоящего из 7 контейнеров.
cd ~ git clone git@github.com:v-kolesov/vue-sanic-spa.git cd vue-sanic-spa docker-compose up -d
После запуска всех контейнеров в браузере вбейте http://localhost. Вы должны увидеть, нечто похожее. В моем случае, в правом нижнем углу — 2 терминала, которые подключены непосредственно к контейнерам test_app, test_ws (нужно в целях отладки и дебагинга).