Мы находимся в интересной фазе переосмысления внешнего интерфейса для торговых площадок eBay, и в этом блоге кратко изложено, куда мы движемся.

Модульное программирование — это фундаментальная техника проектирования, которая применялась на заре разработки программного обеспечения. Это по-прежнему наиболее рекомендуемый шаблон для создания поддерживаемого программного обеспечения, и сообщество Node.js полностью разделяет эту философию проектирования. Большинство модулей Node.js создаются с использованием меньших модулей в качестве строительных блоков для достижения конечной цели. Теперь, когда Веб-компоненты набирают обороты, мы решили изменить наш подход к созданию интерфейсных веб-приложений.

Модульность уже была в нашей кодовой базе внешнего интерфейса, но только в рамках конкретного языка. Большая часть нашего общего JavaScript (оверлеи, вкладки, карусель и т. д.) и CSS (кнопки, сетка, формы, значки и т. д.) были написаны по модульному принципу. Это здорово, но когда дело доходит до страницы или представления, мысль по-прежнему была о создании страниц, а не о создании модулей пользовательского интерфейса. С таким мышлением мы обнаружили, что по мере роста сложности страниц становится экспоненциально труднее поддерживать их. Нам нужен был простой способ разделить страницу на маленькие и управляемые части и разработать каждую часть отдельно. Именно тогда мы придумали идею: «Не создавайте страницы, создавайте модули».

Модульное мышление

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

Декомпозиция. Во-первых, мы хотели отказаться от идеи непосредственного построения страницы. Вместо этого, когда требование приходит в виде страницы, мы разбиваем его на логические модули пользовательского интерфейса. Мы делаем это рекурсивно, пока модуль не станет ПЕРВЫМ. Это означает, что страница состоит из нескольких модулей верхнего уровня, которые, в свою очередь, строятся из подмодулей, очень похоже на то, как модули JavaScript создаются в мире Node.js. Существуют общие стили и JavaScript (например, jQuery), от которых зависят все модули. Эти файлы вместе становятся самостоятельным модулем (например, базовым модулем) и добавляются как зависимости других модулей. Инженеры начинают работать над этими модулями самостоятельно, а страница — не что иное, как сетка, которая окончательно их собирает.

Инкапсуляция DOM: мы хотели, чтобы все наши внешние модули были связаны с узлом DOM и чтобы этот узел был корневым элементом модуля. Таким образом, мы помещаем все поведение JavaScript на стороне клиента, такое как привязка событий, запросы к DOM, триггеры подключаемых модулей jQuery и т. д., в рамках корневого элемента модуля. Это обеспечивает идеальную инкапсуляцию наших модулей, делая их ограниченными и действительно независимыми. Очевидно, нам нужна какая-то абстракция JavaScript для достижения этой инкапсуляции, и мы решили использовать облегченную функциональность виджетов (названную Marko Widgets), предлагаемую RaptorJS. Виджеты Marko представляют собой небольшой модуль (около 4 КБ), предоставляющий простой механизм для создания экземпляров виджетов и их привязки к элементам DOM. Экземпляры виджетов превращаются в наблюдаемые, а также могут быть уничтожены, когда их нужно удалить из DOM. Чтобы привязать модуль JavaScript к узлу DOM, мы просто использовали директиву w-bind, как показано ниже:

<div class="my-module" w-bind="./my-module-widget.js"> ... </div>

При отображении модуль виджетов Marko отслеживает, какие модули связаны с какими узлами DOM, и автоматически привязывает поведение после добавления HTML в DOM. Некоторые из наших модулей, например модуль отслеживания, имели функциональные возможности JavaScript, но не имели связи с узлами DOM. В таких случаях мы используем тег ‹noscript› для достижения инкапсуляции. Что касается CSS, мы используем пространство имен для всех имен классов в модуле с именем класса корневого элемента, разделенным «-», например, название галереи, миниатюра галереи и т. д.

Упаковка. Следующий большой вопрос заключался в том, как мы упаковываем модули? Управление пакетами во внешнем интерфейсе всегда было сложной задачей и является горячо обсуждаемой темой. Упаковка для одного и того же типа активов довольно проста. Например, в JavaScript, когда мы определяем шаблон модуля (CommonJS, AMD и т. д.), упаковка становится проще с помощью таких инструментов, как browserify. Проблема заключается в том, что нам нужно связать другие типы ресурсов, такие как CSS и шаблоны разметки. Здесь на помощь пришел наш фирменный Оптимизатор Raptor. Оптимизатор — это сборщик модулей JavaScript, очень похожий на browserify или webpack, но с некоторыми отличиями, которые делают его идеальным для нашей модульной экосистемы. Все, что ему нужно, это файл оптимизатора.json в каталоге модуля, чтобы перечислить зависимости CSS и шаблона разметки (пыль или марко). Для зависимостей JavaScript оптимизатор сканирует исходный код в текущем каталоге и рекурсивно разрешает их. Наконец, в слот CSS и JavaScript на странице вставляется упорядоченный пакет без дубликатов, например:

[ "./base", "gallery.less", "gallery.html" ]

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

Организация файлов

Переход на модульность также означал изменение структуры файлов. Прежде чем применять модульность к кодовой базе внешнего интерфейса, команды обычно создавали отдельные каталоги верхнего уровня для JavaScript, CSS, изображений, шрифтов и т. д. Но с новым подходом имело смысл сгруппировать все файлы, связанные с модулем, в одном каталоге и использовать имя модуля в качестве имени каталога. Первоначально эта практика вызывала некоторые опасения, в основном связанные с нарушением проверенной схемы структурирования файлов и изменениями инструментов, связанными с пакетированием и отправкой через CDN. Но инженеры быстро пришли к соглашению, так как преимущества явно перевешивали недостатки. Самым большим преимуществом является то, что новая структура действительно способствовала инкапсуляции на уровне модулей: все файлы, связанные с модулями, живут вместе и могут быть упакованы. Кроме того, любые действия с модулем (удаление, переименование, рефакторинг и т. д., которые часто происходят в больших кодовых базах) становятся очень простыми.

Модуль связи

Мы хотели, чтобы все наши модули следовали Закону Деметры — это означает, что два независимых модуля не могут напрямую общаться друг с другом. Очевидным решением было использование шины событий для связи между клиентскими модулями. Мы оценили различные механизмы обработки событий с целью сделать их централизованными, а также не вводить большую зависимость от библиотек. Удивительно, но мы остановились на модели обработки событий, которая поставляется с самой jQuery. API-интерфейсы триггер, вкл и выкл jQuery проделывают фантастическую работу по абстрагированию всех сложностей событий как для DOM, так и для пользовательских событий. Мы написали небольшую диспетчерскую оболочку, которая обрабатывает взаимодействие между модулями, инициируя и прослушивая события в элементе документа:

(function($) { 'use strict'; var $document = $(document.documentElement); // Create the dispatcher $.dispatcher = $.dispatcher || {}; var dispatcherMethods = { trigger: function(event, data, elem) { // If element is provided trigger from element if(elem) { // Wrap in jQuery and call trigger return $(elem).trigger(event, data); } else { return $document.trigger(event, data); } }, on: function(event, callback, scope) { return $document.on(event, $.proxy(callback, scope || $document)); }, off: function(event) { return $document.off(event); } }; // dispatcherMethods end // Attach the dispatcher methods to $.dispatcher $.extend(true, $.dispatcher, dispatcherMethods); })(jQuery);

Модули теперь могут использовать $.dispatcher для запуска и прослушивания пользовательских событий, не зная о других модулях. Еще одно преимущество использования модели обработки событий на основе jQuery DOM заключается в том, что мы бесплатно получаем всю динамику событий (распространение и интервал между именами).

// Module 1 firing a custom event 'sliderSwiped' $.dispatcher.trigger('sliderSwiped', { activeItemId: 1234 }); // Module 2 listening on 'sliderSwiped' and performing an action $.dispatcher.on('sliderSwiped', function(evt, data) { fetchItem(data.activeItemId); });

Некоторые команды предпочитают создавать централизованный модуль посредника для управления коммуникацией. Мы оставляем это на усмотрение инженеров.

Стандартизация мультиэкрана и модели просмотра

Одним из самых больших преимуществ интерфейсных модулей является то, что они идеально вписываются в многоэкранный мир. Потоки меняются в зависимости от размеров устройства, и сделать так, чтобы страница работала адаптивно или адаптивно на всех устройствах, нецелесообразно. Но с модулями все становится на свои места. Когда инженеры завершают работу над модулями в представлении, они также оценивают, как они выглядят и ведут себя на разных размерах экрана. На основе этой оценки согласовываются имя модуля и соответствующая схема JSON модели представления. Но реализация модуля основана на устройстве. Для некоторых модулей достаточно просто адаптивной реализации, чтобы они работали на всех экранах. Для других дизайн и взаимодействие (сенсорное или бесконтактное) будут совершенно другими, что потребует других реализаций. Какими бы разными ни были реализации, имя модуля и модель представления, поддерживающая его, будут одинаковыми.

Мы действительно распространили эту концепцию на родной мир, где приложениям iOS и Android также требовались одни и те же модули и модели представления. Но реализация по-прежнему является родной (Objective-C или Java) для платформы. Все клиенты взаимодействуют с внешними серверами, которые осведомлены о модулях, необходимых конкретному пользовательскому агенту, и отвечают соответствующими фрагментами модели представления. Этот подход дал нам идеальный баланс с точки зрения согласованности и хорошего пользовательского опыта (без ущерба для реализации). Такие компании, как LinkedIn, уже внедрили модель JSON на основе представлений, которая оказалась успешной. Степень детализации модели представления определяется инженерами и менеджерами по продукту вместе, в зависимости от того, какой уровень контроля над модулем им нужен. Общее правило состоит в том, чтобы сделать JSON модели представления как можно более умным, а модули — тупыми (или тонкими), тем самым обеспечивая центральное место для управления всеми клиентами.

Сопутствующие преимущества

Все остальные преимущества модульного программирования предоставляются бесплатно:

  • Продуктивность разработчиков — инженеры могут работать параллельно над небольшими частями, что приводит к более быстрому темпу.
  • Модульное тестирование — это никогда не было проще.
  • Отладка — легко устранить проблему, и даже если один модуль неисправен, другие остаются целыми.

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

Вывод

Как упоминалось ранее, мы действительно находимся в процессе переосмысления разработки интерфейса в eBay, и модульность — один из первых шагов, вытекающих из этого переосмысления. Спасибо моим коллегам Махди Педрамрази и Патрику Стил-Идему за то, что они объединились и возглавили эту работу во всей организации.

- Сентил
Frontend Engineer

Первоначально опубликовано на www.ebaytechblog.com 2 октября 2014 г.