Микрофронтенд "контейнер" - реализация микрокосмоса

внешний фреймворк
Микрофронтенд "контейнер" - реализация микрокосмоса

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

Тогда спасибо за вашу звезду, пиар, конечно, приветствуется~

Что такое микрофронтенд

​ Впервые я услышал о концепции микроинтерфейса, когда случайно увидел технический блог от Meituan около года назад:Создавайте одностраничные приложения с микроинтерфейсами. Однако на тот момент я даже не знал, что такое одностраничное приложение, поэтому был в недоумении. В настоящее время принято считать, что концепция микроинтерфейса состоит изThoughtWorksПредставлен в 2016 году. За последние четыре года он быстро развивался, и в настоящее время мы смогли увидеть много отличных работ с открытым исходным кодом, таких какsingle-spa,qiankun,icestark,Micro Frontends etc.

Что такое микро-интерфейс? Фактически, изменение вопроса поможет нам лучше понять:Зачем нужны микрофронтенды?

Вы можете не знать микрофронтенды, но вы должны знать микросервисы.

Объяснение в Википедии такое:

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

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

Или дело просто в разлуке надолго, и разлуке надолго?

Я думаю, в это время вы, должно быть, подумали о другой концепции,составной. В чем разница между микро-фронтендом и компонентной разработкой? Чем отличается компонентизация? Я думаю, что их дизайнерские идеи одинаковы, включая микросервисы, упомянутые ранее. В прошлом мы придумали концепцию компонентной разработки, но сегодня она не оправдала наших ожиданий. Это правда, что основная цель компонентизации — добиться большей возможности повторного использования и ремонтопригодности, что похоже на микрофронтенды. Но степень детализации, с которой он разбивает приложение, является составной. МикрофронтендыФронтенд-приложения разбиваются на подприложения, которые можно разрабатывать, тестировать и развертывать независимо друг от друга, при этом они по-прежнему выглядят для пользователей как единый целостный продукт., степень детализации — приложение, и из-за независимой разработки мы ожидаемНезависимость от стека технологий,это очень важно. У меня пока нет опыта работы, поэтому сложно много говорить об этом.Эта статья разработчиков qiankun — хороший ответ на вопрос, почему стек-агностик так важен в микрофронтендах.Основная ценность микрофронтенда

Как выглядит идеальный микро-фронтенд? Я вполне согласен с точкой зрения Гармонии, т.Подпроекты не знают, что они работают как подпроекты. Однако сценарии общения между приложениями все же есть, иначе каждый не всегда будет делать акцент на общении отца и сына.

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

С точки зрения внешнего интерфейса их в основном два. Интеграция во время сборки и интеграция во время выполнения.

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

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

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

Вот что я используюmicrocosmosЯ написал демонстрацию микроинтерфейса, основное приложение содержит приложение vue и приложение для реагирования.

Я думаю, вы уже должны знать, что такое микрофронтенд. Далее рассмотрим техническую реализацию микрокосмоса.

Реализация микрокосмоса

Общая структура

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

Связанный API

представлять

npm i microcosmos

import { start, register,initCosmosStore } from 'microcosmos';

Зарегистрируйте дополнительное приложение

register([
  {
    name: 'sub-react',
    entry: "http://localhost:3001",
    container: "sub-react",
    matchRouter: "/sub-react"
  },
  {
    name: 'sub-vue',
    entry: "http://localhost:3002",
    container: "sub-vue",
    matchRouter: "/sub-vue"
  }
])

Начинать

start()

основная маршрутизация приложений

function App() {
 function goto(title, href) {
  window.history.pushState(href, title, href);
 }
 return (
    <div>
   <nav>
    <ol>
     <li onClick={(e) => goto('sub-vue', '/sub-vue')}>子应用一</li>
     <li onClick={(e) => goto('sub-react', '/sub-react')}>子应用二</li>
    </ol>
   </nav>
      <div id="sub-vue"></div>
      <div id="sub-react"></div>
  </div>
 )
}

Подприложения должны экспортировать хуки жизненного цикла

bootstrap、mount、unmount

export async function bootstrap() {
  console.log('react bootstrap')
}

export async function mount() {
  console.log('react mount')
  ReactDOM.render(<App />, document.getElementById('app-react'))
}

export async function unmount() {
  console.log('react unmout')
  let root = document.getElementById('sub-react');
  root.innerHTML = ''
}

Глобальная связь/хранение состояния

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

В основном приложении:

  • initCosmosStore: инициализировать хранилище

  • subscribeStore: прослушивание изменений в магазине

  • changeStore: Распределить новые значения для хранения

  • getStore: получить текущий снимок магазина

let store = initCosmosStore({ name: 'chuifengji' })

store.subscribeStore((newValue, oldValue) => {

  console.log(newValue, oldValue);

})

store.changeStore({ name: 'wzx' })

store.getStore();

В подприложении:

export async function mount(rootStore) {

  rootStore.subscribeStore((newValue, oldValue) => {
    console.log(newValue, oldValue);
  }
  
  rootStore.changeStore({ name: 'xjp' }).then(res => console.log(res))
  
  rootStore.getStore();
  
  instance = new Vue({
    router,
    store,
    render: h => h(App)
  }).$mount('#app-vue')
}

html-loader

HTML-загрузчик получает информацию о приложении, получая html страницы.Относительный метод - JS-загрузчик.Связь между Js-загрузчиком и подприложением выше, и подприложение должно соглашаться с основное приложение для перевозки контейнера.

Как работает html-загрузчик? На самом деле очень просто, то есть через адрес входа приложения, например:http://localhost:3001, а затем вызовите функцию выборки. После получения информации о текстовом формате html нам нужно вынуть нужную нам часть и установить ее на опорную точку субприложения. На следующем рисунке показана структура элементов демонстрационного интерфейса микроинтерфейса выше. Вы можете видеть, что под-приложение зависло вid является суб-реакциейпод этикеткой.

Как это сделать?

Я думаю, что ваша первая реакция может быть обычной, сначала я использовал обычную, чтобы справиться с этим, но позже я обнаружил, что обычную слишком сложно выполнить (простите мою обычную слепоту). Я всегда могу написать пример и позволить своему обычному экспортировать неправильный результат. . А при обычном написании код выглядит очень грязно, а последующее сопровождение не очень удобно. Поскольку это html-строка, почему бы нам не использовать dom api для ее обработки? Первая реакция — iframe, создать новый iframe напрямую и использовать атрибут src для загрузки iframe. Вопрос в том, как узнать, когда загружен iframe? Загрузка? Очевидно, что нет, мы просто хотим получить данные.DOMContentLoaded? Как и в следующем, написать готовую функцию еще недостаточно,DOMContentLoadedБудет ждать, пока JS выполнит мелодии. На этот раз может быть немного дольше Спа.

function iframeReady(iframe: HTMLIFrameElement, iframeName: string): Promise<Document> {
    return new Promise(function (resolve, reject) {
        window.frames[iframeName].addEventListener('DOMContentLoaded', () => {
            let html = iframe.contentDocument || (iframe.contentWindow as Window).document;
            resolve(html);
        });
    });
}

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

function iframeReady(iframe: HTMLIFrameElement): Promise<Document> {
    return new Promise(function (resolve, reject) {
        (function isiframeReady() {
            if (iframe.contentDocument.body || (iframe.contentWindow as Window).document.body) {
                resolve(iframe.contentDocument || (iframe.contentWindow as Window).document)
            } else {
                setInterval(isiframeReady, 10)
            }
        })()
    })
}

И чтобы получить iframecontentWindowЗатем вам нужно повесить iframe на дом, действительно, его можно установить наdisplay:none, но слишком неэлегантно. Как неудобно.

srcdoc— хороший выбор, но, к сожалению, IE не поддерживает это новое свойство.

Затем объедините регулярное выражение с DOM API. Мы получаем содержимое под узлами головы и тела через регуляризацию, Эти две регуляризации довольно легко выполнить, а затем использовать ихinnerHtmlприбытьcreateElementВ узле div он проходит через DOM API. Структура DOM стабильна, и мы можем легко и надежно получить нужный нам контент, то есть информацию о структуре html и js.

js-изоляция

Нет идеальной практики для песочницы микро-фронтенда?

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

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

Есть три основные идеи:

Снимок песочницы:

Песочница моментальных снимков фактически активируется и создает моментальные снимки при применении монтирования, а также деактивирует и восстанавливает исходную среду при размонтировании. Например, при монтировании приложения A глобальная переменная изменяется.window.appName = 'vue', то я могу записать текущий снимок (значение свойства до модификации). Когда приложение A удалено, я могу сравнить текущий снимок с текущей средой, изучить исходную среду и восстановить работающую среду.

class SnapshotSandbox {
    constructor() {
        this.proxy = window; 
        this.modifyPropsMap = {}; // 修改了哪些属性
        this.active();
    }
    active() {
        this.windowSnapshot = {}; // window对象的快照
        for (const prop in window) {
            if (window.hasOwnProperty(prop)) {
                // 将window上的属性进行拍照
                this.windowSnapshot[prop] = window[prop];
            }
        }
        Object.keys(this.modifyPropsMap).forEach(p => {
            window[p] = this.modifyPropsMap[p];
        });
    }
    inactive() {
        for (const prop in window) { // diff 差异
            if (window.hasOwnProperty(prop)) {
                // 将上次拍照的结果和本次window属性做对比
                if (window[prop] !== this.windowSnapshot[prop]) {
                    // 保存修改后的结果
                    this.modifyPropsMap[prop] = window[prop]; 
                    // 还原window
                    window[prop] = this.windowSnapshot[prop]; 
                }
            }
        }
    }
}

let sandbox = new SnapshotSandbox();
((window) => {
    window.a = 1;
    window.b = 2;
    window.c = 3
    console.log(a,b,c)
    sandbox.inactive();
    console.log(a,b,c)
})(sandbox.proxy);

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

Заимствование iframe:

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

прокси прокси:

class ProxySandbox {
    constructor() {
        const rawWindow = window;
        const fakeWindow = {}
        const proxy = new Proxy(fakeWindow, {
            set(target, p, value) {
                target[p] = value;
                return true
            },
            get(target, p) {
                return target[p] || rawWindow[p];
            }
        });
        this.proxy = proxy
    }
}
let sandbox1 = new ProxySandbox();
let sandbox2 = new ProxySandbox();
window.a = 1;
((window) => {
    window.a = {a:'ldl'};
    console.log(window.a)
})(sandbox1.proxy);a:'ldl'
((window) => {
    window.a = 'world';
    console.log(window.a)
})(sandbox2.proxy);

Вышеупомянутый прокси очень прост, при чтении преимущественно получается "копируемое значение", если нет, то проксируется к исходному значению, а при записи - к "копируемому значению". Но у него много проблем, не говоря уже о всевозможном вредоносном коде, если глобальный объект использует self, this, globalThis, прокси недействителен, и недостаточно только прокси получить и установить. Самое главное, что глобальные переменные изолированы лишь до определенной степени, а нативные объекты и методы окна недействительны.

function getOwnPropertyDescriptors(target: any) {
    const res: any = {}
    Reflect.ownKeys(target).forEach(key => {
        res[key] = Object.getOwnPropertyDescriptor(target, key)
    })
    return res
}


export function copyProp(target: any, source: any) {
    if (Array.isArray(target)) {
        for (let i = 0; i < source.length; i++) {
            if (!(i in target)) {
                target[i] = source[i];
            }
        }
    }
    else {
        const descriptors = getOwnPropertyDescriptors(source)
        //delete descriptors[DRAFT_STATE as any]
        let keys = Reflect.ownKeys(descriptors)
        for (let i = 0; i < keys.length; i++) {
            const key: any = keys[i]
            const desc = descriptors[key]
            if (desc.writable === false) {
                desc.writable = true
                desc.configurable = true
            }
            if (desc.get || desc.set)
                descriptors[key] = {
                    configurable: true,
                    writable: true, 
                    enumerable: desc.enumerable,
                    value: source[key]
                }
        }
        target = Object.create(Object.getPrototypeOf(source), descriptors)
        console.log(target)
    }
}

export function copyOnWrite(draftState: {
    originalValue: {
        [key: string]: any;
    };
    draftValue: any;
    onWrite: any;
    mutated: boolean;
}) {
    const { originalValue, draftValue, mutated, onWrite } = draftState;
    if (!mutated) {
        draftState.mutated = true;
        if (onWrite) {
            onWrite(draftValue);
        }
        copyProp(draftValue, originalValue);
    }
}

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

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

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

Если вам интересно, вы можете провести собственное исследование:immer

css-изоляция

Нужно ли нам рассматривать возможность изоляции CSS в дизайне контейнеров микрофронтенда?

На самом деле, я лично считаю, что это не то, что нужно учитывать контейнеру микро-фронтенда, потому что эта проблема не имеет никакого отношения к микро-фронтенду, это почти с момента зарождения CSS. Эпоха СПА стала проблемой, которую необходимо учитывать. Так что в микрокосмосе я не решил проблему изолированности css. Вам все еще нужно принять префикс проекта соглашения BEM (Block Element Modifier), модуль css, css-in-js и другие решения, такие как разработка SPA.

И как сказал ЦянькуньDynamic StylesheetНа самом деле это довольно скучно (я сам добавил hh), загрузка и выгрузка саб-приложений естественно включает в себя загрузку и выгрузку css, но это не гарантирует отсутствия конфликта между саб-приложением и основным приложением , не говоря уже о том, что параллельно может быть несколько подприложений. (Конечно, теперь они предложили и другие решения, которых стоит с нетерпением ждать!)

Тогда вы могли бы сказать: «Почему бы не теневой дом?»

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

Например, до React17, чтобы уменьшить количество объектов событий в DOM для экономии памяти, оптимизации производительности страницы, а также для реализации механизма планирования событий, все события делегировались элементу документа. а такжеshadow domСобытие, инициированное во внутреннем слое, получается во внешнем слое.event.target , получается только хост (элемент хоста), поэтому возникает проблема с планированием событий реакции.

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

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

Для теневого дома, потому что сценарий внутри основного документа не знает о теневом доме внутри, особенно когда компонент исходит из сторонней библиотеки, поэтому, чтобы не усложнять детали, браузер будетпереместить(перенацелить) событие.События, происходящие в теневой модели DOM, будут нацелены на элемент хоста, когда событие захватывается вне компонента..

Это заставит React обрабатывать синтетические события, не думая о том, что события, связанные с элементами в ShadowDOM на основе синтаксиса JSX, запускаются.

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

life-cycle

Цикл жизненного цикла — это большой обход.

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

lifeCycle будет проходить по списку подприложений и выполнять их функции жизненного цикла по очереди. Здесь есть небольшая проблема. Как функции жизненного цикла подприложений получаются основным приложением? Если вы не знакомы с webpack, как я, вы может попасть в этот Confused, на самом деле webpack начинается сumdЕсли формат запакован, то функция require синтезирует экспортированную функцию в модель и прикрепляет ее к окну. Так что мы можем получить его.

本身倒没有什么问题,只是我写的lifeCycle,对于应用状态依赖有点弱。 .比如说,第一次进入某个子应用需要fetch,后面就不应需要了。我的做法是通过函数缓存来实现,但是整个生命周期的执行没有丝毫变化,这样似乎不太好,不够优雅。

Здесь есть что добавить, мы надеемся, что клик пользователя сработаетwindow.history.pushStateсобытие, чтобы явно изменить URL-адрес адресной строки, но нам также нужно отслеживать pushSate, чтобы активировать функцию для переключения приложений. PushState нельзя отслеживать напрямую. Нам нужно обернуть событие window.history.pushState и отслеживать изменения истории, прослушивая пользовательские события. Ниже приведена функция реализации.

export function patchEventListener(event: any, ListerName: string) {
    return function (this: any) {
        const e = new Event(ListerName);
        event.apply(this, arguments)
        window.dispatchEvent(e);
    };
}
window.history.pushState = patchEventListener(window.history.pushState, "cosmos_pushState");
window.addEventListener("cosmos_pushState", routerChange);

связь приложения

Спрос определяет реализацию.

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

так вmicrocosmosЗдесь я просто использую простую публикацию и подписку для достижения. Да ладно, это слишком просто для тебя. (Не ругай, не ругай, просто используй

export function initCosmosStore(initData) {
    return window.MICROCOSMOS_ROOT_STORE = (function () {
        let store = initData;
        let observers: Array<Function> = [];
        function getStore() {
            return store;
        }
        function changeStore(newValue) {
            return new Promise((resolve, reject) => {
                if (newValue !== store) {
                    let oldValue = store;
                    store = newValue;
                    resolve(store);
                    observers.forEach(fn => fn(newValue, oldValue));
                }
            })
        }
        function subscribeStore(fn) {
            observers.push(fn);
        }
        return { getStore, changeStore, subscribeStore }
    })()
}

Предварительная загрузка

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

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

На этом техническая реализация микрокосмоса закончена, конечно, есть еще мелкие детали, и обо всем рассказать невозможно.

Суммировать

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

PS: одна из особенностей грядущего webpack5module federationПозволяет приложению JavaScript динамически загружать код из другого приложения JavaScript при совместном использовании зависимостей. Если федеративный модуль, используемый приложением, не имеет необходимых зависимостей в федеративном коде, Webpack загрузит отсутствующие зависимости из федеративного источника сборки.Webpack может лучше и проще поддерживать взаимную загрузку продуктов сборки между разными проектами, что позволяет Let’s посмотрите, что это в конечном итоге приведет к микро-фронтендам.

Цитировать

Категории