Як працює Wargaming Common Menu

Доброго часу доби!

Хочу поділитися з співтовариством досвідом розробки 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.



Верстка

Верстка меню адаптована під мобільні пристрої, і автоматично змінює уявлення на встановлених брейкпойнтах. Як шаблонизатора використовуємо злегка модифікований під наші потреби рішення Джона Ресига. Воно скромно по функціональності, зате дозволяє використовувати одні і ті ж шаблони в 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 можуть бути реалізовані і доставлені користувачеві одночасно на всіх веб-сервісах в стислі терміни.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.