Недавно мы запустили «Mail.Ru для бизнеса». В прошлом посте мы уже рассказывали о том, как организовать корпоративную почту на Mail.Ru, а сегодня подробнее остановимся на технологиях, которые мы задействовали при реализации. При работе над проектом мы реализовали ряд технических решений на серверной и клиентской стороне, которые в итоге позволили сделать сервис удобнее. В этом посте мы подробнее расскажем, как технологически устроена наша клиентская часть.
Под катом — про single-page, сбор ошибок, шаблонизацию и переход от одного URL-а к другому без перезагрузки страницы.
Single-Page
Интерфейс главной страницы «Mail.Ru для бизнеса» достаточно простой и удобный. Именно на этой странице начинается процесс добавления доменов. После регистрации первого домена вам станет доступна админка. Всё вместе — главная и админка (и другие скрытые страницы) – это одно полноценное single-page приложение.
Для реализации single-page приложения были использованы техники, которые с успехом применялись при разработке Почты, Календаря и Адресной книги. Например, отрисовка страниц происходит на клиентской стороне с помощью шаблонизатора Fest (подробнее о нем – чуть ниже), а всё общение с сервером ограничивается обращением к методам API.
Некоторые же приёмы, наоборот, были применены впервые, и я очень рада, что мне удалось привнести что-то новое в процессе разработки :)
Мультиавторизация
Помимо красоты и удобства, single-page открывает нам доступ к плюшкам мультиавторизации. Мультиавторизация у нас есть и на основной Почте; для новой же почты с доменами и красивостями это, как говорится, то, что доктор прописал. Можно одновременно сидеть в корпоративном и личном ящике, и быстро переключаться между ними. При этом опять-таки не происходит перезагрузки страницы.
CSS-анимация
Мы решили отказаться от поддержки старых браузеров, поскольку считаем, что администраторы доменов — достаточно продвинутые пользователи. Например, IE мы поддерживаем, начиная с 8-й версии и выше. Благодаря этому у нас развязались руки в плане использования современных плюшек. Особо выделяется среди них CSS-анимация. Как известно, пользователи любят, чтобы на странице все было красиво и плавно. Можно, конечно, это «красиво и плавно» реализовать теми же скриптами, но если браузер сам дает нам возможность организовать динамику стандартными средствами, почему бы этим не воспользоваться?
Основная работа по реализации анимации выполняется с помощью CSS.
Скриптом мы только запускаем ее в нужные нам моменты.
Но мы решили пойти дальше и сделать анимацию даже для тех пользователей, которые пользуются браузерами, не поддерживающими эти замечательные CSS-плюшки. Для них мы делаем анимацию средствами jQuery. Для того, чтобы определить, поддерживает ли браузер CSS-красоту или нет, мы используем стандартную браузерную функцию CSS.supports:
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, содержащий нужный адрес, а после авторизации мы перенаправим его туда, куда ему хотелось попасть — таким образом, ему не придется снова переходить ручками. Вся логика переходов выполняется в браузере, без перезагрузки страницы, при этом адрес страницы выглядит «по-человечески», без хешей (“#”).
Затем, после авторизации, мы понимаем, куда хотел отправиться пользователь, и перекидываем его в место назначения.
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 перед каждой отправкой ошибки на сервер:
В этом примере мы проверяем, не запушены ли мы в iframe’ме, версию jQuery (если она вообще загружена) и код setTimeout (т.к. он может быть подменён). Мы же собираем чуть больше статистической информации для правильной диагностики ошибок. Значение свойства «sentry.interfaces.User» будет отображаться в интерфейсе Sentry в специальном поле.
Discuss
Так работает наша Почта для бизнеса. Надеюсь, она будет радовать пользователей красотой, удобством и функциональностью. Подробнее про «Mail.Ru для бизнеса» можно почитать прямо здесь. Напомню, что сервис сейчас в бете и мы будем рады вашим отзывам и предложениям по почте feedback@biz.mail.ru или в комментариях к этому посту.
С удовольствием окажем любую поддержку пользователям Хабра, которые захотят попробовать наш новый сервис.
Ольга Алексеева,
Егор Халимоненко (termi)
Группа frontend-разработки Почты
Под катом — про 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-разработки Почты