Импорт фрагментов из других пакетов Webpack во время выполнения, используя их, как если бы они были всегда - приложения с чередованием

Обновление: этот проект переписывается и будет включен в Webpack 5!





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

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

Контекст

Создание современных распределенных приложений JavaScript - сложная задача. Управление несколькими репозиториями, сборками и совместным использованием кода - сложная задача, выполняемая вручную. Я хочу создавать автономные микро-интерфейсные приложения, которые работают вместе как одно в браузере.

Вот дополнительный контекст: https://github.com/webpack/webpack/issues/8524

Многие сборки веб-пакетов действуют как единое целое, когда они открыты в браузере, без контекста в качестве времени сборки.

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



оркестровка Javascript

Масштабируемое управление универсальными микроприложениями.

Чередование времени выполнения

Проще говоря, я хочу объединить два манифеста Webpack во время выполнения и заставить их работать вместе, как если бы он был скомпилирован как один SPA с самого начала. Вроде как DLLPlugin, но без требования передачи контекста во время сборки - скорее передача контекста во время выполнения.

in · ter · leave
in (t) ərˈlēv - глагол
1)
вставлять страницы, обычно пустые, между страницами (книги).
2) микшировать (два или более цифровых сигнала), чередуя их.

Это позволило бы микро-интерфейсам (или MFE, как я их называю) работать вместе без проблем.

Интеллектуальное разделение кода между сборками. Динамический импорт URL-адресов, без перезагрузки страницы

Цели

Чередование необходимо для достижения следующих

  • При маршрутизации к другому MFE страница не обновляется, несколько приложений должны маршрутизироваться, как один SPA.
  • Не загружайте повторно код поставщика, который уже предоставлен другой сборкой Webpack на странице. (Не объединяйте несколько копий одной и той же зависимости)
  • Каждый MFE должен быть полностью автономным и не иметь централизованной зависимости. Я не хочу делиться кодом, управляя внешними объектами Webpack или частями обычных поставщиков.
  • Ресурсы внешнего интерфейса должны иметь возможность постоянно обновляться, не требуя от потребителя повторной установки.
  • Мне не нужно повторно развертывать весь парк из-за изменения общего компонента или чего-то, что управляется другой командой (например, навигация, я не хочу повторно развертывать весь свой парк всякий раз, когда они запускают новое обновление)
  • Управление оркестровкой должно полностью осуществляться на уровне пользователя, что позволяет динамическую адаптацию в зависимости от того, какие пакеты JavaScript загружаются на страницу. Не должно быть никакой удаленной логики или вызовов, кроме добавления статического JavaScript, такого как сами пакеты.

Во избежание путаницы - допустим, у вас есть два микро-интерфейса или два отдельных приложения. У них есть собственная сборка Webpack, они работают на собственных серверах и полностью автономны. Назовем их App1 и App2. Я хочу использовать Nav из App1 внутри App2.

Вы можете просто взять Nav, сделать его пакетом npm и установить обратно в App1 и App2, совместное использование решено. Но вам все равно придется повторно развертывать несколько приложений каждый раз, когда вы обновляете Nav.

Разработка решения

Воспользовавшись преимуществами встроенных основ разделения кода, я хотел динамически import() фрагмент из другой сборки, так как это наиболее логичный способ, и Webpack уже может обрабатывать импорт фрагментов. Однако в настоящее время вы можете сделать это только из собственной сборки. Webpack на самом деле не знает, как обрабатывать внешний импорт или фрагмент из чужого пакета. Как можно App2 импортировать Nav.chunk.js из App1?

Задача 1: автоматическое разделение кода

Магический экспорт

Мне понадобится способ пометить файл как тот, который я собираюсь использовать в другом приложении. Webpack должен гарантировать, что все, что 'externalized' разделено на код, независимо от того, действительно ли App1 импортируется динамически или нет - чтобы было эффективно чередование, мне нужно импортировать как как можно меньше, не main.bundle.js или vendor.chunk.js. Большинство вещей, которые не являются разделением кода, попадают в массивный фрагмент, поэтому должен быть способ кодового разделения файла без необходимости принудительного динамического импорта файла, возможно, изменения потока разработки и введения очень абстрактного правила. Как мы с этим справимся?

Используя волшебный экспорт, вот как! Я собираюсь поместить /*externalize:Nav*/, который я проанализирую с помощью Webpack во время сборки, а затем обработаю эти файлы другим способом.

Экспорт магии может привести к возможным столкновениям внутри одной и той же сборки. Я рассматриваю альтернативу, которая будет напоминать интерфейс, который вы получаете на других языках, таких как C. Определение будет реализовано в package.json и предотвратит случайный вызов двух частей одного и того же

"interface": {
  './src/components/Menu.js': "Nav"
},

Использование splitChunks API

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

Теперь отмеченные файлы можно распознать и автоматически разбить на отдельные фрагменты. App2 сможет import("someUrl/js/Nav.chunk.js") и будет существовать.

Задача 2: хешированные идентификаторы модулей

Как Webpack назначает модули по умолчанию

По умолчанию Webpack генерирует идентификатор модуля в виде числа. __webpack_modules__ содержит массивный массив, каждый идентификатор модуля - это позиция в массиве.

Это не сработает для чередования, мне нужно знать, что это за модуль.

Вариант оптимизации hashedModuleID?

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

App2 использует рабочие области пряжи и является частью монорепозитория. App1 - нормальный пакет.

App2 импортирует React from ../../node_modules и App1 импортирует реагирует из ../node_modules

Независимо от того, что версии совпадают, при чередовании Nav Webpack не узнает, что у него уже есть React (../node_modules/react), потому что Nav запросит другой модуль хеширования из-за хеширования в зависимости от пути к файлу. В таком случае App2 загрузит блок App1, содержащий React (../../node_modules/react), чтобы предоставить Nav искомый module.id.

[contenthash] для идентификаторов модулей

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

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

При чередовании App2 может __webpack_require__("Nav") и должно быть в состоянии найти его в __webpack_modules__. Важно, чтобы мы знали ключ, под которым он хранится. Если мы не можем найти чередующийся экспорт в манифесте Webpack, мы не сможем его вызвать, даже если он уже загружен в манифест.

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

Сочетанием этих двух частей:

1) Теперь я могу проверять все приложения в браузере и полагаться на все без исключения зависимости, независимо от того, из какой сборки они созданы.

2) Я могу найти внешний фрагмент, который был введен в манифест Webpack, и вызвать его по имени, так же, как работает динамический import()

Связанные вместе, можно было бы построить такие инженерные сети.

const ExternalComponent = (props) => {
  const {
    src, module, export: exportName, cors, ...rest
  } = props;
  let Component = null;
  const [loaded, setLoaded] = useState(false);
  const importPromise = useCallback(
    () => {
      if (!src) return Promise.reject();
      if (cors) {
        return require("./corsImport").default(src);
      }
      return new Promise((resolve) => {
        resolve(new Function(`return import("${src}")`)());
      });
    },
    [src, cors]
  );

  useEffect(() => {
    require("./polyfill");
    if (!src) {
      throw new Error(`interleaving error: ${JSON.stringify(props, null, 2)}`);
    }

    importPromise(src).then(() => {
      const requiredComponent = __webpack_require__(module);
      Component = requiredComponent.default ? requiredComponent.default : requiredComponent[exportName];
      setLoaded(true);
    }).catch((e) => {
      throw new Error(`dynamic-import: ${e.message}`);
    });
  }, []);

  if (!loaded) return null;
  return (
    <Component {...rest} />
  );
};


Вызов 3: очистка кеша

Нам нужен способ загрузки хешированных файлов с закрытым кешем. В большинстве сборок происходит перебор кеша за счет хеширования имен пакетов и блоков, но это затрудняет загрузку Nav.chunk.js, если его имя хешируется, например, Nav.chunk.[contenthash].js

Я решил создать файл Javascript, который может быть заблокирован кешем с помощью строки запроса (каждый раз). С файлами JavaScript нет проблем CORS, поэтому манифесты проще встраивать в приложения. Если вы его используете, вам потребуется настроить CSP.

У каждого есть пространство имен. В противном случае риск столкновения между приложениями слишком высок.

Проблема 4: Отсутствует разрешение зависимостей

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

Улучшение карты манифеста немного улучшило поиск и разрешение обратной зависимости.

Определение того, от чего зависит модуль в подключаемом модуле Webpack.

* Возможно, есть лучший способ сделать это.

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

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

Вызов 5: сотрясение деревьев и устранение мертвого кода

Команда Next.js указала мне на то, что я еще не тестировал: поднятие области видимости, раскачивание дерева и отсутствие экспорта.

К сожалению, Webpack хеширует module.ids до встряхивания дерева, поэтому мои [contenthash] модули не были надежными. Хеш был основан на содержимом того, что было установлено, а не на том, что было в комплекте. Мне нужен был способ справиться с сотрясением деревьев в каждом конкретном случае

Варианты были ограничены, отключение встряхивания дерева взорвало бы размер связки.

Ниже приведен пример сценария, App1 встряхивает дерево экспорта, используемого только App2

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

Возьмите под свой контроль, как Webpack встряхивает дерево

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

Я также узнал, как сделать это более подробно. При циклическом переходе по модулям вы можете установить module.buildMeta.usedExports таким же, как module.providedExports

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

Будущее состояние

Куда это идет?

Самовосстановление

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

ПО промежуточного слоя SSR

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

Конечная согласованность: работает для таких вещей, как верхний или нижний колонтитул. Что-то, вероятно, принадлежащее одному рабочему потоку, но должно быть общим для всего парка MFE. Команда могла опубликовать свой MFE как пакет npm. Потребители SSR это, но клиентские браузеры будут чередовать прямо из источника. Который мог быть развернут позже, чем копия SSR, установленная потребителем.

Это не идеально, но работает.

Распределенный рендеринг: потребитель GET обращается к другому MFE через API рендеринга, который обслуживает JSON, содержащий HTML, CSS, JS, исходное состояние. При этом Node может передать его в качестве свойств <App> и отрендерить остальное. С кешированием фрагментов в react-dom рендеринг становится чрезвычайно быстрым.

Можно также воспользоваться потоковой передачей HTTP2 и фактически передавать SSR из распределенного кластера рендеринга MFE. Или можно просто передавать серверы друг другу в потоковом режиме для уменьшения задержки.

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

Слабые реализации! Когда пришло время

server.get(path, (req, res) =>
  serveFragment(
    req,
    res,
    fragmentID => require(`./components/${fragmentID}`).default) 
);

Новая платформа маршрутизации

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

Объединяемый экспорт

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

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

Улучшенные служебные функции и интернализация манифестов

Взяв все части, такие как манифесты импорта и служебные функции, переместим в webpackJsonP

Ссылка на репо: https://github.com/ScriptedAlchemy/webpack-external-import

Не стесняйтесь открывать PR, или, если вы хотите сотрудничать, напишите мне в Twitter. Как и в любом проекте OSS, вклад сообщества ценится и помогает проекту процветать.

Вопросы? Хватай меня в Твиттере



ИЛИ GitHub



Следующий

  • Создание чередующейся оболочки приложения.
  • Чередование двух приложений - практический пример
  • Простое управление несколькими микро-интерфейсами с помощью синхронизации оболочки приложения и настраиваемых хуков
  • Чередование зон next.js
  • Полный пример управляемого стека с чередованием - архитектура FOSA
  • Серия видео, объясняющая низкий уровень на более удобной для восприятия среде.

Предыдущая статья в серии

Https://levelup.gitconnected.com/micro-frontend-architecture-replacing-a-monolith-from-the-inside-out-61f60d2e14c1