Как Fiverr уравновешивает совместное использование ресурсов и независимость компонентов в децентрализованной интерфейсной системе.

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

В нашей организации многие из этих компонентов используют очень похожие технологические стеки (React, Redux, Lodash…), но мы по-прежнему хотим, чтобы они оставались независимыми и не зависели от ресурсов, находящихся вне их контроля. Некоторые компоненты могут оставаться как есть без обслуживания в течение длительного времени, в то время как другие часто меняются - новые компоненты и функции создаются постоянно. Мы хотим разместить их всех.

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

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

В системе мы распознали наиболее часто используемые ресурсы и разделили их на 3 группы:

  1. Поставщики - большие библиотеки и фреймворки, обычно хорошо известные проекты с открытым исходным кодом, которые имеют достаточно низкие циклы выпуска основных версий.
  2. Высокая повторяемость - модули, которые настолько хорошо адаптированы к нашим потребностям в разработке, что стали их второй натурой для разработчиков и, как ожидается, будут доступны во всех средах, в том числе в нашем i18n решение и статистические отчеты, такие как технический и бизнес-мониторинг.
  3. Разные утилиты - эти утилиты могут иметь различные уровни повторного использования. Они включают вспомогательные функции, полезные классы, повторяемую бизнес-логику или технические функциональные операции, которые не имеют решающего отношения к какой-либо функции или предметной области.

1. Продавцы

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

Компоненты объявляют зависимости, которые они намереваются использовать, из заранее определенного списка (сам список поддерживается как общая зависимость). Это своего рода контракт между компонентами и сервисами. Например; компонент A объявит: react, react-dom, lodash, а компонент B объявит react, react-dom, redux. Они также обязательно включают эти зависимости как внешние компоненты Webpack для соответствующих сборок, ориентированных на браузер, поэтому они не включаются в связанные решения кода. Наконец, они должны исходить из того, что будущие обновления основной версии находятся вне их контроля, поэтому экспериментальные функции обычно не рассматриваются.

vendors.json

{
  "lodash": "_",
  "react": "React",
  "react-dom": "ReactDOM",
  "react-redux": "ReactRedux",
  "redux": "Redux"
}

externals.js

module.exports = Object.entries(require('./vendors.json'))
  .reduce(
    (collection, [route, name]) => Object.assign(
      collection,
      {
        [route]: {
          'commonjs': route,
          'commonjs2': route,
          'amd': route,
          'root': name,
          'var': name,
        },
      }
    ),
    {}
  );

Приложения шлюза, которые используют эти компоненты, являются владельцами среды выполнения; они подделывают HTML-документы. Они знают всех поставщиков, поскольку используют один и тот же пакет «поставщиков». Они также могут свободно обновлять патчи и минорные версии, при условии, что серьезных поломок не произойдет. Они будут учитывать требования всех компонентов, загружать только необходимые скрипты поставщика и либо отображать их в окне, либо в глобальном.

Что мы обнаружили особенно полезным в этом подходе, так это то, что поскольку мы передаем представление всех скриптов поставщика из одного места по конвейеру, мы можем накладывать глобальные имена. Первоначально мы представляли React как React, но решили, что лучше указать, какую основную версию мы используем, поэтому React будет представлен как window.React15.

Поскольку все компоненты используют внешние элементы Webpack, глобальное имя неоднозначно для самого кода - они просто import React from 'react’, а их конфигурации будут указывать на желаемую версию. Такой подход позволяет нам переходить постепенно и использовать одну и ту же страницу для обеих версий React без конфликтов. Однако мы решили установить крайний срок миграции в компании, чтобы со временем не раздувать скрипты поставщиков.

2. Высокая повторяемость

В конечном итоге мы решили разбить эту группу и разделить инструменты между двумя существующими решениями - «Vendors» и «Miscellaneous Utilities» - в зависимости от размера, зрелости и вероятности критических изменений. На момент написания «Поставщики» включают только две из наших собственных зависимостей, которые мы ранее рассматривали: группу «Высокая повторяемость», наше решение i18n и метод декоратора для API выборки браузера.

Остальные наши утилиты, как бы часто они ни использовались, были объединены в библиотеку утилит для использования и объединения в отдельные компоненты (см. Ниже).

3. Разные утилиты

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

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

Наше решение предлагает служебную библиотеку в виде необработанных модулей Harmony. Каждый потребитель связывает все, что ему нужно от этих модулей, и внутренняя связь между модулями также остается неизменной. Это обеспечивает оптимальное решение для каждого отдельного компонента.

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

На библиотеку утилит накладывается ограничение - она ​​должна использовать только широко поддерживаемые функции Javascript, экспериментальные функции могут не подпадать под процесс «транспиляции» потребителя (например, плагины babel).

Утилита

export const resolve = (string = '', context = global) =>
  string
  .split('.')
  .reduce((prev, current) =>
    typeof prev === 'object' ? prev[current] : prev, context);

Утилиты root

export * from './deepAssign';
export * from './env';
export * from './inTimeRange';
export * from './multiEventListener';
export * from './pluck';
export * from './resolve';
export * from './select';
export * from './sendEvents';

Все модули экспортируются как именованные модули. Сохранение имен методов полезно для межпроектного сопровождения и имеет решающее значение для способности разработчиков переходить к проектам и выходить из них. Это также предотвращает конфликты имен.

По этой причине, а также для упрощения использования, независимо от того, находятся ли они в пространстве имен или вложены в каталоги, модули отображаются в корне служебной библиотеки.

import { inTimeRange, env, resolve } from '@fiverr/util';

Библиотека утилит оптимизирована для создания функциональных модулей и очищает код функции от технических операций. Этот репозиторий также охвачен тестовой средой с реальными браузерами (включая Edge!), Создает автоматизированную документацию и, как правило, оптимален для разработки функциональных блоков кода. Разработчикам рекомендуется использовать его даже для модулей без возможности повторного использования.

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

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

Bonus Gotcha - Webpack исключить / включить

Обычно вывод зависимостей исключается из процесса «транспиляции» (например, babel). Потребители необработанных модулей Harmony должны «исключить» утилиты из своего процесса. Этот пункт также относится к настройкам Jest и любому другому транспилятору.

const MODULES_TO_INCLUDE = ['@fiverr/util'];
const exclude = new RegExp(`node_modules/(?!(${MODULES_TO_INCLUDE.join('|')})/).*`);
...
  module: {
    rules: [{
      loader: 'babel-loader',
      exclude,