Русский
Русский
English
Статистика
Реклама

Iframe

Перевод Пришло время ленивой загрузки закадровых ltiframegt

26.07.2020 16:05:43 | Автор: admin

Нативная отложенная загрузка для изображений была добавлена в Chrome 76 через атрибут loading, а затем пришла и в Firefox. Мы рады сообщить, что встроенная отложенная загрузка для iframe теперь стандартизирована и также поддерживается в браузерах на основе Chrome и Chromium.


<iframe src="http://personeltest.ru/aways/example.com"        loading="lazy"        width="600"        height="400"></iframe>

Встроенная lazy-loading загрузка iframe откладывает загрузку закадровых элементов iframe до тех пор, пока пользователь не прокрутит их рядом. Это сохраняет данные, ускоряет загрузку других частей страницы и уменьшает использование памяти.



Это Демо показывает вставки отложенной загрузки видео:


Почему следует загружать iframes отложено?


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



Основываясь на исследованиях Chrome по автоматической отложенной загрузке закадровых iframe для пользователей Data Saver, было установлено, что отсрочивание загрузки фреймов может привести к 2-3% экономии данных, к 1-2% сокращению значения First Contentful Paint на средних значениях и 2% к первой задержке ввода (FID) улучшения на 95-м процентиле.


Как работает нативная отложенная загрузка для фреймов?


loading атрибут позволяет браузеру откладывать загрузку закадровых фреймов и изображений до тех пор, пока пользователи не прокрутят их рядом. Загрузка поддерживает три значения:


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

*auto в настоящее время является нестандартизированным значением, но по умолчанию именно это значение использует Chrome. Корпорация Chrome намерена внести предложение по этому значению в таблицу стандартов.

Использование атрибута loading в iframes работает следующим образом:


<!-- Отложенная загрузка iframe --><iframe src="http://personeltest.ru/aways/example.com"         loading="lazy"         width="600"         height="400"></iframe><!-- Загрузка iframe по готовности --><iframe src="http://personeltest.ru/aways/example.com"         width="600"         height="400"></iframe><!-- или используйте loading="eager" что бы выйти из автоматического lazy-loading режима в Lite Mode --><iframe src="http://personeltest.ru/aways/example.com"         loading="eager"         width="600"         height="400"></iframe>

Отсутствие указного атрибута loading будет иметь тот же эффект, что и явная загрузка ресурса, за исключением пользователей включивших облегченный режим(Lite Mode), где Chrome будет использовать значение auto, чтобы решить, следует ли загружать его с отложенной загрузкой.


Если вам нужно динамически создавать iframe через JavaScript, то для элемента также поддерживается установка iframe.loading = 'lazy':


var iframe = document.createElement('iframe');iframe.src = 'https://example.com';iframe.loading = 'lazy';document.body.appendChild(iframe);

Специфичное для iframe поведение с отложенной загрузкой


Атрибут loading влияет на фреймы иначе, чем на изображения, в зависимости от того, скрыт ли фрейм. (Скрытые элементы iframe часто используются в аналитических или коммуникационных целях.)


Chrome использует следующие критерии для определения того, является ли iframe скрытым:


  • Ширина и высота iframe составляет 4 пикселя или меньше.
  • Если установлено значение display: none или visibility: hidden фрейм также является скрытым
  • iframe размещается вне экрана с использованием отрицательного положения X или Y
  • критерий скрытности применяется как к loading = lazy, так и к loading = auto.

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


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


Какое влияние мы можем увидеть от отложенных загрузок встраиваемых в iframe?


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


Загрузка видео YouTube с отложенной загрузкой (экономит ~ 500 КБ при начальной загрузке страницы):

<iframe src="http://personeltest.ru/aways/www.youtube.com/embed/YJGCZCaIZkQ"        loading="lazy"         width="560"         height="315"         frameborder="0"         allow="accelerometer; autoplay;         encrypted-media; gyroscope;         picture-in-picture"         allowfullscreen></iframe>

Немного юмора
Когда мы переключились на отложенную загрузку iframe с YouTube для сайта Chrome.com, мы сэкономили 10 секунд от того значения, как скоро наши страницы станут интерактивными на мобильных устройствах. На этом этапе я обнаружил внутреннюю ошибку YouTube, чтобы обсудить добавление loading = lazy к его встраиваемому коду.

Если вы ищете более эффективные способы загрузки встраиваемого YouTube контента, вас может заинтересовать компонент YouTube lite.

Вставка в Instagram с отложенной загрузкой (экономит> 100 КБ в разархивированном виде при начальной загрузке):


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


Загрузка Spotify с отложенной загрузкой (экономит 514 КБ при начальной загрузке)


<iframe src="http://personeltest.ru/aways/open.spotify.com/embed/album/1DFixLWuPkv3KT3TnV35m3"         loading="lazy"        width="300"         height="380"         frameborder="0"         allowtransparency="true"         allow="encrypted-media"></iframe>

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


Нативная загрузка социальных плагинов Facebook


Социальные плагины Facebook позволяют разработчикам встраивать контент Facebook в свои веб-страницы. Предлагается несколько таких плагинов, таких как встроенные записи, фотографии, видео, комментарии Самым популярным является плагин Мне нравится кнопка, которая показывает количество понравившихся страниц. По умолчанию встраивание плагина Like в веб-страницу (с использованием FB JSSDK) потребляет ~ 215 КБ ресурсов, из которых 197 КБ JavaScript. Во многих случаях плагин может появляться в конце статьи или ближе к концу страницы, поэтому загрузка его по умолчанию, когда он находится за кадром видимости пользователя, может быть неоптимальной.



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


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


Они, конечно, могут. В Chrome 77 в Хром добавлена поддержка автоматической отложенной загрузки закадровых изображений и фреймов, когда пользователь выбрал облегченный режим Lite Mode (режим сохранения данных) в Chrome для Android.


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


Геолокация может определять, какой процент их трафика поступает от пользователей облегченного режима, проверяя свойство navigator.connection.saveData, которое является частью API NetworkInformation.


Могу я обеспечить отложенную загрузку для фреймов кросс-браузерно? Да


Нативная отложенная загрузка iframe может применяться как прогрессивное улучшение. Браузеры, поддерживающие loading=lazy для iframes, будут загружать iframe с отложенной загрузкой, в то время как атрибут загрузки будет безопасно игнорироваться в браузерах, которые его ещё не поддерживают.


Также можно лениво загружать закадровые iframes с помощью библиотеки JavaScript lazysizes. Это может быть оправдано, если вы:


  • требуете большего количества пользовательских порогов отложенной загрузки, чем то, что в настоящее время предлагает встроенная отложенная загрузка
  • хотите предложить пользователям постоянную загрузку iframe в разных браузерах
    <script src="lazysizes.min.js" async></script><iframe frameborder="0"    class="lazyload"    allowfullscreen=""    width="600"    height="400"    data-src="http://personeltest.ru/aways/www.youtube.com/embed/ZfV-aYdU4uE"></iframe>
    


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


<iframe frameborder="0"  class="lazyload"    loading="lazy"    allowfullscreen=""    width="600"    height="400"    data-src="http://personeltest.ru/aways/www.youtube.com/embed/ZfV-aYdU4uE"></iframe><script>  if ('loading' in HTMLIFrameElement.prototype) {    const iframes = document.querySelectorAll('iframe[loading="lazy"]');    iframes.forEach(iframe => {      iframe.src = iframe.dataset.src;    });  } else {    // Dynamically import the LazySizes library    const script = document.createElement('script');    script.src =      'https://cdnjs.cloudflare.com/ajax/libs/lazysizes/5.2.2/lazysizes.min.js';    document.body.appendChild(script);  }</script>

Вывод


Поддержка встроенных iframe с отложенной загрузкой значительно упрощает повышение производительности ваших веб-страниц. Если у вас есть какие-либо отзывы о lazy-loading загрузке встроенного iframe, не стесняйтесь отправлять сообщения в Chromium Bug Tracker.


Благодарности Addy Osmani за статью. Если вас интересует более глубокое погружение в тег iframe предлагаю вашему вниманию статью Исчерпывающий путеводитель по тегу iframe

Подробнее..

Микрофронтенды и виджеты в 2021-м. Доклад Яндекса

07.05.2021 12:20:51 | Автор: admin
Давайте поговорим о микрофронтендах и о встраиваемых виджетах, которые, по сути, были предшественниками концепции микрофронтендов. В докладе я рассказал о способах встраивать виджеты на страницу, об их плюсах и минусах с точки зрения изоляции и производительности кода, а также о способах применять виджеты в микрофронтендной архитектуре.

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

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

Микрофронтенды дальнейшее развитие этой идеи. Мы наше монолитное фронтенд-приложение также разделяем на независимые куски, независимые сервисы. Каждый сервис делается выделенной командой. Это может быть команда, у которой собственный фронтенд и бэкенд.

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

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

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

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

Евангелистом идеи микрофронтендов я совершенно не являюсь. У этой идеи, как и у всех, есть свои плюсы и минусы. Давайте мы с вами о них поговорим.

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

Какие у этой концепции проблемы? Она сильно усложняет код, дает дополнительные накладные расходы на интеграцию и взаимодействие виджетов в микрофронтендах между собой, накладывает требования по обратной совместимости чтобы API, через который виджеты общаются, при релизах не ломался и ваше приложение не развалилось. Свобода в технологическом стеке наверное, тоже своего рода минус. Думаю, вам не очень хотелось бы пользоваться сайтом, который для отрисовки одного кусочка грузит Angular, а для другого React, это будет работать не слишком быстро. Так что свобода это одновременно и плюс, и минус.

Зачем вам эту концепцию использовать? Я для себя на этот вопрос ответил двумя пунктами. Первый: у вас большое приложение и несколько независимых команд разработки.

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

Но это все лирика. Поговорим о том, как технически реализовать микрофронтенды так, чтобы они обеспечивали все бонусы, о которых мы говорили, и при этом все работало производительно и быстро.

Как технически реализовать микрофронтенды/виджеты?


declare const DoggyWidget: {    init: ({        container: HTMLElement,    }) => DoggyWidgetInstance;}declare interface DoggyWidgetInstance {    destroy(): void;    updateDoggy(): void;}

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

Из чего он будет состоять? В первую очередь он будет декларировать глобальный namespace DoggyWidget, в котором будет фабрика и с помощью которого можно создать инстанс этого виджета. У инстанса будет два метода. Первый метод destroy, который при вызове удалит виджет со страницы и почистит всё, что он успел сделать с DOM-ом. Второй метод updateDoggy, который делает то же самое, что нажатие на кнопку, а именно меняет картинку.

Давайте подумаем, как такой виджет реализовать.

<script>


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

class Widget {    constructor({ container }) {        this.container = container;        container.classList.add('doggy-widget');        this._renderImg();        this._renderBtn();        this.updateDoggy();    } }

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

    _renderImg() {        this.img = document.createElement('img');        this.img.classList.add('doggy-widget__img');        this.img.alt = 'doggy';        this.container.appendChild(this.img);    }

Что будет делать renderImg? Он будет создавать тег img, навешивать на него className и аппендить его в контейнер.

    _renderBtn() {        this.btn = document.createElement('button');        this.btn.classList.add('doggy-widget__btn');        this.btn.addEventListener('click', () => this.updateDoggy());        this.container.appendChild(this.btn);        this.btn.innerText = 'New doggy!';    } 

renderBtn будет делать примерно то же самое, только он будет создавать не img, а кнопочку.

    updateDoggy() {        const { width, height } = this.container.getBoundingClientRect();        const src = `https://placedog.net/${width - 10}/${height - 10}?random=${Math.random()}`;        this.img.src = src;    }

И у нас еще есть публичный API. updateDoggy определяет параметры контейнера, куда мы вставили виджет, конструирует ссылку на изображение. Я здесь буду использовать сервис placedog.net, который подставляет рандомные плейсхолдеры с фотками собак. Метод src ставит тег img.

    destroy() {        this.container.innerHTML = '';        this.container.classList.remove('doggy-widget');    }

destroy будет очень простой он будет подчищать innerHTML у контейнера и снимать с него className, который мы поставили в конструкторе.

(() => {    class Widget {        ...    }    window.DoggyWidget = {        init(config) {            return new Widget(config);        }    }})();

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

<script src="doggy-widget.js"></script><link rel="stylesheet" href="doggy-widget.css"><div id="widget-1"></div><div id="widget-2"></div><script>    const widget1 = DoggyWidget.init({         container: document.getElementById('widget-1'),    });    const widget2 = DoggyWidget.init({         container: document.getElementById('widget-2'),    });</script>

Как это все будет ставиться на страничку? Вот два файла: doggy-widget.js с JS-кодом, который мы разобрали, и doggy-wodget.css со стилями для виджета.

Мы заведем два div, и в каждый из них вставим виджет через DoggyWidget.init(), который мы тоже в doggy-widget.js описали.

Ссылка со слайда

Это все будет выглядеть так. У первого виджета будет updateDoggy.

Ссылка со слайда

Мы его вызовем. Он изменит нам фотографию.

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

Ссылка со слайда

        * {            font-family:         Arial, Helvetica, sans-serif !important;            font-size: 10px !important;        }

Представим, что мы наш виджет встроили на страничку, где находится вот такой CSS-код.

Ссылка со слайда

Что произойдет, когда мы отрисуем виджет? Очевидно, у него поедет верстка, потому что у нас есть глобальный CSS selector, который для всех элементов переопределяет font-family и font-size. Так что виджет не очень хорошо изолирован от окружающего его CSS-кода.

Вы скажете, что это вредительство и такого CSS никто не пишет.


Ссылка со слайда

<link rel="stylesheet"       href="bootstrap.min.css">*, ::after, ::before {    box-sizing: border-box;}

Окей, рассмотрим чуть более реальный пример. Мы встраиваемся на страничку, на которой используется Bootstrap, например. В Bootstrap есть такой код, который всем элементам задает box-sizing.

Предположим, мы наш виджет отрисуем на такой страничке:

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

Как этого можно избежать? Первый вариант: есть достаточно старый проект cleanslate.css.

<body>  <div class="blah">      <!-- general content is not affected -->      <div class="myContainer cleanslate">          <!-- this content will be reset -->      </div>  </div></body>

Это специальный CSS reset, который перезагружает стили не на всей страничке, а только на том div, где стоит класс cleanslate. Всё, что находится внутри cleanslate, будет переопределено, у него будут дефолтные зарезеченные стили.

Либо есть более современное решение, которое использует часть спецификаций веб-компонентов, а именно Shadow DOM.

Shadow DOM это такой способ отрисовать часть DOM-дерева изолированно и скрыто от других элементов на страничке. С помощью Shadow DOM рисуются встроенные в браузер контролы, например, input range. Если вы посмотрите на него в dev tools, там внутри в shadow root находится верстка, стилизованная с помощью CSS, который зашит в движок браузера.

    constructor({ container }) {        this.shadowRoot = container.attachShadow(            { mode: 'open' }        );        this.innerContainer = document.createElement('div');        this.innerContainer.classList.add('doggy-widget');        this.shadowRoot.appendChild(this.innerContainer);            }

Окей, попробуем заюзать Shadow DOM для нашего виджета. Что нам для этого нужно? В конструкторе мы приаттачим в контейнер shadowRoot, создадим еще один div, назовем его innerContainer и зааппендим его внутрь нашего shadowRoot.

    _renderImg() {                this.innerContainer.appendChild(this.img);    }    _renderBtn() {                this.innerContainer.appendChild(this.btn);    }

И нам потребуется немного переделать методы renderImg(), renderBtn(). Теперь мы будем картинку и кнопку складывать не в контейнер, который нам пришел, а в innerContainer, который мы уже положили внутрь shadowRoot.

    destroy() {                this.shadowRoot.innerHTML = '';    } 

Осталось еще немного поправить destroy. В destroy будем shadowRoot просто подчищать за собой.

Класс! Кажется, мы использовали Shadow DOM и смогли нашу верстку изолировать от другого кода.


Ссылка со слайда

В этом случае мы получим что-то такое у нас пропали все стили.


Что именно произошло? Изоляция, которую обеспечивает Shadow DOM, работает в обе стороны: она блокирует как вредоносные стили, которые нам не нужны, так и наши собственные стили, которые мы хотим добавить. Смотрите, link с doggy widget CSS остался снаружи shadowRoot, а верстка виджета находится внутри. Соответственно, правила, которые описаны снаружи, не влияют на то, что находится внутри shadowRoot.

     constructor() {                const link = document.createElement('link');        link.rel = 'stylesheet';        link.href = 'doggy-widget.css';        this.shadowRoot.appendChild(link);            }

<script src="doggy-widget.js"></script>

<link rel="stylesheet" href="doggy-widget.css">

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

Ссылка со слайда

Это будет работать примерно так. По ссылочке пример с подключенным Bootstrap, где можно посмотреть, что наш виджет по верстке получилось изолировать.

Единственная проблема, которую вы можете заметить, если откроете dev tools: на каждую инициализацию виджета появился отдельный запрос за doggy-widget.css. Здесь вам нужно будет убедиться, что у вас корректно настроено кеширование, чтобы повторно не грузить этот файл вашим клиентам.

Вроде изоляцию мы полечили. Или не совсем? Давайте немножко поиграем в шарады.

Опытные разработчики поймут, что здесь зашифрован monkey-patching. Это техника, которая нам позволяет делать прототипное наследование JavaScript, а именно изменять стандартную библиотеку. Например, через это работают полифилы. Мы можем в старый браузер притащить метод, который появился в новой спецификации, чтобы писать код, используя новые спеки. Но monkey-patching позволяет как делать хорошие штуки, так и очень сильно всё ломать.

Расскажу пример, с которым мы столкнулись, когда я работал в рекламных технологиях.

var str = JSON.stringify(['haha'])> '["haha"]'JSON.parse(str)> ["haha"]

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

Очевидно, если мы такую строку распарсим, то получим массив. Все хорошо.

var str = JSON.stringify(['haha'])> '"[\"haha\"]"'JSON.parse(str)> '["haha"]'

А вот на сайте одного из партнеров, куда мы этот виджет встраивали, мы видели такую картину.

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

Array.prototype.toJSON: () => Object

Стали разбираться, что происходит. JSON.stringify по спецификации, если у объекта определен метод toJSON, вызывает его. Метод должен вернуть сериализуемый объект, который дальше будет через JSON.stringify преобразован, чтобы получить строку.

Array.prototype.toJSON = function () {    var c = [];    this.each(function (a) {        var b = Object.toJSON(a);        if (!Object.isUndefined(b))            c.push(b)    });    return '[' + c.join(', ') + ']'}

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

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

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

var _json_stringify = JSON.stringify;JSON.stringify = function(value) {    var _array_tojson = Array.prototype.toJSON;    delete Array.prototype.toJSON;    var r=_json_stringify(value);    Array.prototype.toJSON = _array_tojson;    return r;};

Строго говоря, предлагается полечить monkey-patching еще одним monkey-patching, что не кажется очень хорошим решением.

Так что изоляция виджетов в верстке у нас работает, а изолировать JS с таким подходом получится не очень хорошо.

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

Давайте попробуем сломать наш виджет, посмотреть, как такой мониторинг будет работать и что он нам даст.

    _renderImg() {        const img = document.createElement(img');        this.img = img;        img.classList.add('doggy-widget__img');        img.alt = 'doggy';        this.container.appendChild(this.img);         this.updateDoggy(img);    }

Если помните, у нас был метод renderImg, который отрисовывал картинку. Давайте мы его сломаем, а именно удалим третью строчку, которая img кладет в поле нашего класса.

Что произойдет? Начальная отрисовка у нас отработает.

Ссылка со слайда

А вот если мы нажмем на кнопочку, то увидим exception.

window.addEventListener('error', (e) => {    console.log('got error:', e.error);    e.preventDefault();});


Как этот exception можно поймать, обработать и залогировать? Что делают те сервисы, которые я показывал несколько слайдов назад? Есть глобальный ивент 'error', который срабатывает на объекте window. На него можно подписаться и получить из этого ивента объект ошибки, которая произошла и которую вы не отловили через try-catch. У ивента можно вызвать preventDefault, чтобы также скрыть красную ошибку в консольке и не пугать ваших пользователей, которые внезапно решили открыть devtools.

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

window.addEventListener('unhandledrejection', (e) => {    console.log('got promise reject:', e.reason);    e.preventDefault();});

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

Promise.reject(new Error('bla'))

Подумаем. Если мы используем микрофронтенды и хотим мониторить ошибки, которые в каждом из виджетов происходят, все наши виджеты будут сыпать эксепшены в один и тот же обработчик:


window.addEventListener('error', (e) => {    console.log('got error:', e.error);    e.preventDefault();});

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

Так что независимые мониторинги при таком подходе мы не получаем.

Давайте подведем промежуточные итоги. Что нам дает использование независимых скриптов?

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

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

Тем не менее, эта идея активно используется. Например, один из популярных фреймворков для построения микрофронтендов single-spa как раз на ней, в общем-то, и построен.

Что делать, если нам это все не подходит и хочется больше изоляции? Здесь поможет старая технология iframe.

<iframe>


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

(() => {    window.DoggyWidget = {        init({ container }) {            const iframe = document.createElement('iframe');        }    }})();

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

(() => {    window.DoggyWidget = {        init({ container }) {            const iframe = document.createElement('iframe');            iframe.style.width = '100%';            iframe.style.height = '100%';            iframe.style.borderWidth = 0;            iframe.style.display = 'block';            iframe.src = 'https://some-url/doggy-widget.html';                    }    }})();

В фабрике init нашего виджета нам нужно будет создать iframe и повесить на него стили. Мы поставим width и height 100%, чтобы он полностью растягивался до размеров контейнера, куда его вставили. Мы переопределим ему display и поставим границу 0, потому что по дефолту браузеры рисуют border.

Внутри iframe загрузим документ, в котором будет рендериться наш виджет.

(() => {    window.DoggyWidget = {        init({ container }) {            const iframe = document.createElement(iframe');            iframe.style.width = '100%';            iframe.style.height = '100%';            iframe.style.borderWidth = 0;            iframe.style.display = 'block';            iframe.src = 'https://some-url/doggy-widget.html';            container.appendChild(iframe);                        ...        }    }})();

Осталось зааппендить этот iframe внутрь контейнера.


Ссылка со слайда

Все будет работать, виджет будет отрисовываться.

Вроде здорово. Мы использовали iframe, виджет в нем работает, не подвержен влиянию другого кода на страничке. У нас есть полная изоляция, независимые мониторинги внутри iframe.

declare const DoggyWidget: {    init: ({        container: HTMLElement,    }) => DoggyWidgetInstance;}declare interface DoggyWidgetInstance {    destroy(): void;    updateDoggy(): void;}

Но мы кое о чем забыли. У нашего виджета есть API. У инстанса есть destroy и updateDoggy. Давайте попробуем их реализовать.

destroy() {    this.container.innerHTML = '';}

destroy будет суперпростой. Нам нужно будет просто почистить контейнер, если вы не используете этого парня. В IE 11 и legacy Edge есть неприятный баг, связанный с тем, что контекст JS, который работает внутри фрейма, продолжает частично жить после удаления iframe из DOM. Что значит частично? В нем ломается стандартная библиотека, перестают, например, быть доступны объекты Date, Object, Array и прочее. Но асинхронный код, сет таймауты, сет интервалы, реакция на ивенты, которая там была, продолжают работать, и вы можете в ваших мониторингах в таком случае увидеть очень странные эксепшены из IE и legacy Edge о том, что у вас вдруг пропал Date, он стал undefined.

Чтобы это обойти, нам наш iframe предварительно перед удалением его из DOM нужно будет вот таким образом почистить. Тогда IE 11 и старый Edge корректно его задестроят и остановят весь JS-код, который внутри него выполнялся.

destroy() {    // чистим iframe для ie11 и legacy edge     this.iframe.src = '';    this.container.innerHTML = '';}


Ссылка со слайдов

Proof of concept destroy работает.

Что еще? У нас остался updateDoggy, для него нам нужно обновить картинку, которая рисуется внутри фрейма. Соответственно, сделать какое-то действие между нашим основным документом, отправить команду внутрь iframe. Здесь есть проблема. Если iframe загружается с другого хоста, браузер заблокирует любое взаимодействие с window внутри фрейма и вы получите примерно такую ошибку.

Как же все-таки можно взаимодействовать? Для взаимодействия нужно использовать postMessage. Это API, который позволяет отправить сериализуемую команду внутрь другого window, и внутри этого window подписаться на объект message, прочитать то, что было в команде. И отреагировать на нее.

updateDoggy() {    this.iframe.contentWindow        .postMessage({ command: 'updateDoggy' });}

Давайте реализуем updateDoggy через postMessage. В родительском документе у нас будет отправляться сообщение с командой updateDoggy внутрь iframe.

window.addEventListener('message', (e) => {    if (e.data.command === 'updateDoggy') {        widget.updateDoggy();    }})

И внутри iframe нам нужно будет написать вот такой код, который подписывается на события message, а если там updateDoggy, то дергает updateDoggy у виджета, который перерисует нам картинку.


Ссылка со слайдов

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

События, которые происходят внутри iframe, наружу не всплывают. Если вы хотите реагировать, например, снаружи на то, что пользователь кликнул внутри виджета, то вам нужно отправлять postMessage наверх. Использовать addEventListener напрямую у вас не получится событие через iframe не всплывет.

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

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

Что мы в итоге получаем? У нас сильно усложняется код. И еще появляются накладные расходы.

Если мы вспомним, как рисовался наш виджет, вставляемый через скрипт, это это выглядело бы так. У нас бы загружалась страничка, загружался CSS, JS. Дальше, когда виджет рисовался бы, каждый виджет запрашивал бы для себя картинку.

Если мы рассмотрим наш новый вариант с iframes, мы увидим такое. Внутри каждого виджета загрузится документ, у нас загрузится CSS, который там нужен, и JS, который внутри этого документа исполняется.

Для первого виджета, для второго. Сколько у вас их будет на странице, столько будет загрузок этих файлов?

Ссылка со слайда

Здесь могло бы помочь кеширование, но недавно браузеры сделали так, чтобы изолировать кеши друг от друга между различными сайтами. Это нужно, чтобы предотвратить трекинг посещения пользователем одного сайта с другого. То есть если на сайте номер 1 используется какая-то библиотечка, сайт номер 2 тоже может ее подключить и посмотреть через Performance API, была они ла загружена из кеша. Если да, то пользователь, скорее всего, до этого посещал сайт 1 и это можно как-то использовать. Браузеры сейчас от такого поведения стараются пользователей защищать.

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

К чему это приводит, если мы пытаемся строить микрофронтенды на виджетах, которые загружаются каждый в независимом iframe?

https://website.ru/    https://yastatic.net/react/16.8.4/react-with-dom.min.js    Widget #1        <iframe> https://widget-1.ru/            https://yastatic.net/react/16.8.4/react-with-dom.min.js    Widget #2        <iframe> https://widget-2.ru/            https://yastatic.net/react/16.8.4/react-with-dom.min.js

Допустим, у нас есть наш основной сайт, на котором подключен React. Есть виджет номер 1, на котором подключен React допустим, даже тот же самый bundle. И виджет номер 2 с еще одного хоста, на нем тоже подключен React.

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

Итак, что мы получаем с iframe? У нас есть полная изоляция виджетов в CSS. Есть полная изоляция JS, потому что документы не зависят друг от друга. Есть независимые мониторинги, потому что внутри каждого iframe свой собственный window, на котором мы можем ловить ошибки.

Но при этом сильно усложнился код, поскольку появилась асинхронность.

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

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

Выглядит так, будто у нас есть два варианта: один с использованием скрипта, один с использованием iframe.

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

Здесь поможет так называемый friendly <iframe>. Вы еще можете встретить название same-origin <iframe>, или anonymous <iframe>.

const globalOne = window;let iframe = document.createElement('iframe');document.body.appendChild(iframe);const globalTwo = iframe.contentWindow;

В чем идея? Есть глобальная область наш текущий window. Можно создать через createElement новый iframe и зааппендить его на страничку. При этом заметьте, что я внутри этого фрейма никакой документ не загружаю, дополнительного запроса за HTML здесь не будет и внутри документа окажется пустая страничка, которую туда автоматически подложит браузер.

Теперь contentWindow этого iframe можно рассматривать как еще один независимый контекст, который мы можем использовать.

foobar.js:
window.someMethod = () => {...}

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

Вот наш скрипт foobar.js, который в глобальную область добавляет метод. Как подключить его внутрь нашего нового контекста? Создаем скрипт, ставим ему src и аппендим внутрь head нашего iframe.

const script = document.createElement(script);script.src = 'foobar.js';globalTwo.head.appendChild(script);

Теперь, чтобы взаимодействовать с кодом внутри этого скрипта, нам больше не нужно использовать postMessage, потому что контекст у нас same-origin:

globalTwo.postMessage();

globalTwo.someMethod();

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

Давайте попробуем, используя эту идею, переписать наш виджет еще раз.

Единственное, нам надо понять, где у нас будет находиться верстка. Если помните, я сказал, что внутрь friendly frame документ мы не загружаем, но верстку надо где-то отрисовать.

А верстку мы будем рисовать в контексте родительской страницы и будем использовать Shadow DOM. Попробуем взять лучшее из двух подходов, которые мы до это рассматривали.

Как теперь будет выглядеть фабрика нашего виджета?

const iframe = document.createElement('iframe');document.head.appendChild(iframe);const script = document.createElement('script');script.src = 'doggy-widget-inner.js';const loaded = new Promise((resolve) => {    script.onload = resolve;});loaded.then(() => {    iframe.contentWindow.init(config);})iframe.contentDocument.head.appendChild(script);

Создаем в нем iframe, загружаем внутрь этого iframe скрипт и сохраняем promise, который зарезолвится, когда этот скрипт загрузится.

После того, как он прогрузился, мы вызовем внутри нашего виджета init и передадим его config, который отрисует виджет внутри. Нам осталось зааппендить скрипт в head нашего iframe.

Как теперь преобразуется doggy-widget-inner.js, код, который работает внутри фрейма?

window.init = (config) => {    const widget = new Widget(config);    window.widget = widget;};

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

Как в итоге все будет работать? Если мы отрисуем таким способом два виджета на страничке, то получим примерно такое DOM-дерево.



Ссылка со слайдов

Для каждого виджета у нас будет в хэде скрытый friendly iframe, который пользователь не видит, но при этом код внутри него исполняется и с ним можно работать. Для каждого виджета в контейнере, который мы передали, будет использоваться shadow root, внутри которого будет находиться верстка этого конкретного виджета. Вот для первого виджета, а вот для второго.

Код целиком:

<head>    <iframe>        #document            <html>                <head>                    <script src="doggy-widget-inner.js"></script>                </head>                <body></body>            </html>    </iframe>    <iframe>        #document            <html>                <head>                    <script src="doggy-widget-inner.js"></script>                </head>                <body></body>            </html>    </iframe></head><body>    <div id="widget-1">        #shadow-root            <link rel="stylesheet" href="doggy-widget.css">            <div class="doggy-widget">                <img class="doggy-widget__img"/>                <button class="doggy-widget__btn"/>            </div>    </div>    <div id="widget-2">        #shadow-root            <link rel="stylesheet" href="doggy-widget.css">            <div class="doggy-widget">                <img class="doggy-widget__img"/>                <button class="doggy-widget__btn"/>            </div>    </div>    <script src="doggy-widget.js"></script></body>

Что этот подход нам дает? Мы получаем:

  • Полную изоляцию наших виджетов в CSS, потому что используем Shadow DOM.
  • Полную изоляцию в JS, потому что код работает внутри выделенного iframe, и какой-либо monkey-patching в родительском документе на него никак не влияет.
  • Независимые мониторинги, потому что код виджета работает, опять-таки, в независимом window, где мы можем слушать эксепшены.
  • Работающее кеширование, так как контекст same-origin в браузере больше не изолирует кеши между виджетами.

При этом все еще есть:

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

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

Немного поговорим о том, что ждет нас в светлом будущем. Там нас ждет спецификация Realms API. Она сейчас находится в TC39 на Stage 2, это draft. Активно идет написание стандарта. Спецификация развивается. Надеемся, что скоро она перейдет на stage 3.

Что она позволяет делать? Вспомним, как мы создавали friendly frame. У нас был глобальный контекст globalOne. Мы создавали элемент iframe, аппендили его в документ и получали globalTwo еще один независимый контекст внутри этого фрейма.

const globalOne = window;let iframe = document.createElement('iframe');document.body.appendChild(iframe);const globalTwo = iframe.contentWindow;

const globalOne = window;const globalTwo = new Realm().globalThis;

Realms позволяет это заменить на такую конструкцию. Появляется новый глобальный объект Realm. Создав инстанс Realm, вы получаете внутри него globalThis, который является как раз тем самым независимым контекстом, который при этом работает оптимальнее, чем отдельный iframe.

Как внутри Realm можно будет исполнить код? Через вызов импорта.

const realm = new Realm();const { doSomething } = await realm.import(    ./file.js');doSomething();

Заимпортируем какой-нибудь JS-файл, который экспортирует метод doSomething. Его сразу можно будет вызвать, он будет работать в контексте Realm независимо от основной странички.

У этого API даже есть полифильчик, который построен как раз с использованием friendly frame. Правда, полифил, похоже, стал deprecated. Там года два уже не было коммитов, и он сильно отстал от текущего драфта спеки. Но надеюсь, с развитием спецификации его оживят.

Итоги


Используйте микрофронтендный подход с умом, только если вам это действительно нужно. Выбирайте подходящую вам техническую реализацию. И следите за спецификациями. Эта тематика активно развивается, в будущем у нас появятся более удобные способы строить микрофронтенды. Спасибо!
Подробнее..

Микрофронтенды. Учимся на ошибках

18.08.2020 10:04:18 | Автор: admin
В этой статье я расскажу с какими проблемами удалось столкнуться при построении микрофронтендов, каким образом их можно было бы избежать, а также немного привнести полученного опыта в довольно редкоподнимаемую тему микрофронтендов.



Микрофронтенды на iframe


В одной компании взвешенным и обдуманным решением CTO было принято, что микрофронтендам наравне с микросервисами быть, причем сервировать их надо на iframe'ах.

В качестве аргумента, кстати, был приведен продукт Office 360 от Microsoft, раньше там использовался `<iframe />` для верхнего и бокового меню. Теперь iframe'ов там нет.

Причины и предпосылки для микрофронтендов


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

Все имеющиеся приложения это React SPA. Ничего общего кроме Material UI, React Router v4 и зародыша UI-kit в качестве npm-модулей, не имеют.

Часть приложений будет использоваться и поставляться как в standalone-варианте, так и как часть другого приложения.

Микрофронтенды были поделены по крупным функциональным блокам:

1. Шапка приложения. Роутинг между приложениями
2. Приложение-дэшбоард. С метрикой и виджетами. Каждый виджет дэшборда должен быть частью соответсвующего приложения (подключаться по iframe).
3. Сами приложения. Некоторые из них включают части друг друга (но без фанатизма и рекурсии)



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

Проблема #0. Неправильное разделение на микрофронтенды


К сожалению, микрофронтенды не были продуманы достаточно глубого. В качестве примера можно привести интернет-магазин. Кнопка купить и корзина могут быть разбросаны по множеству мест, но все они один микрофронтенд. Как карточка товара в 10 вариациях, так и процесс оформления заказа (где платежки и адреса). Все это могут быть отдельными микрофронтендами со своими релизными циклами.



В реальности, получилось так, что приложения разделили очень грубо. В аналогии с интернет-магазином это когда страницу корзины и оформления заказа делает одна команда, а все остальное вторая команда (включая счетчики корзины на всех остальных страницах). При этом все используют одну и ту же бизнес-логику, а интерфейс переиспользуется на уровне UI-kit, или библиотеки Material-UI.

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

Как результат, экономия времени на переиспользовании приложений не удалась.
Дублирование функционала все также осталось на высоком уровне. Использование микрофронтендов без iframe или компонентов с более сложной логикой из расширенного UI-kit могло бы решить проблему дублирования функионала.

Проблема #1. Отсутствие оркестрации процесса


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

Чтобы нивелировать все CORS проблемы раз и навсегда, было принято решение посадить nginx, который бы разруливал маршрутизацию. Таким образом, каждый микрофронтенд и каждый микросервис имел свой адрес, например:

https://domain.zone/dashboard
https://domain.zone/header
https://domain.zone/app1
https://domain.zone/app2
https://domain.zone/api1
https://domain.zone/api2


Остается вопрос, как тестировать приложеняи во время режима разработки? Каждое приложение будет сервироваться на своем порту?

Здесь приходит на помощь пакет `http-proxy-middleware` которое можно настроить в паре с CRA. Оказалось, только половина front-end разработчиков оказалось в силе настроить такой сетап. Никого обвинять здесь, разумеется, нельзя, но такая проблема появилась, а решать ее надо организационно.

Также необходима четкая версионность всех приложений с описанием функционала, доступных методов и внутреннего API. Отсюда вытекает следующая проблема: документация.

Проблема #2. Отсутствие внутреннего API


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

Это очень критическая часть системы в случае распределенных команд, да еще с учетом текучки кадров.

Также необходимо разработать механизм взаимодействия между приложениями. Тут на помощь приходит `postMessage API` или же прямой доступ к другому, встроенному почти в каждое React-приложение Redux. Чем не message bus? Но об этом позже.

Проблема #3. Iframe не обладает достаточной гибкостью


В использовании тега `<iframe />` нет ничего плохого. Это мощный инструмент с встроенным message bus (postMessage API) и широкой настройкой безопасности.

Однако, в случае микрофронтендов, `<iframe />` накладывает большое количество ограничений. Одно из них невозможность переиспользовать одно приложение в нескольких частях страницы.

Переиспользование приложений

В случае с аналогией интернет-магазина, 10 кнопок Купить создадут 10 `<iframe />`, то есть 10 запущенных React-приложений. Никакой памяти на это не хватит. Это одна из причин, по которой разделение приложения на команды по фичам не выдается возможным.

URL в качестве управлением стейтом не подходит

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

Вот пример, как то фиолетовое приложение из КДПВ с другим URL может работать как standalone приложение:



Однако, использовать URL интерфейс iframe для переключения состояния микрофронтенда в нашем случае оказалось невозможным: микрофронтенд начинает грузиться с нуля из-за неполной интеграции history API с его `pushState`и React Router получаем полное обновление страницы.

Обработчики кликов вне области `iframe`

Представьте, что вы заходите сделать выпадающее меню. Как на схеме выше: из розового меню. А еще и закрывать его по клику по пустому месту. В случае с iframe, вам необходимо использовать условный postMessage API, так как клик вне по просту не распознается из-за разных объектов window. Либо же придумывать хак с прозрачным фоном увеличенного элемента iframe во весь экран.

Кстати, изменение размеров iframe и подстраивание под него родительского приложения тоже становится более громоздкой и сложной.

Бонусная проблема: Несоразмерное использоваение Cookie


Эта проблема не относится напрямую к микрофронтендам, но с ними она переходит на следующий уровень безумства.

Было решено записывать в авторизационные куки не только токен, но и полный список прав на те или иные части приложения. Все это шифровалось SHA-??? и конвертировалось в base64.
Как результат, размер кук превышал 8KB, что является значением по умолчанию для nodejs/nginx, (или 2KB для размера одной записи Cookie в Google Chrome), что привело более сложной настройке серверов (без eject CRA уже не запустить с такой настройкой), а также к разделению этого большого зашифрованного массива данных на более мелкие куки-строки.

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

Заключение. Как тогда жить с микрофронтендами?


Для начала, конечно, нужно определиться с тем, а нужны ли микрофронтенды?
Зачастую, правильно настроенный и обогащенный компонентами UI-kit в виде npm модуля решает проблему и независимых релизов, и одинакового визуального стиля.

  • Не использовать iframe. Он не упрощает работу, а лишь добавляет проблем с производительностью, сильно ограничивая в возможности разбиения приложения на микрофронтенды.
    Разработать оркестрацию процесса: как в продакшене, так и для разработки. Не каждый разработчик захочет лезть в смежную отрасль маршрутизации и прокси, когда он нанимался клепать интерфейсы из готовых блоков.
    Разработать message bus между приложениями. Это намного проще в случае единого глобального пространства объекта window.
    Создать документацию по интерфейсу взаимодействия приложений, а также их запуска и настройки.
Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru