Как работает Wargaming Common Menu

Доброго времени суток!

Хочу поделиться с сообществом опытом разработки JS-виджета межпроектной навигации. Он представляет собой модуль, который подключается на большинство сайтов вселенной Wargaming (Порталы, Wiki, WarGag и пр.).

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

aace21ebe8c140edaf3b14b04e99dcc0.PNG

Сначала немного о требованиях

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

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

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

Меню должно уметь показывать текущему пользователю данные по его аккаунту — наличие/отсутствие профилей в разных проектах, краткую текущую статистику по каждому из проектов.

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

Теперь о реализации

По сути проект состоит из двух самостоятельных приложений — фронтенда и бекенда, которые деплоятся отдельно и существуют независимо.

Фронтенд — это JS-приложение, набор статических файлов, а бекенд — это JSON-API с доступом к данным по специальному токену. Такая схема была выбрана в основном из-за того, что необходимо выдерживать приличную нагрузку (суммарную по всем сайтам) и обеспечивать работу без даунтайма при штатных обновлениях до новой версии, с минимальными последствиями в случае «падения», так как это означало бы частичную неработоспособность почти всех наших публичных веб-сервисов.

CDN

Доступность фронтенда обеспечивается схемой с многоуровневым кэшированием: браузер пользователя — CDN-сервер — origin-сервер — запасные origin-серверы. CDN-сервер работает как кэширующий прокси. Origin-серверы находятся в разных дата-центрах, и на уровне конфигурации CDN для них настроен поочередный fallback.

Инвалидация кэша происходит с помощью purge-команды API CDN-провайдера, а управление кэшем браузера — с помощью GET-параметров URL.

Подключение

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

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

Так как для сайта, подключающего меню, скрипт-загрузчик — это сторонний скрипт, который в общем случае может грузиться дольше, чем локальная статика проекта, существует возможность подключать загрузчик асинхронно (с атрибутом async) — чтобы загрузка сайта не блокировалась, и быть оповещенным о факте успешной загрузки с помощью callback«а.

Сайты-консюмеры не участвуют в релизах меню, а это значит, что нет возможности изменять src loader«а, чтобы сбрасывать кэш браузеров, поэтому используются HTTP-заголовки:

location / { expires 7d; } location = /loader.min.js { add_header Cache-Control «no-cache, must-revalidate»; } С ними браузер каждый раз при обращении к файлу загрузчика отправляет HEAD-запрос на сервер, и если сервер отвечает 304 Not Modified — файл берется из кэша.

Сборка

Фронтенд выкатывается на прод-серверы собранным пакетом. Сборкой занимается Grunt, он склеивает и минифицирует исходники, компилирует scss в css, собирает иконки в спрайты (отдельно SVG и PNG), генерирует предустановленные наборы ссылок для меню. Также в dev-режиме есть возможность запустить проект «сам по себе» на express«е с эмуляцией бекенда.

Все иконки отрисованы в векторном формате SVG и сжимаются в один спрайт Grunt-плагином dr-svg-sprites. Это позволяет не заботиться об отдельной копии увеличенного размера для ретины и выигрывает по размеру файла. К тому же для старых IE этот плагин генерирует PNG-спрайт, что очень удобно и избавляет нас от головной боли и кучи багов.

Конфигурация

Для конфигурации меню под нужды конкретного сайта используются data-атрибуты, которые разработчик сайта определяет когда встраивает к себе загрузчик. На случай, когда сайт на момент встраивания загрузчика сам не знает значения всех параметров, есть альтернативный способ передачи параметров — записать соответствующие куки: cm.options.notifications_enabled=»1» cm.options.chat_enabled=»0» cm.options.intro_tooltips_enabled=»1» И еще есть JS-API, через которое можно сделать то же самое асинхронно. WG.CommonMenu.update ({ notifications_enabled: 1, chat_enabled: 0, intro_tooltips_enabled: 1 });

Хранение настроек

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

Меню умеет сохранять персональные настройки пользователя вне зависимости от сайта, на котором оно подключено без участия бекенда. Для сохранения предпочтений пользователя используется Local Storage, либо Cookies в случае недоступности первого. Так как у меню собственный домен, это позволяет сохранять настройки кроссдоменно, то есть можно работать так, будто все открываемые сайты с меню находятся на одном домене. Чтобы добиться такого эффекта, используется статический HTML-файл, который загружается во фрэйме. Этот фрэйм на своем домене и хранит информацию в LS или в куках, а для обмена данными используется PostMessage API.

Уведомления

Меню умеет отправлять десктопные уведомления пользователю (если они поддерживаются браузером). Они используются, чтобы сообщить о новых непрочитанных сообщениях, когда вкладка с сайтом неактивна или браузер свернут. Так как у пользователя может быть одновременно открыто несколько вкладок с разными сайтами, но на всех есть меню и каждое умеет оправлять уведомление, мы научили разные экземпляры приложения общаться между собой, чтобы они могли договориться, кто именно будет отправлять уведомление. Сделано это с помощью все того же фрэйма, который хранит данные в Local Storage или Cookies, и предоставляет к ним доступ через JS API.

a862cb187baf40299df3afe93d153f03.PNG

Верстка

Верcтка меню адаптирована под мобильные устройства, и автоматически меняет представление на предустановленных брейкпойнтах. В качестве шаблонизатора используем слегка модифицированное под наши нужды решение Джона Ресига. Оно скромно по функциональности, зато позволяет использовать одни и те же шаблоны в JS и на бекенде в Jinja2 и не требует подгрузки тяжелых библиотек.

Для большинства ссылок по задумке дизайнеров нужно было сделать нестандартное подчеркивание (линия подчеркивания должна была быть ниже стандартной и появляться плавно). Сделали мы это таким образом:

.link: after { content:»; border-bottom: 0 solid; position: absolute; top: 50%; left: 0; width: 100%; margin-top: 8 px; opacity: 0; transition: .3s ease opacity; } .link: hover: after { opacity: .8; border-bottom-width: 1 px; // IE8 hack }

Сперва решили анимировать ширину подчеркивания, но это выглядело слишком уж нестандартно. Затем был вариант с подъезжанием подчеркивания снизу, но, в итоге решили остановиться на простом появлении из прозрачности.

5e170f5ddc5547de956b7a87cda2abd8.jpg

Еще одной особенностью дизайна была разная стилизация элементов в меню «Игры», в зависимости от количества этих самых элементов.

e64cf7e17e74474182f697240f651352.jpg

21f6dd5c1c2c45259ceb3dfee9b2dbb6.jpg

1ceedd10222b41fd8583d450795dca57.jpg

Т.к. на разных регионах в меню может быть разное количество игр, пришлось сразу прописывать в коде все варианты. Сделали мы это на чистом CSS. О хитром способе подсчета элементов уже писали на хабре здесь и здесь.Если вкратце, то с помощью псевдоселектора : nth-last-child (n) мы узнаем, есть ли у нас хотя бы n элементов. Как проверить, что у нас ровно n элементов? Всего-лишь добавить псевдоселектор : first-child (или nth-child (1)).Таким образом мы выберем первый элемент из n. Остальные элементы можно выбрать с помощью селектора ~.Например, вот так мы стилизуем список с шестью вложенными элементами:

li: nth-child (1): nth-last-child (6), li: nth-child (1): nth-last-child (6) ~ li { … }

Досье пользователя

Если пользователь сайта в данный момент залогинен, то меню обращается в бекенд за данными по этому пользователю. Такие обращения периодически повторяются для актуализации UI.

Бекенд построен на Twisted-воркерах, каждый из которых отвечает за конкретную функциональность; например, есть отдельный сервер для выдачи профайла пользователя, отдельный для уведомлений и так далее.

Однако публично доступные сервера — это вершина айсберга. Вся основная работа происходит в Twisted-демонах, которые обрабатывают поступающие по AMQP-очереди события и складируют результаты работы по разным БД.

Основной их задачей является сбор необходимых данных по внутренним веб-компонентам.

67e42c8d133a4cf683b7e738f17f9c97.png

Хранение данных

Для быстрого доступа к данным извне организован Redis-кэш, который наполняется и актуализируется теми же воркерами. Чтение из этого кэша происходит прямо из Nginx, с помощью модуля HttpRedis с fallback«ом на питоновский сервер.

Раньше Redis был основным хранилищем данных. Он полностью устраивал, поскольку обладал хорошей производительностью и позволял не заботиться об устаревании сессий. И доступ к данным был мгновенным: все данные были в памяти. Однако объем данных неумолимо рос, и вскоре мы начали сталкиваться с проблемой нехватки оперативной памяти. Для корректной работы Redis необходимо, чтобы было свободной как минимум столько же памяти, сколько уже занято, так как периодически происходит сбрасывание дампа хранилища на диск, для чего форкается процесс.

Мы решили использовать MySQL базу данных с движком TokuDB в качестве постоянного хранилища (TokuDB выбрали из-за хорошего сжатия данных и возможности быстро менять структуру таблиц) и Redis для хранения сессионного ключа. Также Redis используется как хранилище кэша.

Отключенный JS

Если у пользователя в браузере отключен Javascript, бекенд умеет подстраховывать и рендерить меню на сервере. Тогда отображение происходит во фрэйме. Для рендеринга используюся те же шаблоны и стили, что и в JS.

В заключение

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

Также в планах ряд интересных фич, которые с помощью Common Menu могут быть реализованы и доставлены пользователю одновременно на всех веб-сервисах в сжатые сроки.

© Habrahabr.ru