Миграция Франкенштейна: независимый от фреймворка подход (часть 2)

Опубликовано: 2022-03-10
Краткое резюме ↬ Недавно мы обсудили, что такое «миграция Франкенштейна», сравнили ее с обычными типами миграции и упомянули два основных строительных блока: микросервисы и веб-компоненты . Мы также получили теоретическую основу того, как работает этот тип миграции. Если вы не читали или забыли это обсуждение, вы можете сначала вернуться к части 1, потому что она помогает понять все, что мы рассмотрим во второй части статьи.

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

Пришло время проверить теорию
Пришло время проверить теорию. (Большой превью)

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

Представление приложения TodoMVC по умолчанию
Представление приложения TodoMVC по умолчанию (большой предварительный просмотр)

Для этой статьи в качестве отправной точки я выбрал приложение jQuery из проекта TodoMVC — пример, который, возможно, уже знаком многим из вас. jQuery достаточно унаследован, может отражать реальную ситуацию с вашими проектами и, что наиболее важно, требует значительного обслуживания и хаков для обеспечения работы современного динамического приложения. (Этого должно быть достаточно, чтобы рассмотреть вопрос о переходе на что-то более гибкое.)

Что это за «более гибкий», на который мы тогда собираемся мигрировать? Чтобы показать очень практичный кейс, полезный в реальной жизни, мне пришлось выбирать из двух самых популярных фреймворков в наши дни: React и Vue. Однако, что бы я ни выбрал, мы бы упустили некоторые аспекты другого направления.

Еще после прыжка! Продолжить чтение ниже ↓

Итак, в этой части мы рассмотрим оба следующих пункта:

  • Миграция приложения jQuery на React и
  • Миграция приложения jQuery на Vue .
Наши цели: результаты миграции на React и Vue
Наши цели: результаты миграции на React и Vue. (Большой превью)

Репозитории кода

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

  • Франкенштейн ТодоMVC
    Этот репозиторий содержит приложения TodoMVC в разных фреймворках/библиотеках. Например, в этом репозитории вы можете найти такие ветки, как vue , angularjs , react и jquery .
  • Демо Франкенштейна
    Он содержит несколько веток, каждая из которых представляет определенное направление миграции между приложениями, доступными в первом репозитории. Существуют такие ветки, как migration/jquery-to-react и migration/jquery-to-vue , которые мы рассмотрим позже.

Оба репозитория находятся в стадии разработки, и в них следует регулярно добавлять новые ветки с новыми приложениями и направлениями миграции. ( Вы также можете внести свой вклад! ) История коммитов в ветках миграции хорошо структурирована и может служить дополнительной документацией с даже более подробной информацией, чем я мог бы описать в этой статье.

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

  • Клонируйте ветку jquery из репозитория Frankenstein TodoMVC и строго следуйте всем приведенным ниже инструкциям.
  • В качестве альтернативы вы можете открыть ветку, посвященную либо миграции на React, либо миграции на Vue из демо-репозитория Frankenstein и следить за историей коммитов.
  • В качестве альтернативы вы можете расслабиться и продолжить чтение, потому что я собираюсь выделить наиболее важный код прямо здесь, и гораздо важнее понять механику процесса, а не сам код.

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

Давайте погрузимся прямо в!

  1. Определить микросервисы
  2. Разрешить доступ хоста к пришельцу
  3. Напишите чужой микросервис/компонент
  4. Напишите оболочку веб-компонента вокруг чужой службы
  5. Замените службу хоста веб-компонентом
  6. Промойте и повторите для всех ваших компонентов
  7. Переключиться на пришельца

1. Определите микросервисы

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

Итак, чтобы увидеть процесс миграции Франкенштейна более подробно, мы можем пойти дальше и разделить это приложение на два независимых микросервиса:

  1. Поле ввода для добавления нового элемента.
    Эта служба также может содержать заголовок приложения, основанный исключительно на близости расположения этих элементов.
  2. Список уже добавленных элементов.
    Этот сервис более продвинутый, и вместе с самим списком он также содержит такие действия, как фильтрация, действия с элементами списка и так далее.
Приложение TodoMVC разделено на два независимых микросервиса
Приложение TodoMVC разделено на два независимых микросервиса. (Большой превью)

Совет . Чтобы проверить, действительно ли выбранные службы независимы, удалите HTML-разметку, представляющую каждую из этих служб. Убедитесь, что остальные функции все еще работают. В нашем случае должна быть возможность добавлять новые записи в localStorage (которое это приложение использует в качестве хранилища) из поля ввода без списка, в то время как список по-прежнему отображает записи из localStorage , даже если поле ввода отсутствует. Если ваше приложение выдает ошибки при удалении разметки для потенциального микросервиса, посмотрите раздел «Рефакторинг, если необходимо» в части 1, где приведен пример того, как поступать в таких случаях.

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

2. Разрешить доступ хоста к пришельцу

Кратко напомню, что это такое.

  • Хозяин
    Так называется наше текущее приложение. Он написан в рамках, от которых мы собираемся отойти . В данном конкретном случае наше приложение jQuery.
  • Иностранец
    Проще говоря, это постепенная переработка Host на новой платформе, на которую мы собираемся перейти . Опять же, в данном конкретном случае это приложение React или Vue.

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

Сохранение независимости Host и Alien друг от друга имеет решающее значение для миграции Франкенштейна. Однако это делает организацию связи между ними немного сложной. Как разрешить Хосту доступ к Чужому, не столкнув их вместе?

Добавление Alien в качестве подмодуля вашего хоста

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

Общие принципы архитектуры нашего проекта с подмодулями git должны выглядеть так:

  • И Host, и Alien независимы и хранятся в отдельных репозиториях git ;
  • Хост ссылается на Чужого как на подмодуль. На этом этапе Host выбирает конкретное состояние (фиксацию) Alien и добавляет его как подпапку в структуре папок Host.
React TodoMVC добавлен как подмодуль git в приложение jQuery TodoMVC
React TodoMVC добавлен как подмодуль git в приложение jQuery TodoMVC. (Большой превью)

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

В приведенных ниже фрагментах мы используем направление React в качестве примера. Для любого другого направления миграции замените react именем ветки из Frankenstein TodoMVC или настройте пользовательские значения там, где это необходимо.

Если вы продолжите использовать исходное приложение jQuery TodoMVC:

 $ git submodule add -b react [email protected]:mishunov/frankenstein-todomvc.git react $ git submodule update --remote $ cd react $ npm i

Если вы следуете ветке migration/jquery-to-react (или любому другому направлению миграции) из репозитория Frankenstein Demo, приложение Alien уже должно быть там как git submodule , и вы должны увидеть соответствующую папку. Однако по умолчанию папка пуста, и вам необходимо обновить и инициализировать зарегистрированные подмодули.

Из корня вашего проекта (вашего хоста):

 $ git submodule update --init $ cd react $ npm i

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

После добавления приложения Alien в качестве подмодуля вашего Host вы получаете независимые (с точки зрения микросервисов) приложения Alien и Host. Однако в этом случае Host считает Alien вложенной папкой, и, очевидно, это позволяет Host без проблем получить доступ к Alien.

3. Напишите чужой микросервис/компонент

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

Ветки репозитория Frankenstein TodoMVC содержат результирующий компонент, представляющий первый сервис «Поле ввода для добавления нового элемента» в виде компонента Header:

  • Компонент заголовка в React
  • Компонент заголовка в Vue

Написание компонентов в выбранном вами фреймворке выходит за рамки этой статьи и не является частью Frankenstein Migration. Тем не менее, при написании компонента Alien следует помнить о нескольких вещах.

Независимость

Во-первых, компоненты в Чужом должны следовать тому же принципу независимости, ранее установленному на стороне Хоста: компоненты никоим образом не должны зависеть от других компонентов.

Совместимость

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

В качестве примера взгляните на файл js/storage.js , который является основным каналом связи для наших компонентов jQuery:

 ... fetch: function() { return JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]"); }, save: function(todos) { localStorage.setItem(STORAGE_KEY, JSON.stringify(todos)); var event = new CustomEvent("store-update", { detail: { todos } }); document.dispatchEvent(event); }, ...

Здесь мы используем localStorage (поскольку этот пример не является критическим с точки зрения безопасности) для хранения наших элементов списка дел, и как только изменения в хранилище будут записаны, мы отправляем пользовательское событие DOM для элемента document , которое может прослушивать любой компонент.

В то же время, на стороне Чужого (скажем, React) мы можем настроить настолько сложную коммуникацию управления состоянием, насколько захотим. Тем не менее, вероятно, разумно сохранить его на будущее: чтобы успешно интегрировать наш компонент Alien React в Host, нам нужно подключиться к тому же каналу связи, который используется Host. В данном случае это localStorage . Для простоты мы просто скопировали файл хранилища Host в Alien и подключили к нему наши компоненты:

 import todoStorage from "../storage"; class Header extends Component { constructor(props) { this.state = { todos: todoStorage.fetch() }; } componentDidMount() { document.addEventListener("store-update", this.updateTodos); } componentWillUnmount() { document.removeEventListener("store-update", this.updateTodos); } componentDidUpdate(prevProps, prevState) { if (prevState.todos !== this.state.todos) { todoStorage.save(this.state.todos); } } ... }

Теперь наши компоненты Alien могут говорить на одном языке с компонентами Host и наоборот.

4. Напишите оболочку веб-компонента вокруг чужой службы

Несмотря на то, что мы сейчас только на четвертом шаге, мы достигли довольно многого:

  • Мы разделили наше хост-приложение на независимые сервисы, которые можно заменить сервисами Alien;
  • Мы настроили Host и Alien так, чтобы они были полностью независимы друг от друга, но очень хорошо связаны через git submodules ;
  • Мы написали наш первый компонент Alien, используя новый фреймворк.

Теперь пришло время установить мост между Host и Alien, чтобы новый компонент Alien мог функционировать в Host.

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

Соглашение об именовании

Как упоминалось в предыдущей статье, мы собираемся использовать веб-компоненты для интеграции Alien в Host. На стороне хоста мы создаем новый файл: js/frankenstein-wrappers/Header-wrapper.js . (Это будет наша первая оболочка Франкенштейна.) Имейте в виду, что хорошей идеей будет называть ваши оболочки так же, как ваши компоненты в приложении Alien, например, просто добавив суффикс « -wrapper ». Позже вы увидите, почему это хорошая идея, но сейчас давайте согласимся, что это означает, что если компонент Alien называется Header.js (в React) или Header.vue (в Vue), соответствующая оболочка на Сторона хоста должна называться Header-wrapper.js .

В нашей первой оболочке мы начинаем с основного шаблона для регистрации пользовательского элемента:

 class FrankensteinWrapper extends HTMLElement {} customElements.define("frankenstein-header-wrapper", FrankensteinWrapper);

Далее мы должны инициализировать Shadow DOM для этого элемента.

Пожалуйста, обратитесь к части 1, чтобы понять, почему мы используем Shadow DOM.

 class FrankensteinWrapper extends HTMLElement { connectedCallback() { this.attachShadow({ mode: "open" }); } }

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

 import React from "../../react/node_modules/react"; import ReactDOM from "../../react/node_modules/react-dom"; import HeaderApp from "../../react/src/components/Header"; ...

Здесь мы должны сделать паузу на секунду. Обратите внимание, что мы не импортируем зависимости Alien из node_modules . Все исходит от самого Чужого, который находится в подпапке react react/ . Вот почему Шаг 2 так важен, и очень важно убедиться, что Хозяин имеет полный доступ к активам Чужого.

Теперь мы можем визуализировать наш компонент Alien в Shadow DOM веб-компонента:

 ... connectedCallback() { ... ReactDOM.render(<HeaderApp />, this.shadowRoot); } ...

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

 ... connectedCallback() { const mountPoint = document.createElement("div"); this.attachShadow({ mode: "open" }).appendChild(mountPoint); new Vue({ render: h => h(VueHeader) }).$mount(mountPoint); } ...

Причиной этого является разница в том, как React и Vue отображают компоненты: React добавляет компонент к указанному узлу DOM, в то время как Vue заменяет указанный узел DOM компонентом. Следовательно, если мы выполним .$mount(this.shadowRoot) для Vue, он по существу заменит Shadow DOM.

Это все, что нам нужно сделать с нашей оберткой на данный момент. Текущий результат для оболочки Frankenstein в направлениях миграции jQuery-to-React и jQuery-to-Vue можно найти здесь:

  • Frankenstein Wrapper для компонента React
  • Frankenstein Wrapper для компонента Vue

Подводя итог механике обертки Франкенштейна:

  1. Создайте пользовательский элемент,
  2. Инициировать теневой DOM,
  3. Импортируйте все необходимое для рендеринга компонента Alien,
  4. Визуализируйте компонент Alien в Shadow DOM пользовательского элемента.

Однако это не делает нашего Alien in Host автоматически. Мы должны заменить существующую разметку Host нашей новой оболочкой Frankenstein.

Пристегните ремни безопасности, это может быть не так просто, как можно было бы ожидать!

5. Замените службу хоста веб-компонентом

Давайте продолжим и добавим наш новый файл Header-wrapper.js в index.html и заменим существующую разметку заголовка недавно созданным пользовательским элементом <frankenstein-header-wrapper> .

 ... <!-- <header class="header">--> <!-- <h1>todos</h1>--> <!-- <input class="new-todo" placeholder="What needs to be done?" autofocus>--> <!-- </header>--> <frankenstein-header-wrapper></frankenstein-header-wrapper> ... <script type="module" src="js/frankenstein-wrappers/Header-wrapper.js"></script>

К сожалению, это не будет работать так просто. Если вы откроете браузер и проверите консоль, вас ждет Uncaught SyntaxError . В зависимости от браузера и его поддержки модулей ES6 это будет связано либо с импортом ES6, либо со способом рендеринга компонента Alien. В любом случае, с этим нужно что-то делать, но проблема и решение должны быть знакомы и понятны большинству читателей.

5.1. Обновите Webpack и Babel, где это необходимо

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

  • Конфигурация для миграции на React
  • Конфигурация для перехода на Vue

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

Как только Webpack в Host узнает, как обрабатывать компонент Alien и веб-компоненты, мы готовы заменить разметку Host новой оболочкой Frankenstein.

5.2. Замена фактического компонента

Теперь замена компонента должна быть простой. В index.html вашего хоста сделайте следующее:

  1. Замените элемент DOM <header class="header"> на <frankenstein-header-wrapper> ;
  2. Добавьте новый скрипт frankenstein.js . Это новая точка входа в Webpack, которая содержит все, что связано с обертками Франкенштейна.
 ... <!-- We replace <header class="header"> --> <frankenstein-header-wrapper></frankenstein-header-wrapper> ... <script src="./frankenstein.js"></script>

Вот и все! Перезагрузите сервер, если необходимо, и станьте свидетелем волшебства компонента Alien, интегрированного в Host.

Однако чего-то все же, казалось, не хватает. Компонент Alien в контексте хоста выглядит не так, как в контексте автономного приложения Alien. Это просто нестильно.

Компонент Alien React без стиля после интеграции в Host
Компонент Alien React без стиля после интеграции в Host (большой предварительный просмотр)

Почему это так? Разве стили компонента не должны автоматически интегрироваться с компонентом Alien в Host? Я бы хотел, чтобы они были, но, как и во многих других ситуациях, это зависит от обстоятельств. Мы подходим к сложной части миграции Франкенштейна.

5.3. Общая информация о стиле инопланетного компонента

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

Глобальные стили

Мы все знакомы с этим: глобальные стили могут распространяться (и обычно так и происходит) без какого-либо конкретного компонента и применяться ко всей странице. Глобальные стили влияют на все узлы DOM с соответствующими селекторами.

Несколько примеров глобальных стилей — это теги <style> и <link rel="stylesheet"> , найденные в вашем index.html . Как вариант, глобальную таблицу стилей можно импортировать в какой-нибудь корневой JS-модуль, чтобы все компоненты тоже могли получить к ней доступ.

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

Связанные стили

Эти стили обычно тесно связаны с самим компонентом и редко распространяются без него. Стили обычно находятся в одном файле с компонентом. Хорошими примерами этого типа стилей являются стилизованные компоненты в React или модулях CSS и Scoped CSS в отдельных файловых компонентах в Vue. Однако, независимо от разнообразия инструментов для написания связанных стилей, основной принцип большинства из них один и тот же: инструменты предоставляют механизм области действия для блокировки стилей, определенных в компоненте, чтобы стили не нарушали другие компоненты или глобальные компоненты. стили.

Почему стили с ограниченной областью действия могут быть хрупкими?

В части 1, при обосновании использования Shadow DOM в Frankenstein Migration, мы кратко рассмотрели тему области видимости и инкапсуляции, а также то, чем инкапсуляция Shadow DOM отличается от инструментов определения области видимости. Однако мы не объяснили, почему инструменты скоупинга обеспечивают такую ​​хрупкую стилизацию для наших компонентов, а теперь, когда мы столкнулись с нестилизованным компонентом Alien, это становится важным для понимания.

Все инструменты обзора для современных фреймворков работают одинаково:

  • Вы каким-то образом пишете стили для своего компонента, не задумываясь об области действия или инкапсуляции;
  • Вы запускаете свои компоненты с импортированными/встроенными таблицами стилей через какую-либо систему связывания, такую ​​как Webpack или Rollup;
  • Сборщик генерирует уникальные классы CSS или другие атрибуты, создавая и внедряя индивидуальные селекторы как для вашего HTML, так и для соответствующих таблиц стилей;
  • Сборщик делает запись <style> в <head> вашего документа и помещает туда стили ваших компонентов с уникальными смешанными селекторами.

Вот и все. Он работает и во многих случаях работает нормально. За исключением случаев, когда это не так: когда стили для всех компонентов находятся в глобальной области стилей, их становится легко сломать, например, используя более высокую специфичность. Это объясняет потенциальную хрупкость инструментов определения области видимости, но почему наш компонент Alien совершенно не оформлен?

Давайте посмотрим на текущий хост с помощью DevTools. Например, при проверке недавно добавленной оболочки Франкенштейна с компонентом Alien React мы можем увидеть что-то вроде этого:

Обертка Франкенштейна с компонентом Alien внутри. Обратите внимание на уникальные классы CSS на узлах Чужого.
Обертка Франкенштейна с компонентом Alien внутри. Обратите внимание на уникальные классы CSS на узлах Чужого. (Большой превью)

Итак, Webpack генерирует уникальные классы CSS для нашего компонента. Здорово! Где тогда стили? Что ж, стили находятся именно там, где они должны быть — в <head> документа.

В то время как компонент Alien находится внутри оболочки Frankenstein, его стили находятся в заголовке документа.
Хотя компонент Alien находится внутри оболочки Frankenstein, его стили находятся в <head> документа. (Большой превью)

Так что все работает как надо, и это основная проблема. Поскольку наш компонент Alien находится в Shadow DOM и, как объяснялось в части 1, Shadow DOM обеспечивает полную инкапсуляцию компонентов из остальной части страницы и глобальных стилей, включая недавно сгенерированные таблицы стилей для компонента, которые не могут пересекать границу тени и добраться до компонента Alien. Следовательно, компонент Alien остается без стиля. Однако теперь тактика решения проблемы должна быть ясна: мы должны как-то поместить стили компонента в тот же Shadow DOM, где находится наш компонент (вместо <head> документа).

5.4. Исправление стилей для инопланетного компонента

До сих пор процесс перехода на любой фреймворк был одинаковым. Однако тут начинаются разногласия: у каждого фреймворка есть свои рекомендации по стилю компонентов, а значит, и пути решения проблемы разные. Здесь мы обсудим наиболее распространенные случаи, но если фреймворк, с которым вы работаете, использует какой-то уникальный способ стилизации компонентов, вам нужно помнить об основных тактиках, таких как помещение стилей компонента в Shadow DOM вместо <head> .

В этой главе мы рассмотрим исправления для:

  • Связанные стили с модулями CSS в Vue (тактика для Scoped CSS такая же);
  • Связанные стили со стилизованными компонентами в React;
  • Общие модули CSS и глобальные стили. Я комбинирую их, потому что модули CSS в целом очень похожи на глобальные таблицы стилей и могут быть импортированы любым компонентом, что делает стили не связанными с каким-либо конкретным компонентом.

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

Связанные стили в Vue и Shadow DOM

Если вы пишете приложение Vue, то, скорее всего, вы используете однофайловые компоненты. Если вы также используете Webpack, вы должны быть знакомы с двумя загрузчиками vue-loader и vue-style-loader . Первый позволяет вам писать эти отдельные файловые компоненты, а второй динамически внедряет CSS компонента в документ в виде <style> . По умолчанию vue-style-loader внедряет стили компонента в <head> документа. Однако оба пакета принимают параметр shadowMode в конфигурации, что позволяет нам легко изменить поведение по умолчанию и внедрить стили (как следует из названия параметра) в Shadow DOM. Давайте посмотрим на это в действии.

Конфигурация веб-пакета

Как минимум, файл конфигурации Webpack должен содержать следующее:

 const VueLoaderPlugin = require('vue-loader/lib/plugin'); ... module: { rules: [ { test: /\.vue$/, loader: 'vue-loader', options: { shadowMode: true } }, { test: /\.css$/, include: path.resolve(__dirname, '../vue'), use: [ { loader:'vue-style-loader', options: { shadowMode: true } }, 'css-loader' ] } ], plugins: [ new VueLoaderPlugin() ] }

В реальном приложении ваш блок test: /\.css$/ будет более сложным (вероятно, с использованием правила oneOf ) для учета конфигураций как Host, так и Alien. Однако в этом случае наш jQuery оформлен с помощью простого <link rel="stylesheet"> в index.html , поэтому мы не создаем стили для Host через Webpack, и безопасно обслуживать только Alien.

Конфигурация оболочки

В дополнение к конфигурации Webpack нам также необходимо обновить нашу оболочку Frankenstein, указав Vue на правильный Shadow DOM. В нашем Header-wrapper.js рендеринг компонента Vue должен включать свойство shadowRoot , ведущее к shadowRoot нашей оболочки Франкенштейна:

 ... new Vue({ shadowRoot: this.shadowRoot, render: h => h(VueHeader) }).$mount(mountPoint); ...

После того, как вы обновите файлы и перезапустите сервер, вы должны получить что-то вроде этого в DevTools:

Стили, связанные с компонентом Alien Vue, помещены в оболочку Frankenstein с сохранением всех уникальных классов CSS.
Стили, связанные с компонентом Alien Vue, помещены в оболочку Frankenstein с сохранением всех уникальных классов CSS. (Большой превью)

Наконец, стили для компонента Vue находятся в нашем Shadow DOM. При этом ваше приложение должно выглядеть так:

Компонент заголовка начинает выглядеть более правильно. Однако чего-то все же не хватает.
Компонент заголовка начинает выглядеть более правильно. Однако чего-то все же не хватает. (Большой превью)

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

Связанные стили в React и Shadow DOM

Поскольку существует множество способов стилизации компонента React, конкретное решение по исправлению компонента Alien в Frankenstein Migration зависит в первую очередь от того, как мы стилизуем компонент. Кратко рассмотрим наиболее часто используемые альтернативы.

стилизованные компоненты

styled-components — один из самых популярных способов стилизации компонентов React. Для компонента Header React styled-components — это именно то, как мы его стилизуем. Поскольку это классический подход CSS-in-JS, нет файла с выделенным расширением, к которому мы могли бы подключить наш упаковщик, как, например, для файлов .css или .js . К счастью, styled-components позволяют внедрять стили компонента в пользовательский узел (в нашем случае Shadow DOM) вместо head документа с помощью вспомогательного компонента StyleSheetManager . Это предопределенный компонент, устанавливаемый вместе с пакетом styled-components , который принимает target свойство, определяющее «альтернативный DOM-узел для ввода информации о стилях». Именно то, что нам нужно! Более того, нам даже не нужно менять нашу конфигурацию Webpack: все зависит от нашей обертки Франкенштейна.

Мы должны обновить наш Header-wrapper.js , содержащий компонент React Alien, следующими строками:

 ... import { StyleSheetManager } from "../../react/node_modules/styled-components"; ... const target = this.shadowRoot; ReactDOM.render( <StyleSheetManager target={target}> <HeaderApp /> </StyleSheetManager>, appWrapper ); ...

Здесь мы импортируем компонент StyleSheetManager (из Alien, а не из Host) и оборачиваем им наш компонент React. В то же время мы отправляем target свойство, указывающее на наш shadowRoot . Вот и все. Если вы перезапустите сервер, вы должны увидеть что-то вроде этого в DevTools:

Стили, связанные с компонентом React Alien, помещены в оболочку Frankenstein с сохранением всех уникальных классов CSS.
Стили, связанные с компонентом React Alien, помещены в оболочку Frankenstein с сохранением всех уникальных классов CSS. (Большой превью)

Теперь стили нашего компонента находятся в Shadow DOM вместо <head> . Таким образом, рендеринг нашего приложения теперь напоминает то, что мы видели ранее в приложении Vue.

После переноса связанных стилей в оболочку Frankenstein компонент Alien React стал выглядеть лучше. Однако мы еще не там.
После переноса связанных стилей в оболочку Frankenstein компонент Alien React стал выглядеть лучше. Тем не менее, мы еще не там. (Большой превью)

Та же история: styled-components отвечают только за связанную часть стилей компонента React , а глобальные стили управляют остальными битами. Мы вернемся к глобальным стилям чуть позже, после того как рассмотрим еще один тип компонентов стиля.

CSS-модули

Если вы внимательно посмотрите на компонент Vue, который мы исправили ранее, вы можете заметить, что CSS-модули — это именно то, как мы стилизуем этот компонент. However, even if we style it with Scoped CSS (another recommended way of styling Vue components) the way we fix our unstyled component doesn't change: it is still up to vue-loader and vue-style-loader to handle it through shadowMode: true option.

When it comes to CSS Modules in React (or any other system using CSS Modules without any dedicated tools), things get a bit more complicated and less flexible, unfortunately.

Let's take a look at the same React component which we've just integrated, but this time styled with CSS Modules instead of styled-components. The main thing to note in this component is a separate import for stylesheet:

 import styles from './Header.module.css'

The .module.css extension is a standard way to tell React applications built with the create-react-app utility that the imported stylesheet is a CSS Module. The stylesheet itself is very basic and does precisely the same our styled-components do.

Integrating CSS modules into a Frankenstein wrapper consists of two parts:

  • Enabling CSS Modules in bundler,
  • Pushing resulting stylesheet into Shadow DOM.

I believe the first point is trivial: all you need to do is set { modules: true } for css-loader in your Webpack configuration. Since, in this particular case, we have a dedicated extension for our CSS Modules ( .module.css ), we can have a dedicated configuration block for it under the general .css configuration:

 { test: /\.css$/, oneOf: [ { test: /\.module\.css$/, use: [ ... { loader: 'css-loader', options: { modules: true, } } ] } ] }

Note : A modules option for css-loader is all we have to know about CSS Modules no matter whether it's React or any other system. When it comes to pushing resulting stylesheet into Shadow DOM, however, CSS Modules are no different from any other global stylesheet.

By now, we went through the ways of integrating bundled styles into Shadow DOM for the following conventional scenarios:

  • Vue components, styled with CSS Modules. Dealing with Scoped CSS in Vue components won't be any different;
  • React components, styled with styled-components;
  • Components styled with raw CSS Modules (without dedicated tools like those in Vue). For these, we have enabled support for CSS modules in Webpack configuration.

However, our components still don't look as they are supposed to because their styles partially come from global styles . Those global styles do not come to our Frankenstein wrappers automatically. Moreover, you might get into a situation in which your Alien components are styled exclusively with global styles without any bundled styles whatsoever. So let's finally fix this side of the story.

Global Styles And Shadow DOM

Having your components styled with global styles is neither wrong nor bad per se: every project has its requirements and limitations. However, the best you can do for your components if they rely on some global styles is to pull those styles into the component itself. This way, you have proper easy-to-maintain self-contained components with bundled styles.

Nevertheless, it's not always possible or reasonable to do so: several components might share some styling, or your whole styling architecture could be built using global stylesheets that are split into the modular structure, and so on.

So having an opportunity to pull in global styles into our Frankenstein wrappers wherever it's required is essential for the success of this type of migration. Before we get to an example, keep in mind that this part is the same for pretty much any framework of your choice — be it React, Vue or anything else using global stylesheets!

Let's get back to our Header component from the Vue application. Take a look at this import:

 import "todomvc-app-css/index.css";

This import is where we pull in the global stylesheet. In this case, we do it from the component itself. It's only one way of using global stylesheet to style your component, but it's not necessarily like this in your application.

Some parent module might add a global stylesheet like in our React application where we import index.css only in index.js , and then our components expect it to be available in the global scope. Your component's styling might even rely on a stylesheet, added with <style> or <link> to your index.html . Это не имеет значения. What matters, however, is that you should expect to either import global stylesheets in your Alien component (if it doesn't harm the Alien application) or explicitly in the Frankenstein wrapper. Otherwise, the wrapper would not know that the Alien component needs any stylesheet other than the ones already bundled with it.

Caution . If there are many global stylesheets to be shared between Alien components and you have a lot of such components, this might harm the performance of your Host application under the migration period.

Here is how import of a global stylesheet, required for the Header component, is done in Frankenstein wrapper for React component:

 // we import directly from react/, not from Host import '../../react/node_modules/todomvc-app-css/index.css'

Nevertheless, by importing a stylesheet this way, we still bring the styles to the global scope of our Host, while what we need is to pull in the styles into our Shadow DOM. как нам это сделать?

Webpack configuration for global stylesheets & Shadow DOM

First of all, you might want to add an explicit test to make sure that we process only the stylesheets coming from our Alien. In case of our React migration, it will look similar to this:

 test: /\.css$/, oneOf: [ // this matches stylesheets coming from /react/ subfolder { test: /\/react\//, use: [] }, ... ]

In case of Vue application, obviously, you change test: /\/react\// with something like test: /\/vue\// . Apart from that, the configuration will be the same for any framework. Next, let's specify the required loaders for this block.

 ... use: [ { loader: 'style-loader', options: { ... } }, 'css-loader' ]

Two things to note. First, you have to specify modules: true in css-loader 's configuration if you're processing CSS Modules of your Alien application.

Second, we should convert styles into <style> tag before injecting those into Shadow DOM. In the case of Webpack, for that, we use style-loader . The default behavior for this loader is to insert styles into the document's head. Typically. And this is precisely what we don't want: our goal is to get stylesheets into Shadow DOM. However, in the same way we used target property for styled-components in React or shadowMode option for Vue components that allowed us to specify custom insertion point for our <style> tags, regular style-loader provides us with nearly same functionality for any stylesheet: the insert configuration option is exactly what helps us achieve our primary goal. Отличные новости! Let's add it to our configuration.

 ... { loader: 'style-loader', options: { insert: 'frankenstein-header-wrapper' } }

However, not everything is so smooth here with a couple of things to keep in mind.

Глобальные таблицы стилей и возможность insert style-loader

Если вы проверите документацию для этой опции, вы заметите, что эта опция использует один селектор для каждой конфигурации. Это означает, что если у вас есть несколько компонентов Alien, требующих глобальных стилей, загруженных в оболочку Frankenstein, вы должны указать style-loader для каждой из оболочек Frankenstein. На практике это означает, что вам, вероятно, придется полагаться на правило oneOf в вашем конфигурационном блоке для обслуживания всех обёрток.

 { test: /\/react\//, oneOf: [ { test: /1-TEST-FOR-ALIEN-FILE-PATH$/, use: [ { loader: 'style-loader', options: { insert: '1-frankenstein-wrapper' } }, `css-loader` ] }, { test: /2-TEST-FOR-ALIEN-FILE-PATH$/, use: [ { loader: 'style-loader', options: { insert: '2-frankenstein-wrapper' } }, `css-loader` ] }, // etc. ], }

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

Пытаясь решить эту проблему, мы могли бы заметить, что опция insert также может принимать функцию вместо простого селектора, чтобы указать более сложную логику для вставки. При этом мы можем использовать эту опцию для вставки таблиц стилей прямо в Shadow DOM! В упрощенном виде это может выглядеть примерно так:

 insert: function(element) { var parent = document.querySelector('frankenstein-header-wrapper').shadowRoot; parent.insertBefore(element, parent.firstChild); }

Заманчиво, не правда ли? Однако это не сработает для нашего сценария или будет работать далеко от оптимального. Наш <frankenstein-header-wrapper> действительно доступен из index.html (потому что мы добавили его на шаге 5.2). Но когда Webpack обрабатывает все зависимости (включая таблицы стилей) либо для компонента Alien, либо для обертки Frankenstein, Shadow DOM еще не инициализируется в оболочке Frankenstein: импорты обрабатываются до этого. Следовательно, указание insert прямо на shadowRoot приведет к ошибке.

Есть только один случай, когда мы можем гарантировать, что Shadow DOM будет инициализирован до того, как Webpack обработает нашу зависимость таблицы стилей. Если компонент Alien не импортирует саму таблицу стилей, и ее импортирует оболочка Frankenstein, мы можем использовать динамический импорт и импортировать требуемую таблицу стилей после того, как настроим Shadow DOM:

 this.attachShadow({ mode: "open" }); import('../vue/node_modules/todomvc-app-css/index.css');

Это будет работать: такой импорт в сочетании с приведенной выше конфигурацией insert действительно найдет правильный Shadow DOM и вставит в него тег <style> . Тем не менее, получение и обработка таблицы стилей потребует времени, а это означает, что ваши пользователи с медленным соединением или медленными устройствами могут столкнуться с моментом отсутствия стиля компонента до того, как ваша таблица стилей займет свое место в Shadow DOM оболочки.

Нестилизованный компонент Alien визуализируется до того, как глобальная таблица стилей будет импортирована и добавлена ​​в Shadow DOM.
Нестилизованный компонент Alien визуализируется до того, как глобальная таблица стилей будет импортирована и добавлена ​​в Shadow DOM. (Большой превью)

В общем, несмотря на то, что insert принимает функцию, к сожалению, нам этого недостаточно, и мы вынуждены вернуться к простым селекторам CSS, таким как frankenstein-header-wrapper . Однако это не помещает таблицы стилей в Shadow DOM автоматически, и таблицы стилей находятся в <frankenstein-header-wrapper> вне Shadow DOM.

style-loader помещает импортированную таблицу стилей в оболочку Frankenstein, но вне Shadow DOM.
style-loader помещает импортированную таблицу стилей в оболочку Frankenstein, но вне Shadow DOM. (Большой превью)

Нам нужен еще один кусочек головоломки.

Конфигурация оболочки для глобальных таблиц стилей и Shadow DOM

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

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

  • Мы импортируем таблицу стилей, которую необходимо добавить в Shadow DOM. Таблицу стилей можно импортировать либо в сам компонент Alien, либо явно в оболочку Frankenstein. Например, в случае миграции на React импорт инициализируется из обертки. Однако при миграции на Vue аналогичный компонент сам импортирует нужную таблицу стилей, и нам не нужно ничего импортировать в обертке.
  • Как указывалось выше, когда Webpack обрабатывает импорт .css для компонента Alien, благодаря опции insert style-loader таблицы стилей внедряются в оболочку Frankenstein, но вне Shadow DOM.

Упрощенная инициализация Shadow DOM в обертке Frankenstein должна в настоящее время (до того, как мы подтянем какие-либо таблицы стилей) выглядеть примерно так:

 this.attachShadow({ mode: "open" }); ReactDOM.render(); // or `new Vue()`

Теперь, чтобы избежать мерцания нестилизованного компонента, нам нужно загрузить все необходимые таблицы стилей после инициализации Shadow DOM, но до рендеринга компонента Alien.

 this.attachShadow({ mode: "open" }); Array.prototype.slice .call(this.querySelectorAll("style")) .forEach(style => { this.shadowRoot.prepend(style); }); ReactDOM.render(); // or new Vue({})

Это было длинное объяснение с большим количеством деталей, но, в основном, все, что нужно, чтобы добавить глобальные таблицы стилей в Shadow DOM:

  • В конфигурации Webpack добавьте style-loader с параметром insert , указывающим на требуемую оболочку Франкенштейна.
  • В самой оболочке извлеките «ожидающие» таблицы стилей после инициализации Shadow DOM, но до рендеринга компонента Alien.

После внесения этих изменений ваш компонент должен иметь все необходимое. Единственное, что вы можете захотеть добавить (это не обязательно), — это какой-нибудь пользовательский CSS для точной настройки компонента Alien в среде Host. Вы даже можете стилизовать свой компонент Alien совершенно по-другому, если используете его в Host. Это выходит за рамки основного смысла статьи, но вы посмотрите на окончательный код оболочки, где вы можете найти примеры того, как переопределять простые стили на уровне оболочки.

  • Обертка Франкенштейна для компонента React
  • Оболочка Франкенштейна для компонента Vue

Вы также можете взглянуть на конфигурацию Webpack на этом этапе миграции:

  • Миграция на React со стилизованными компонентами
  • Переход на React с модулями CSS
  • Миграция на Vue

И, наконец, наши компоненты выглядят именно так, как мы задумали.

Результат переноса компонента Header, написанного с помощью Vue и React. Список дел по-прежнему является приложением jQuery.
Результат переноса компонента Header, написанного с помощью Vue и React. Список дел по-прежнему является приложением jQuery. (Большой превью)

5.5. Сводка стилей исправления для компонента Alien

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

  • Исправить связанные стили, реализованные с помощью styled-components в модулях React или CSS и Scoped CSS в Vue, можно так же просто, как пару строк в оболочке Frankenstein или конфигурации Webpack.
  • Исправление стилей, реализованное с помощью модулей CSS, начинается всего с одной строки в конфигурации css-loader . После этого модули CSS рассматриваются как глобальная таблица стилей.
  • Исправление глобальных таблиц стилей требует настройки пакета style-loader стилей с опцией insert в Webpack и обновления оболочки Frankenstein для загрузки таблиц стилей в Shadow DOM в нужный момент жизненного цикла оболочки.

В конце концов, у нас есть правильно стилизованный компонент Alien, перенесенный в Host. Однако есть только одна вещь, которая может вас беспокоить или не беспокоить в зависимости от того, на какой фреймворк вы переходите.

Сначала хорошие новости: если вы переходите на Vue , демо должно работать нормально, и вы должны иметь возможность добавлять новые задачи из перенесенного компонента Vue. Однако, если вы переходите на React и пытаетесь добавить новый элемент списка дел, у вас ничего не получится. Добавление новых элементов просто не работает, и в список не добавляются никакие записи. Но почему? В чем проблема? Никаких предубеждений, но у React есть свое мнение на некоторые вещи.

5.6. События React и JS в Shadow DOM

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

Как вы видели, исправляя стиль для нашего компонента Alien, в отличие от Vue, где все подходит для веб-компонентов почти из коробки, React не готов к веб-компонентам. На данный момент у нас есть понимание того, как заставить компоненты React хотя бы хорошо выглядеть в веб-компонентах, но есть также функциональность и события JavaScript, которые нужно исправить.

Короче говоря: Shadow DOM инкапсулирует события и перенаправляет их, в то время как React изначально не поддерживает такое поведение Shadow DOM и, следовательно, не перехватывает события, поступающие из Shadow DOM. Есть более глубокие причины для такого поведения, и даже есть открытая проблема в трекере ошибок React, если вы хотите углубиться в подробности и обсуждения.

К счастью, умные люди подготовили для нас решение. @josephnvu послужил основой для решения, и Лукас Бомбах преобразовал его в модуль npm react react-shadow-dom-retarget-events . Таким образом, вы можете установить пакет, следовать инструкциям на странице пакетов, обновить код вашей оболочки, и ваш компонент Alien волшебным образом начнет работать:

 import retargetEvents from 'react-shadow-dom-retarget-events'; ... ReactDOM.render( ... ); retargetEvents(this.shadowRoot);

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

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

6. Промойте и повторите для всех ваших компонентов

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

Новые оболочки для новых компонентов

Начнем с добавления новой оболочки. Следуя соглашению об именовании, рассмотренному выше (поскольку наш компонент React называется MainSection.js ), соответствующая оболочка при миграции на React должна называться MainSection-wrapper.js . При этом аналогичный компонент во Vue называется Listing.vue , следовательно, соответствующая обертка при миграции на Vue должна называться Listing-wrapper.js . Однако, независимо от соглашения об именах, сама оболочка будет почти идентична той, что у нас уже есть:

  • Обертка для листинга React
  • Обертка для листинга Vue

Есть только одна интересная вещь, которую мы представляем во втором компоненте приложения React. Иногда по той или иной причине вы можете захотеть использовать какой-нибудь плагин jQuery в своих компонентах. В случае с нашим компонентом React мы ввели две вещи:

  • Плагин всплывающей подсказки от Bootstrap, использующий jQuery,
  • Переключатель для классов CSS, таких как .addClass() и .removeClass() .

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

Конечно, может показаться странным вводить jQuery в компонент Alien при переходе от jQuery, но ваш хост может отличаться от хоста в этом примере — вы можете мигрировать с AngularJS или чего-то еще. Кроме того, функциональность jQuery в компоненте и глобальная jQuery не обязательно совпадают.

Однако проблема в том, что даже если вы подтвердите, что компонент отлично работает в контексте вашего приложения Alien, когда вы поместите его в Shadow DOM, ваши подключаемые модули jQuery и другой код, основанный на jQuery, просто не будут работать.

jQuery в теневой модели DOM

Давайте посмотрим на общую инициализацию случайного плагина jQuery:

 $('.my-selector').fancyPlugin();

Таким образом, все элементы с .my-selector будут обработаны fancyPlugin . Эта форма инициализации предполагает, что .my-selector присутствует в глобальной модели DOM. Однако, как только такой элемент помещается в Shadow DOM, как и в случае со стилями, теневые границы не позволяют jQuery проникнуть в него. В результате jQuery не может найти элементы внутри Shadow DOM.

Решение состоит в том, чтобы предоставить необязательный второй параметр селектору, который определяет корневой элемент для поиска jQuery. И здесь мы можем предоставить наш shadowRoot .

 $('.my-selector', this.shadowRoot).fancyPlugin();

Таким образом, селекторы jQuery и, как следствие, плагины будут работать нормально.

Имейте в виду, однако, что компоненты Alien предназначены для использования как в Alien без теневого DOM, так и в Host внутри Shadow DOM. Следовательно, нам нужно более унифицированное решение, которое не предполагало бы наличие Shadow DOM по умолчанию.

Анализируя компонент MainSection в нашем приложении React, мы обнаруживаем, что он устанавливает свойство documentRoot .

 ... this.documentRoot = this.props.root? this.props.root: document; ...

Итак, мы проверяем переданное root свойство, и если оно существует, то используем его как documentRoot . В противном случае мы возвращаемся к document .

Вот инициализация плагина всплывающей подсказки, который использует это свойство:

 $('[data-toggle="tooltip"]', this.documentRoot).tooltip({ container: this.props.root || 'body' });

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

Теперь, когда компонент Alien готов принять root свойство, мы обновляем рендеринг компонента в соответствующей обертке Frankenstein:

 // `appWrapper` is the root element within wrapper's Shadow DOM. ReactDOM.render(<MainApp root={ appWrapper } />, appWrapper);

Вот и все! Компонент работает в Shadow DOM так же хорошо, как и в глобальной DOM.

Конфигурация Webpack для сценария с несколькими оболочками

Самое интересное происходит в конфигурации Webpack при использовании нескольких оболочек. Ничего не меняется для связанных стилей, таких как модули CSS в компонентах Vue или стилизованные компоненты в React. Однако теперь глобальные стили должны немного измениться.

Помните, мы говорили, что style-loader стилей (отвечающий за вставку глобальных таблиц стилей в правильную Shadow DOM) негибок, поскольку он принимает только один селектор за раз для своей опции insert . Это означает, что мы должны разделить правило .css в Webpack, чтобы иметь одно подправило для каждой оболочки с использованием правила oneOf или аналогичного, если вы используете сборщик, отличный от Webpack.

Это всегда проще объяснить на примере, поэтому давайте на этот раз поговорим о переходе от миграции к Vue (однако пример миграции к React почти идентичен):

 ... oneOf: [ { issuer: /Header/, use: [ { loader: 'style-loader', options: { insert: 'frankenstein-header-wrapper' } }, ... ] }, { issuer: /Listing/, use: [ { loader: 'style-loader', options: { insert: 'frankenstein-listing-wrapper' } }, ... ] }, ] ...

Я исключил css-loader , так как его конфигурация одинакова во всех случаях. Вместо этого поговорим о style-loader . В этой конфигурации мы вставляем <style> либо в *-header-* , либо в *-listing- *-listing-* , в зависимости от имени файла, запрашивающего эту таблицу стилей (правило issuer в Webpack). Но мы должны помнить, что глобальная таблица стилей, необходимая для рендеринга компонента Alien, может быть импортирована в двух местах:

  • Сам компонент Alien,
  • Обертка Франкенштейна.

И здесь мы должны оценить соглашение об именах для оберток, описанное выше, когда имя компонента Alien и соответствующей обертки совпадают. Если, например, у нас есть таблица стилей, импортированная в компонент Vue с именем Header.vue , она получает правильную *-header-* . В то же время, если мы вместо этого импортируем таблицу стилей в обертку, такая таблица стилей подчиняется точно такому же правилу, если обертка называется Header-wrapper.js без каких-либо изменений в конфигурации. То же самое для компонента Listing.vue и его соответствующей оболочки Listing-wrapper.js . Используя это соглашение об именах, мы уменьшаем конфигурацию в нашем сборщике.

После того, как все ваши компоненты перенесены, пришло время для последнего шага миграции.

7. Переключиться на пришельца

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

Например, контентная часть index.html в приложении jQuery — после миграции обоих микросервисов — теперь выглядит примерно так:

 <section class="todoapp"> <frankenstein-header-wrapper></frankenstein-header-wrapper> <frankenstein-listing-wrapper></frankenstein-listing-wrapper> </section>

На данный момент нет смысла держать наше приложение jQuery рядом: вместо этого мы должны переключиться на приложение Vue и забыть обо всех наших обертках, Shadow DOM и причудливых конфигурациях Webpack. Для этого у нас есть элегантное решение.

Давайте поговорим о HTTP-запросах. Я упомяну здесь конфигурацию Apache, но это всего лишь деталь реализации: переключение в Nginx или что-то еще должно быть таким же тривиальным, как и в Apache.

Представьте, что ваш сайт обслуживается из папки /var/www/html на вашем сервере. В этом случае ваш httpd.conf или httpd-vhost.conf должен иметь запись, указывающую на эту папку, например:

 DocumentRoot "/var/www/html"

Чтобы переключить ваше приложение после миграции Франкенштейна с jQuery на React, все, что вам нужно сделать, это обновить запись DocumentRoot на что-то вроде:

 DocumentRoot "/var/www/html/react/build"

Создайте свое приложение Alien, перезапустите сервер, и ваше приложение будет обслуживаться непосредственно из папки Alien: приложение React обслуживается из папки react/ . Однако то же самое верно и для Vue, конечно, или для любого другого фреймворка, который вы мигрировали. Вот почему так важно, чтобы Носитель и Чужой были полностью независимыми и функциональными в любой момент времени, потому что на этом этапе ваш Инопланетянин становится вашим Хостом.

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

Заключение

В этой статье мы определенно прошли несколько пересеченную местность. Однако после того, как мы начали с приложения jQuery, нам удалось перенести его как на Vue, так и на React. По пути мы обнаружили некоторые неожиданные и не очень тривиальные проблемы: нам пришлось исправить стили, нам пришлось исправить функциональность JavaScript, ввести некоторые конфигурации сборщиков и многое другое. Тем не менее, это дало нам лучшее представление о том, чего ожидать в реальных проектах. В итоге мы получили современное приложение без каких-либо оставшихся битов от приложения jQuery, хотя у нас были все права скептически относиться к конечному результату, пока шла миграция.

После перехода на Чужого Франкенштейна можно отправить на пенсию.
После перехода на Чужого Франкенштейна можно отправить на пенсию. (Большой превью)

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