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