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

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

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



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


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

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

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

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

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

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


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

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

CDN


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

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

Подключение


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

<script id="common_menu_loader" type="text/javascript" charset="utf-8" data-language="en" src="http://menu.com/loader.min.js"></script>
<div id="common_menu"></div>

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

Так как для сайта, подключающего меню, скрипт-загрузчик — это сторонний скрипт, который в общем случае может грузиться дольше, чем локальная статика проекта, существует возможность подключать загрузчик асинхронно (с атрибутом 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-атрибуты, которые разработчик сайта определяет когда встраивает к себе загрузчик.

<script id="common_menu_loader" type="text/javascript" charset="utf-8"
    data-notifications_enabled="1"
    data-chat_enabled="0"
    data-intro_tooltips_enabled="1"
src="http://menu.com/loader.min.js"></script>

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

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.



Верстка


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

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

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


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



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







Т.к. на разных регионах в меню может быть разное количество игр, пришлось сразу прописывать в коде все варианты. Сделали мы это на чистом 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-очереди события и складируют результаты работы по разным БД.

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



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


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

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

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

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


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

В заключение


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

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

Комментарии (16)


  1. Pwz
    19.06.2015 15:45
    +3

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


    1. d_burakov Автор
      19.06.2015 18:50
      +5

      Не совсем понял, как ваш вопрос относится к теме статьи, но все же попробую на него ответить.

      1. Сессия пользователя на большинстве наших сервисов хранится 2 недели. Если вы оказываетесь «разлогиненным» раньше, возможно вы не ставите галочку «Запомнить меня». В таком случае ваша сессионная кука удалится после перезапуска браузера.
      2. Насчет «все редиректы авторизации сами проводить» не понял совсем. Поясните пожалуйста вопрос.
      3. Что касается возвращения на место, откуда нажали кнопку «Войти», наш OpenId провайдер работает в соответствии со спецификацией — после успешной аутентификации возвращает на адрес, переданный в параметре return_to.


      1. cynovg
        21.06.2015 12:26

        Насчет пункта 2., всё просто. Могу зайти на какой-то из ваших ресурсов (на пример, на wotreplays.ru), где необходимо авторизоваться. Начинаю авторизовываться, но оказывается что меня уже забыли (это, кстати, пункт 1, зачастую сессии хранятся меньше двух недель, что бы вы не говорили). Ладно, авторизовываюсь на wargaming.net, но назад вернуться мне уже не получается (даже если использую функцию браузера «на предыдущую страницу», меня редиректит на wargaming.net). То есть, мне приходится снова вбивать адрес wotreplays.ru или искать ссылку, по которой я хотел перейти.


        1. d_burakov Автор
          21.06.2015 13:58



          Сторонние сайты, использующие Wargaming.net OpenID, вольны делать любые редиректы. Видимо они не запоминают место, откуда пользователь нажал «Войти», и возвращают всегда на главную.

          То же самое с временем хранения сессий. На Wargaming.net они хранятся 2 недели, а сколько на сайте — сайт решает сам.


          1. cynovg
            21.06.2015 14:48

            До сторонних сайтов дело не доходит, когда пользователь не авторизован на вагрейминге. После авторизации он там и остается.


  1. Samber
    19.06.2015 23:47
    +2

    css для оформления n элементов порадовал, уверен, пригодится в будущем. Спасибо!


  1. MrZaYaC
    22.06.2015 10:54
    +1

    *offtop* вот бы мне столько голды *offtop*
    А по поводу статьи я вообще не понял зачем она. Разделение фронта и бека на два независимых модуля не ново. Вставка на странице скрипта который все делает тоже.
    Может вы этим постом говорите что ваше меню можно использовать на своем сайте?


  1. lonelysuch
    29.06.2015 00:06

    А каптчу до сих пор сразу вводить заставляете… Берите пример с Хабра.