Создание фронтенда для Debug Mail

Создание фронтенда для Debug Mail

В феврале 2014 года мы выпустили Debug Mail — почтовый сервер для тестирования email-рассылок, который перехватывает, сохраняет и отображает в веб-интерфейсе весь почтовый трафик. Реальные письма никому не отправляются, а результатами легко можно поделиться с коллегами, без форвардинга и забитых отладочными письмами личных почтовых ящиков.

Наш тимлид Юра Шиканов написал о серверной архитектуре Debug Mail. Мы также рассказали о возможностях и фунциональности сервиса. Здесь я расскажу о технологиях, которые мы использовали на клиенте.

Фреймворк

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

Поскольку Debug Mail с точки зрения архитектуры выглядел как типовое REST-приложение, для ускорения разработки мы решили использовать AngularJS — фрейморк, ориентированный на такой тип задач.

  • Нас вдохновили отличные примеры и концепция MWW, а также наглядный код.
  • Angular поддерживают талантливые ребята из Гугла, и интерес к фреймворку среди разработчиков с каждым месяцем стабильно растёт, а на StackOverflow отличное комьюнити, где почти любую проблему можно решить за несколько часов. Было время, там отвечали и сами создатели фреймворка.
  • У Angular отличная документация, большая база примеров и готовых компонентов, вышло много неплохих книжек и туториалов.

В 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 мы использовали его для следующих рутин:

  • Компиляция SASS в CSS.
  • Запуск Автопрефиксера для CSS.
  • Минимизация CSS.
  • Компиляция Coffee в JS.
  • Конкатенация JS.
  • Минимизация JS.

Дополнительно ускорял работу принцип непрерывной интеграции и автоматическое развёртывание обновлений из 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 получился очень скромный:

  • angular-route — для организации системы URL и связанных с ними контроллеров и шаблонов,
  • angular-mock — для эмуляции работы сервера и удобной локальной разработки,
  • angular-faye — для общения с сервером в реальном времени,
  • raven.js — для уведомлений об ошибках на стороне клиента через Sentry,
  • moment.js — для удобной работы с датой и временем,
  • autofill-event — для решения проблемы с автозаполнением полей форм, не приводящего к изменению модели.

Напишите нам что-нибудь

Есть внушительный список возможностей, которые мы хотим добавить в Debug Mail, но мы рассчитываем делать это, руководствуясь не только собственными потребностями, но и опираясь на ваши отзывы. Сбросьте нам пару строк на ask@wbtech.pro. Что нравится, что не нравится, чего не хватает?

Если вы хотите реализовать подобный проект, узнайте, сколько стоила разработка Debug Mail в WB—Tech.