19 August 2013

Mail.Ru для бизнеса, часть 2: как это работает

Mail.ru Group corporate blogWebsite developmentJavaScript
Недавно мы запустили «Mail.Ru для бизнеса». В прошлом посте мы уже рассказывали о том, как организовать корпоративную почту на Mail.Ru, а сегодня подробнее остановимся на технологиях, которые мы задействовали при реализации. При работе над проектом мы реализовали ряд технических решений на серверной и клиентской стороне, которые в итоге позволили сделать сервис удобнее. В этом посте мы подробнее расскажем, как технологически устроена наша клиентская часть.

Под катом — про single-page, сбор ошибок, шаблонизацию и переход от одного URL-а к другому без перезагрузки страницы.





Single-Page

Интерфейс главной страницы «Mail.Ru для бизнеса» достаточно простой и удобный. Именно на этой странице начинается процесс добавления доменов. После регистрации первого домена вам станет доступна админка. Всё вместе — главная и админка (и другие скрытые страницы) – это одно полноценное single-page приложение.



Для реализации single-page приложения были использованы техники, которые с успехом применялись при разработке Почты, Календаря и Адресной книги. Например, отрисовка страниц происходит на клиентской стороне с помощью шаблонизатора Fest (подробнее о нем – чуть ниже), а всё общение с сервером ограничивается обращением к методам API.

Некоторые же приёмы, наоборот, были применены впервые, и я очень рада, что мне удалось привнести что-то новое в процессе разработки :)

Мультиавторизация

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



CSS-анимация

Мы решили отказаться от поддержки старых браузеров, поскольку считаем, что администраторы доменов — достаточно продвинутые пользователи. Например, IE мы поддерживаем, начиная с 8-й версии и выше. Благодаря этому у нас развязались руки в плане использования современных плюшек. Особо выделяется среди них CSS-анимация. Как известно, пользователи любят, чтобы на странице все было красиво и плавно. Можно, конечно, это «красиво и плавно» реализовать теми же скриптами, но если браузер сам дает нам возможность организовать динамику стандартными средствами, почему бы этим не воспользоваться?

Основная работа по реализации анимации выполняется с помощью CSS.

.panel {
    …
    top: -110px; 
    transition: top 0.5s 0;
}
/*показ панели */
.panel__show {
    top: 0; 
}


Скриптом мы только запускаем ее в нужные нам моменты.

var $panel = $('#id1')
    , $wrap= $('#id2')
    , offsetTop = $wrap.offset().top + $wrap.outerHeight()
;

$(window).scroll(function() {
    var show = this.scrollTop() > offsetTop;
    $panel.toggleClass('panel__show', show);
}.throttle(200, $(window)));


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

var cssTransitions = 
    CSS.supports("transition", "all 1s 1s")
    || CSS.supports("-webkit-transition", "all 1s 1s")
    //… etc
;
...
if( cssTransitions ) {
    //анимация средствами css
}
else { 
   //анимация средствами jQuery
}


CSS.supports — это что-то типа стандартизированного Modernizr, только нативного и выполняющего одну конкретную задачу – проверку поддержки определенного CSS-свойства. Нативная поддержка есть в браузерах Chrome 28+, FireFox 22+, Opera 12.1+. Для остальных браузеров, мы используем полифил, написанный одним из разработчиков нашей почты. Этот полифил по-умолчанию доступен практически на любом проекте Mail.Ru и с успехом применяется в некоторых специфических ситуациях.

Fest

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

Все формы, которые присутствуют на странице — добавление домена, пользователей, попапы, добавление/удаление администратора — строятся на одном шаблоне. Благодаря универсальности Fest мы можем позволить себе внутри одного и того же шаблона отрисовывать абсолютно разные контролы: попапы с дропдаунами, попапы без дропдаунов, страницы с чек-боксами. Формирование всех этих форм происходит в одном месте.


History API

Как вы могли заметить, мы считаем хорошей практикой использование сторонних разработок, если они хороши и подходят для наших целей: зачем изобретать велосипед? Так, в нашем проекте мы активно используем полифил History API – форк библиотеки от Devote. Применение History API существенно упрощает жизнь разработчикам. Mail.Ru для бизнеса «магическим образом» переходит от одного URL-а к другому без перезагрузки страницы — в зависимости от того, какие действия совершал пользователь. History API позволяет нам передавать нужные параметры, которые никак не отображаются в UI, и вуаля: новый слайд отрисован. При этом, если пользователь перезагрузит страницу, то он окажется ровно на том месте, где был.

Как это все работает? API предоставляет нам доступ к объекту типа History, который есть у каждой вкладки и располагается по адресу window.history в объектной модели. С помощью JavaScript мы можем манипулировать с адресной строкой, «переходя» по страницам с помощью методов .pushState(state, title, url), которые добавляют новый url в историю браузера и .replaceState(state, title, url), которые меняют текущий url без добавления нового пункта в историю.

Свойство же state объекта history всегда будет указывать на актуальное «состояние» текущего url-а. Например, если мы сделали pushState({ data: “test1” }, “”, “url1”) и pushState({ data: “test2” }, “”, “url2”) – т.е. совершили два «перехода» по страницам с добавлением пунктов в историю браузера и оказались на странице “url2”, а пользователь нажал «Back» в браузере и оказался на странице “url1”, то значение свойства history.state.data будет “test1” – то, что нам нужно.

Особенно это удобно на главной странице: если пользователь хотел перейти на вполне конкретный адрес, но в данный момент не авторизован, то в pushState мы передадим объект state, содержащий нужный адрес, а после авторизации мы перенаправим его туда, куда ему хотелось попасть — таким образом, ему не придется снова переходить ручками. Вся логика переходов выполняется в браузере, без перезагрузки страницы, при этом адрес страницы выглядит «по-человечески», без хешей (“#”).

//определяем текущий URL
var path = history.location.pathname; // history.location – from polyfill 
if( /* пользователь не авторизован */ ) { 
    var state = {
        notAuth: true
        , urlBack: (path != "/") ? path : ""
    };
    //запоминаем текущее состояние пользователя и
    // переходим на главную страницу с отрисовкой соответствующего слайда
    history.pushState( state, null, "/");
}


Затем, после авторизации, мы понимаем, куда хотел отправиться пользователь, и перекидываем его в место назначения.

RequireJS vs SingleFile

Большое singe-page приложение это всегда много js-модулей. Какие-то модули — это общий функционал, который нужен практически всегда, какие-то модули нужны только на отдельных страницах. При разработке достаточно сложного функционала количество js-файлов растёт очень быстро. При этом разработчику нужно постоянно думать о зависимостях и о порядке подключения js-файлов. Ошибившись в порядке или забыв подключить нужный файл, можно получить ошибку в самом непредсказуемом месте. AMD API как раз решает эту проблему. Разработчику просто нужно указывать список зависимостей и объявлять модули специальным образом, а о загрузке этих модулей в правильном порядке позаботится RequireJS.

Но было бы неправильно заставлять пользователя ждать загрузки множества файлов каждый раз, когда он заходит в наше приложение. Поэтому RequireJS мы используем только на этапе разработки, а в бой идёт «сборка» — один js-файл, который содержит все модули, необходимые для работы приложения. Сборка производится библиотекой r.js, которая является частью проекта RequireJS.

Понятно, что в большинстве случаев весь набор модулей не нужен и может показаться, что некоторые из них подключаются «зря», увеличивая количество данных, передаваемых в браузер. Но это только на первый взгляд. На самом деле gzip-сжатие на сервере отдаваемых js-файлов практически нивелирует эту проблему. Тем более, что мы экономим на http-запросах. В результате один большой js-файл загрузится быстрее, чем несколько маленьких, даже с учётом того, что маленькие файлы будут загружаться «лениво» — т.е. только по мере необходимости. Для подробного ознакомления с вопросом предлагаю к прочтению статью Загрузка и инициализация JavaScript или самостоятельный research на тему.

Сборка проекта

Такие вещи, как описанная в предыдущем разделе сборка js-файлов, не должны делать руками. Перед выкладкой в продакшн, помимо сборки js-файлов, нужно собрать css-ки из scss-файлов, «скомпилировать» fest-шаблоны и полученный результат передать js-сборщику, нужно обновить номер версии в конфигах и т.д. Все эти вещи нужно было как-то автоматизировать.

Было принято решение использовать Grunt – это менеджер задач общего назначения, написанный на js под nodejs. Он позволяет js-разработчику писать таски для сборки проекта на сервере. Для Grunt существует база готовых тасков, из которой и были взяты, например, grunt-contrib-sass и grunt-contrib-requirejs. Grunt запускает нужные таски автоматически при выполнении git push.

Cбор ошибок

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

Sentry – это опенсорсный проект, который работает с целой россыпью языков и платформ. Он позволяет отслеживать как серверные, так и клиентские ошибки и делать по ним статистику. Этой платформой также пользуются, например, в Mozilla и Instagram.

Sentry умеет отправлять письма с описанием ошибок. При этом можно не опасаться, что одинаковые репорты о малозначительных неполадках забьют ящик: параметры сообщений, которые достойны того, чтобы беспокоить разработчика, можно легко настроить.

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

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

Однако простого описания ошибки и номера строки может оказаться не достаточно. Для некоторых экзотических или сложно воспроизводимых ошибок нужна ещё дополнительная информация для анализа этой ошибки. Например для ошибки “$ is not defined” нужно понять, а загрузился ли у нас jQuery? Для сбора браузерных ошибок мы используем библиотеку Raven.js, которая обладает одной недокументированной возможностью – вызывать обработчик dataCallback перед каждой отправкой ошибки на сервер:

var ravenSetTimeout = typeof setTimeout === 'function' && setTimeout;
var options = {
    dataCallback: function(obj){
        var userData;
        if( typeof obj === "object" ) {
            userData = obj["sentry.interfaces.User"];
            if( !userData ) {
                userData = obj["sentry.interfaces.User"] = {};
            }
            userData.inFrame = window.parent !== window;
            userData.jQueryVersion = typeof jQuery === 'function' && jQuery.fn.jquery;
            userData.setTimeoutBody = 
              typeof setTimeout === 'function'
              && (setTimeout + "").contains("[native code]")
                ? "[native]"
                : ravenSetTimeout
                    ? setTimeout === ravenSetTimeout ? "[raven]" : "[other]"
                    : "[none]"
            ;
        }
 
        return obj;
    }
};
Raven.config("…", options).install();


В этом примере мы проверяем, не запушены ли мы в iframe’ме, версию jQuery (если она вообще загружена) и код setTimeout (т.к. он может быть подменён). Мы же собираем чуть больше статистической информации для правильной диагностики ошибок. Значение свойства «sentry.interfaces.User» будет отображаться в интерфейсе Sentry в специальном поле.

Discuss

Так работает наша Почта для бизнеса. Надеюсь, она будет радовать пользователей красотой, удобством и функциональностью. Подробнее про «Mail.Ru для бизнеса» можно почитать прямо здесь. Напомню, что сервис сейчас в бете и мы будем рады вашим отзывам и предложениям по почте feedback@biz.mail.ru или в комментариях к этому посту.

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

Ольга Алексеева,
Егор Халимоненко (termi)
Группа frontend-разработки Почты
Tags:Бизнес-почтадоменыВеб-разработкаjavascript
Hubs: Mail.ru Group corporate blog Website development JavaScript
+37
19k 42
Comments 54