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

February 02 2023
Фреймворк
Верстка для современных браузеров
Роботы вкалывают
Маленький дропдаун — большие проблемы
Работа с буфером обмена
Безопасный вывод содержимого писем
Обновления в реальном времени
Автофилл
Зависимости
Напишите нам что-нибудь

В феврале 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-ветки репозитория на тестовом, а потом на боевом сервере. Об этом еще будет отдельный пост. В итоге в репозитории всегда был чистый и модульный код, а на продакшене — автоматически собранное, аккуратное, быстрое, оттестированное приложение.

Маленький дропдаун — большие проблемы

Макет у сервиса не сложный, без изощренных дизайнерских изысков. Но бывает, что верстка некоторых едва заметных деталей макета может отнять чуть ли не больше времени, чем весь остальной макет. И в то же время, именно эти аккуратно выверенные детали и создают общее впечатление от сервиса. У нас так получилось с выпадающими менюшками, где ситуация осложнялась фигурной полупрозрачной скругленной границей блока.

Меню в DebugMail со скругленной непрозрачной границей блока.

Это хороший пример ситуации, когда для уменьшения времени на верстку можно найти с дизайнером компромисс и немного упростить макет. В результате компромисса выпадающее меню осталось прежним, но с непрозрачной границей. До лучших времен.

А вот программная реализация этого меню заняла заметно больше времени, и там уже не было места для компромиссов. Всем привычный 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.

Автор статьи
Кирилл Гришанин
Последние 10 лет руковожу командой аналитиков, дизайнеров и разработчиков

Подпишитесь на блог WB—Tech

Никакого спама, только анонсы новых статей

    Последние статьи

    Миграция внутренних пользователей Jira в новую директорию с сохранением данных об активности

    Рассказали, как осуществили перенос пользовательских данных из Jira (Internal Directory) в директорию Microsoft Active Directory.

    Как эффективно хранить и актуализировать корпоративные данные средствами low/no-code

    Рассказали, как организовали поток HR-данных, чтобы оргструктура и бонусно-бухгалтерские расчеты всегда были актуальны.

    Мало кода, больше результативности: платформы low-code и no-code

    О low-code и no-code платформах, примерах использования и разбор нужно ли быть программистом.

    ИП Гришанин Кирилл Олегович
    ИНН 774313842609

    Коворкинг Starthub

    Б. Новодмитровская ул., 36, стр. 12, вход 6,
    Москва, Россия, 127015

    Коворкинг Wework

    Ahad Ha'am 54,Tel Aviv-Yafo,Израиль

    © 2023 WB—Tech. Мы разрабатываем уникальные решения для компаний из России, США и Европы.