В феврале 2014 года мы выпустили Debug Mail — почтовый сервер для тестирования email-рассылок, который перехватывает, сохраняет и отображает в веб-интерфейсе весь почтовый трафик. Реальные письма никому не отправляются, а результатами легко можно поделиться с коллегами, без форвардинга и забитых отладочными письмами личных почтовых ящиков.
Наш тимлид Юра Шиканов написал о серверной архитектуре Debug Mail. Мы также рассказали о возможностях и фунциональности сервиса. Здесь я расскажу о технологиях, которые мы использовали на клиенте.
Мы планировали запустить минимальную жизнеспособную версию Debug Mail в кратчайшие сроки, чтобы как можно раньше начать использовать сервис для решения реальных задач и получить первую обратную связь от пользователей. Разработка дизайна заняла около месяца, и фронтенд нужно было сделать также быстро.
Поскольку Debug Mail с точки зрения архитектуры выглядел как типовое REST-приложение, для ускорения разработки мы решили использовать AngularJS — фрейморк, ориентированный на такой тип задач.
В AngularJS есть немало концепций, для понимания которых новичку потребуется определенное время. Неполный список включает расширенные шаблоны, директивы, области видимости, компиляторы, фильтры, отображения, дата-байндинг, зависимости, сервисы. А ко всему этому желательно знать так называемые лучшие практики, общие рекомендации по использованию всего цирка и коней.
Но и платит за эти сакральные знания фреймворк очень щедро, в разы сокращая объем шаблонного связующего кода и ускоряя разработку и тестирование. Это удалось ощутить в первом же нашем серьезном проекте на AngularJS.
Мы делали инструмент для разработчиков и их менеджеров — целевой аудитории, которая в подавляющем большинстве случаев использует современные браузеры. Поэтому вопроса поддержки античных браузеров не стояло в принципе.
Это развязало руки для того, чтобы использовать, например, технологию Флекс для верстки основного макета. С Флексом было легко добиться соблюдения сложной пропорции ширины трех основных колонок рабочей области сервиса при разных размерах окна.
На самую сложную часть макета четыре строчки простого кода без лишних атрибутов с префиксами.
.projects-section_list {
display: flex;
flex-wrap: nowrap;
height: 100%;
}
.projects-section_item__projects {
flex: 1 1 200px;
height: 100%;
}
.projects-section_item__mails {
flex: 1 1 280px;
height: 100%;
}
.projects-section_item__content {
flex: 1 1 544px;
height: 100%;
}
По той же причине мы повсеместно использовали векторную графику в логотипах и иконках без каких-то дополнительных костылей.
На момент написания статьи, для того чтобы Флексы работали не в 60%, а в 80% браузеров требовались атрибуты с префиксами. А учитывая то, что стандарты — штука живая, за таким кодом нужен глаз да глаз. Как знать, может быть в очередной сборке Webkit текущий синтаксис начнет сбоить?
Но где можно, пусть трудятся роботы. Слова автора Автопрефиксера ничуть не преувеличены, на данный момент это действительно окончательное решение проблемы префиксов в CSS. Нужен только чистый CSS, а вся, местами неочевидная, логика расстановки префиксов остается под капотом регулярно обновляемого Автопрефиксера. Код стало легче читать, а мы экономили время для более важных задач.
Сравните с кодом выше.
.projects-section_item__projects {
-webkit-box-flex: 1;
-webkit-flex: 1 1 200px;
-ms-flex: 1 1 200px;
flex: 1 1 200px;
height: 100%;
}
.projects-section_item__mails {
-webkit-box-flex: 1;
-webkit-flex: 1 1 280px;
-ms-flex: 1 1 280px;
flex: 1 1 280px;
height: 100%;
}
.projects-section_item__content {
-webkit-box-flex: 1;
-webkit-flex: 1 1 544px;
-ms-flex: 1 1 544px;
flex: 1 1 544px;
height: 100%
}
Экономил время и старый добрый Grunt. В Debug Mail мы использовали его для следующих рутин:
Дополнительно ускорял работу принцип непрерывной интеграции и автоматическое развертывание обновлений из master-ветки репозитория на тестовом, а потом на боевом сервере. Об этом еще будет отдельный пост. В итоге в репозитории всегда был чистый и модульный код, а на продакшене — автоматически собранное, аккуратное, быстрое, оттестированное приложение.
Макет у сервиса не сложный, без изощренных дизайнерских изысков. Но бывает, что верстка некоторых едва заметных деталей макета может отнять чуть ли не больше времени, чем весь остальной макет. И в то же время, именно эти аккуратно выверенные детали и создают общее впечатление от сервиса. У нас так получилось с выпадающими менюшками, где ситуация осложнялась фигурной полупрозрачной скругленной границей блока.
Это хороший пример ситуации, когда для уменьшения времени на верстку можно найти с дизайнером компромисс и немного упростить макет. В результате компромисса выпадающее меню осталось прежним, но с непрозрачной границей. До лучших времен.
А вот программная реализация этого меню заняла заметно больше времени, и там уже не было места для компромиссов. Всем привычный jQuery использовать с Angular часто не рекомендуют, он в большинстве случаев избыточен и нередко указывает на отход от ключевых концепций фреймворка. К тому же последний вполне самодостаточен, отлично решает любые задачи без любых библиотек. Если знать как.
Решать пришлось сразу несколько проблем. Нужно было защитить выпадающее меню от влияния возможного overflow:hidden на родительских элементах, то есть выводить его где-то на верхних уровнях DOM и уже программно привязывать его расположение к нужным элементам в интерфейсе. И при этом корректно передавать в каждое меню данные из модели. И еще сделать все это по-ангуляровски.
Рекомендованный способ работы с DOM в AngularJS — создание собственных директив. Поэтому потребовалось кропотливое доскональное изучение документации и всех уникальных подходов, о которых упоминалось выше. А за этим последовало неизбежное хождение по граблям и отлов ошибок, связанных, например, с устройством областей видимости, шаблонами, оперированием DOM с помощью довольно-таки ограниченного jqLite.
К слову, насчет понимания директив. Если кто-то, кто не знаком с Angular, навскидку сможет объяснить суть вот этого примера кода из официальной документации, я буду искренне восхищен.
angular.module('docsTabsExample', []).directive('myPane', function() {
return {
require: ['^myTabs', '^ngModel'], restrict: 'E', transclude: true, scope: {
title: '@'
}
, link: function(scope, element, attrs, controllers) {
var tabsCtrl=controllers[0], modelCtrl=controllers[1]; tabsCtrl.addPane(scope);
}
, templateUrl: 'my-pane.html'
}
;
}
);
Для тех же, кто дойдет до конца, все усилия окупятся с лихвой. На выходе в шаблоне будет один простой атрибут. Все будет просто работать.
< span ng - if="project.is_owner"dropdown - list="contextMenuGet(project.id, project.title)"
class="mail-projects_context">< /span>
В паре мест в приложении мы выводим текст, который предполагает, что его сразу скопируют. Например, ссылка для общего доступа к письму или настройки для внешних приложений.
Автоматическая работа с буфером обмена в браузерах по умолчанию запрещена, ведь в буфере обмена могут быть пароли, номера кредиток и другие важные данные. И несмотря на то, что при помощи флеша можно обойти это ограничение, мы предпочли этого не делать и придерживаться открытых стандартов, внеся свою маленькую лепту в общий замечательный тренд.
Чтобы все-таки скрасить быт любимого пользователя, можно выделять текст, который нужно скопировать: при клике, табуляции в инпут или по готовности данных, если они запрашиваются асинхронно. Пара простых директив, и все довольны.
< input class = 'dropdown_input'
type = 'text'
ng - model = 'dropdownInput'
readonly select - on - click select - on - ready >
DebugMail.directive("selectOnClick", function() {
return function(scope, $el) {
return $el.on("click", function() {
return this.select();
});
};
});
DebugMail.directive("selectOnReady", function() {
return {
priority: 1,
require: ["?ngModel"],
restrict: "A",
link: function(scope, $el) {
return scope.$watch(function() {
return $el.select();
});
}
};
});
Письма в Debug Mail отображаются и в текстовом виде, и в HTML в зависимости от содержимого исходного письма. И если в первом случае мы легко обезопасили контент на сервере, то для вывода HTML ничего лучше старого доброго iframe было не придумать. Простая директива решила проблему.
<div class="mail-content_content_safe" safe-content="data.contents.html"></div>
DebugMail.directive("safeContent", function() {
return {
restrict: "A",
replace: true,
scope: {
safeContent: "="
}
,
template: "<iframe></iframe>",
controller: ["$scope", "$element", "$attrs", function($scope, $element, $attrs) {
return $scope.$watch("safeContent", function(safeContent) {
var anchor, anchors, iframeDoc, _i, _len, _results;
if (safeContent) {
iframeDoc = $element[0].contentWindow.document;
iframeDoc.open();
iframeDoc.write(safeContent);
iframeDoc.close();
anchors = $element[0].contentWindow.document.getElementsByTagName("a");
_results = [];
for (_i = 0, _len = anchors.length; _i < _len; _i++) {
anchor = anchors[_i];
_results.push(anchor.setAttribute("target", "_blank"));
}
return _results;
}
}
);
}
]
}
;
}
);
Нам было важно, чтобы приложение в реальном времени показывало новые и непрочитанные письма вне зависимости от того, где в интерфейсе находился бы пользователь.
Для этого мы использовали библиотеку Faye, основанную на протоколе двунаправленного асинхронного обмена данными Bayeux.
На все про все пара строк клиентского кода, да плюс callback на получение сообщений от сервера.
Автозаполнение полей форм в браузерах — функция отличная, пользуемся ей каждый день. Но вот стандартами она жестко не регламентирована, а потому далека от совершенства. Как минимум в последних сборках Chrome и Firefox срабатывание автозаполнения не порождает событий change, в результате чего Angular не обновляет свои скоупы, в результате чего простые и удобные штуки вроде автоматической валидации формы в примере ниже не срабатывают.
<form ng-submit="signIn()" name="formSignIn" class="auth-form">
<label class="auth-label" for="email">Email</label>
<input class="auth-input" ng-model="email" name="email" id="email" type="email" required autofocus>
<label class="auth-label" for="password">Password</label>
<input class="auth-input" ng-model="password" name="password" id="password" type="password" required>
<input type="submit" class="button-submit auth-button" value="Sign In" ng-disabled="formSignIn.$invalid">
</form>
Проблема увлекательно описана в соответствующем баге в трекере AngularJS и продублирована в трекерах Гугла и Мозиллы.
Но поскольку нам ехать, а не шашечки, пришлось использовать временное решение — библиотеку autofill-event.
Для работы с зависимостями мы использовали Bower. Но, как уже упоминалось выше, AngularJS весьма самодостаточен и список зависимостей Debug Mail получился очень скромный:
Есть внушительный список возможностей, которые мы хотим добавить в Debug Mail, но мы рассчитываем делать это, руководствуясь не только собственными потребностями, но и опираясь на ваши отзывы. Сбросьте нам пару строк на ask@wbtech.pro. Что нравится, что не нравится, чего не хватает?
Если вы хотите реализовать подобный проект, узнайте, сколько стоила разработка Debug Mail в WB—Tech.
Никакого спама, только анонсы новых статей
ИП Гришанин Кирилл Олегович
ИНН 774313842609
Б. Новодмитровская ул., 36, стр. 12, вход 6,
Москва, Россия, 127015
Ahad Ha'am 54,Tel Aviv-Yafo,Израиль