幾種微前端方案探究

前端 | 幾種微前端方案探究.png

前言

隨著技術(shù)的發(fā)展,前端應(yīng)用承載的內(nèi)容也日益復(fù)雜嚎卫,基于此而產(chǎn)生的各種問題也應(yīng)運(yùn)而生拴疤,從MPA(Multi-Page Application,多頁應(yīng)用)到SPA(Single-Page Application依鸥,單頁應(yīng)用),雖然解決了切換體驗(yàn)的延遲問題悼沈,但也帶來了首次加載時(shí)間長贱迟,以及工程爆炸增長后帶來的巨石應(yīng)用(Monolithic)問題姐扮;對(duì)于MPA來說,其部署簡單衣吠,各應(yīng)用之間天然硬隔離溶握,并且具備技術(shù)棧無關(guān)、獨(dú)立開發(fā)蒸播、獨(dú)立部署等特點(diǎn)睡榆。要是能夠?qū)⑦@兩方的特點(diǎn)結(jié)合起來,會(huì)不會(huì)給用戶和開發(fā)帶來更好的用戶體驗(yàn)袍榆?至此胀屿,在借鑒了微服務(wù)理念下,微前端便應(yīng)運(yùn)而生包雀。

An architectural style where independently deliverable frontend applications are composed into a greater whole. [Micro Frontends from martinfowler.com]

(https://martinfowler.com/articles/micro-frontends.html)

根據(jù)martinfowler對(duì)微前端的定義可以看出:微前端是一種由獨(dú)立交付的多個(gè)前端應(yīng)用組成整體的架構(gòu)風(fēng)格宿崭,即微前端和微服務(wù)一樣是一種架構(gòu)風(fēng)格,因而其并不是一種框架或者庫才写,而是一種風(fēng)格或者說是一種思想葡兑,所以為了實(shí)現(xiàn)微前端的方案就有很多種,最常見的方案有以下幾種:

  1. 路由分發(fā)
image
  1. iframe
  2. 應(yīng)用微服務(wù)
image
  1. 微件化
image
  1. 微應(yīng)用化
image
  1. Web Components
image

相關(guān)對(duì)比:

<colgroup><col span="1"><col span="1"><col span="1"><col span="1"><col span="1"><col span="1"><col span="1"><col span="1"></colgroup>
| 方案 | 開發(fā)成本 | 維護(hù)成本 | 可行性 | 同一框架要求 | 實(shí)現(xiàn)難度 | 潛在風(fēng)險(xiǎn) | 落地實(shí)踐 |
| 路由分發(fā) | 低 | 低 | 高 | 否 | easy | 無 | http服務(wù)器反向代理赞草,如nginx配置location |
| iframe | 低 | 低 | 高 | 否 | easy | seo不友好讹堤、cookie管理、通信機(jī)制厨疙、彈窗問題洲守、刷新后退、安全問題 | 前后端不分離項(xiàng)目常用 |
| 應(yīng)用微服務(wù) | 高 | 低 | 中 | 否 | hard | 共享及隔離粒度不統(tǒng)一 | qiankun沾凄、icestark梗醇、mooa及類single-spa應(yīng)用 |
| 微件化 | 高 | 中 | 低 | 是 | hard | 實(shí)現(xiàn)微件管理機(jī)制 | 無 |
| 微應(yīng)用化 | 中 | 中 | 高 | 是 | normal | 多個(gè)項(xiàng)目組合,需要考慮各個(gè)部署升級(jí)情況 | emp |
| Web Components | 高 | 低 | 高 | 否 | normal | 新api撒蟀,瀏覽器兼容性 | 無 |

對(duì)于微前端方案的選擇應(yīng)該從現(xiàn)有資源及歷史積淀中去選擇上述一種或幾種方案的組合叙谨,從不同維度(比如:共享能力、隔離機(jī)制保屯、數(shù)據(jù)方案手负、路由鑒權(quán)等)去考慮,實(shí)現(xiàn)工程的平滑遷移配椭,從而實(shí)現(xiàn)架構(gòu)的迭代升級(jí)逐步重構(gòu)虫溜,切忌為了架構(gòu)而架構(gòu)雹姊,不要無謂的炫技股缸,任何技術(shù)都是合適的才是最好的,大巧不工吱雏,重劍無鋒敦姻!

方案對(duì)比

這里重點(diǎn)分析以應(yīng)用微服務(wù)及微應(yīng)用化的幾種落地方案瘾境,對(duì)其實(shí)現(xiàn)思路做一個(gè)簡單的探究

  • single-spa
  • qiankun
  • icestark
  • emp
  • piral

源碼解析

single-spa源碼

image

single-spa的整體思路是通過生命周期的鉤子函數(shù)來對(duì)劫持的路由進(jìn)行應(yīng)用的加載,核心在于apps及reroute這兩個(gè)文件

apps.js

image

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background-image: none; background-size: auto; background-attachment: scroll; background-origin: padding-box; background-clip: border-box; background-color: rgba(0, 0, 0, 0); background-position: 0% 0%; background-repeat: repeat repeat;">export function getAppChanges() { const appsToUnload = [], appsToUnmount = [], appsToLoad = [], appsToMount = []; const currentTime = new Date().getTime(); apps.forEach((app) => { const appShouldBeActive = app.status !== SKIP_BECAUSE_BROKEN && shouldBeActive(app); switch (app.status) { case LOAD_ERROR: if (appShouldBeActive && currentTime - app.loadErrorTime >= 200) { appsToLoad.push(app); } break; case NOT_LOADED: case LOADING_SOURCE_CODE: if (appShouldBeActive) { appsToLoad.push(app); } break; case NOT_BOOTSTRAPPED: case NOT_MOUNTED: if (!appShouldBeActive && getAppUnloadInfo(toName(app))) { appsToUnload.push(app); } else if (appShouldBeActive) { appsToMount.push(app); } break; case MOUNTED: if (!appShouldBeActive) { appsToUnmount.push(app); } break; } }); return { appsToUnload, appsToUnmount, appsToLoad, appsToMount }; } export function getMountedApps() { return apps.filter(isActive).map(toName); } export function registerApplication( appNameOrConfig, appOrLoadApp, activeWhen, customProps ) { const registration = sanitizeArguments( appNameOrConfig, appOrLoadApp, activeWhen, customProps ); if (getAppNames().indexOf(registration.name) !== -1) throw Error( formatErrorMessage( 21, DEV && There is already an app registered with name ${registration.name}, registration.name ) ); apps.push( assign( { loadErrorTime: null, status: NOT_LOADED, parcels: {}, devtools: { overlays: { options: {}, selectors: [], }, }, }, registration ) ); if (isInBrowser) { ensureJQuerySupport(); reroute(); } } export function unregisterApplication(appName) { if (apps.filter((app) => toName(app) === appName).length === 0) { throw Error( formatErrorMessage( 25, DEV && Cannot unregister application '${appName}' because no such application has been registered, appName ) ); } return unloadApplication(appName).then(() => { const appIndex = apps.map(toName).indexOf(appName); apps.splice(appIndex, 1); }); } export function unloadApplication(appName, opts = { waitForUnmount: false }) { if (typeof appName !== "string") { throw Error( formatErrorMessage( 26, DEV && unloadApplication requires a string 'appName' ) ); } const app = find(apps, (App) => toName(App) === appName); if (!app) { throw Error( formatErrorMessage( 27, DEV && Could not unload application '${appName}' because no such application has been registered, appName ) ); } const appUnloadInfo = getAppUnloadInfo(toName(app)); if (opts && opts.waitForUnmount) { if (appUnloadInfo) { return appUnloadInfo.promise; } else { const promise = new Promise((resolve, reject) => { addAppToUnload(app, () => promise, resolve, reject); }); return promise; } } else { let resultPromise; if (appUnloadInfo) { resultPromise = appUnloadInfo.promise; immediatelyUnloadApp(app, appUnloadInfo.resolve, appUnloadInfo.reject); } else { resultPromise = new Promise((resolve, reject) => { addAppToUnload(app, () => resultPromise, resolve, reject); immediatelyUnloadApp(app, resolve, reject); }); } return resultPromise; } }</pre>

reroute.js

image

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background-image: none; background-size: auto; background-attachment: scroll; background-origin: padding-box; background-clip: border-box; background-color: rgba(0, 0, 0, 0); background-position: 0% 0%; background-repeat: repeat repeat;">export function reroute(pendingPromises = [], eventArguments) { if (appChangeUnderway) { return new Promise((resolve, reject) => { peopleWaitingOnAppChange.push({ resolve, reject, eventArguments, }); }); } const { appsToUnload, appsToUnmount, appsToLoad, appsToMount, } = getAppChanges(); let appsThatChanged, navigationIsCanceled = false, oldUrl = currentUrl, newUrl = (currentUrl = window.location.href); if (isStarted()) { appChangeUnderway = true; appsThatChanged = appsToUnload.concat( appsToLoad, appsToUnmount, appsToMount ); return performAppChanges(); } else { appsThatChanged = appsToLoad; return loadApps(); } function cancelNavigation() { navigationIsCanceled = true; } function loadApps() { return Promise.resolve().then(() => { const loadPromises = appsToLoad.map(toLoadPromise); return ( Promise.all(loadPromises) .then(callAllEventListeners) .then(() => []) .catch((err) => { callAllEventListeners(); throw err; }) ); }); } function performAppChanges() { return Promise.resolve().then(() => { window.dispatchEvent( new CustomEvent( appsThatChanged.length === 0 ? "single-spa:before-no-app-change" : "single-spa:before-app-change", getCustomEventDetail(true) ) ); window.dispatchEvent( new CustomEvent( "single-spa:before-routing-event", getCustomEventDetail(true, { cancelNavigation }) ) ); if (navigationIsCanceled) { window.dispatchEvent( new CustomEvent( "single-spa:before-mount-routing-event", getCustomEventDetail(true) ) ); finishUpAndReturn(); navigateToUrl(oldUrl); return; } const unloadPromises = appsToUnload.map(toUnloadPromise); const unmountUnloadPromises = appsToUnmount .map(toUnmountPromise) .map((unmountPromise) => unmountPromise.then(toUnloadPromise)); const allUnmountPromises = unmountUnloadPromises.concat(unloadPromises); const unmountAllPromise = Promise.all(allUnmountPromises); unmountAllPromise.then(() => { window.dispatchEvent( new CustomEvent( "single-spa:before-mount-routing-event", getCustomEventDetail(true) ) ); }); const loadThenMountPromises = appsToLoad.map((app) => { return toLoadPromise(app).then((app) => tryToBootstrapAndMount(app, unmountAllPromise) ); }); const mountPromises = appsToMount .filter((appToMount) => appsToLoad.indexOf(appToMount) < 0) .map((appToMount) => { return tryToBootstrapAndMount(appToMount, unmountAllPromise); }); return unmountAllPromise .catch((err) => { callAllEventListeners(); throw err; }) .then(() => { callAllEventListeners(); return Promise.all(loadThenMountPromises.concat(mountPromises)) .catch((err) => { pendingPromises.forEach((promise) => promise.reject(err)); throw err; }) .then(finishUpAndReturn); }); }); } function finishUpAndReturn() { const returnValue = getMountedApps(); pendingPromises.forEach((promise) => promise.resolve(returnValue)); try { const appChangeEventName = appsThatChanged.length === 0 ? "single-spa:no-app-change" : "single-spa:app-change"; window.dispatchEvent( new CustomEvent(appChangeEventName, getCustomEventDetail()) ); window.dispatchEvent( new CustomEvent("single-spa:routing-event", getCustomEventDetail()) ); } catch (err) { setTimeout(() => { throw err; }); } appChangeUnderway = false; if (peopleWaitingOnAppChange.length > 0) { const nextPendingPromises = peopleWaitingOnAppChange; peopleWaitingOnAppChange = []; reroute(nextPendingPromises); } return returnValue; } function callAllEventListeners() { pendingPromises.forEach((pendingPromise) => { callCapturedEventListeners(pendingPromise.eventArguments); }); callCapturedEventListeners(eventArguments); } function getCustomEventDetail(isBeforeChanges = false, extraProperties) { const newAppStatuses = {}; const appsByNewStatus = { [MOUNTED]: [], [NOT_MOUNTED]: [], [NOT_LOADED]: [], [SKIP_BECAUSE_BROKEN]: [], }; if (isBeforeChanges) { appsToLoad.concat(appsToMount).forEach((app, index) => { addApp(app, MOUNTED); }); appsToUnload.forEach((app) => { addApp(app, NOT_LOADED); }); appsToUnmount.forEach((app) => { addApp(app, NOT_MOUNTED); }); } else { appsThatChanged.forEach((app) => { addApp(app); }); } const result = { detail: { newAppStatuses, appsByNewStatus, totalAppChanges: appsThatChanged.length, originalEvent: eventArguments?.[0], oldUrl, newUrl, navigationIsCanceled, }, }; if (extraProperties) { assign(result.detail, extraProperties); } return result; function addApp(app, status) { const appName = toName(app); status = status || getAppStatus(appName); newAppStatuses[appName] = status; const statusArr = (appsByNewStatus[status] = appsByNewStatus[status] || []); statusArr.push(appName); } } }</pre>

qiankun源碼

image

qiankun是基于single-spa而封裝了隔離及共享機(jī)制的框架镰惦,其簡化了single-spa的相關(guān)生命周期迷守,并且提供了沙箱隔離機(jī)制及共享機(jī)制

sandbox

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background-image: none; background-size: auto; background-attachment: scroll; background-origin: padding-box; background-clip: border-box; background-color: rgba(0, 0, 0, 0); background-position: 0% 0%; background-repeat: repeat repeat;">export type SandBox = { /** 沙箱的名字 / name: string; /* 沙箱的類型 / type: SandBoxType; /* 沙箱導(dǎo)出的代理實(shí)體 / proxy: WindowProxy; /* 沙箱是否在運(yùn)行中 / sandboxRunning: boolean; /* latest set property / latestSetProp?: PropertyKey | null; /* 啟動(dòng)沙箱 / active: () => void; /* 關(guān)閉沙箱 / inactive: () => void; }; // Proxy沙箱 export default class ProxySandbox implements SandBox { /* window 值變更記錄 / private updatedValueSet = new Set<PropertyKey>(); name: string; type: SandBoxType; proxy: WindowProxy; sandboxRunning = true; latestSetProp: PropertyKey | null = null; active() { if (!this.sandboxRunning) activeSandboxCount++; this.sandboxRunning = true; } inactive() { if (process.env.NODE_ENV === 'development') { console.info([qiankun:sandbox] ${this.name} modified global properties restore..., [ ...this.updatedValueSet.keys(), ]); } if (--activeSandboxCount === 0) { variableWhiteList.forEach((p) => { if (this.proxy.hasOwnProperty(p)) { // @ts-ignore delete window[p]; } }); } this.sandboxRunning = false; } constructor(name: string) { this.name = name; this.type = SandBoxType.Proxy; const { updatedValueSet } = this; const rawWindow = window; const { fakeWindow, propertiesWithGetter } = createFakeWindow(rawWindow); const descriptorTargetMap = new Map<PropertyKey, SymbolTarget>(); const hasOwnProperty = (key: PropertyKey) => fakeWindow.hasOwnProperty(key) || rawWindow.hasOwnProperty(key); const proxy = new Proxy(fakeWindow, { set: (target: FakeWindow, p: PropertyKey, value: any): boolean => { if (this.sandboxRunning) { // We must kept its description while the property existed in rawWindow before if (!target.hasOwnProperty(p) && rawWindow.hasOwnProperty(p)) { const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p); const { writable, configurable, enumerable } = descriptor!; if (writable) { Object.defineProperty(target, p, { configurable, enumerable, writable, value, }); } } else { // @ts-ignore target[p] = value; } if (variableWhiteList.indexOf(p) !== -1) { // @ts-ignore rawWindow[p] = value; } updatedValueSet.add(p); this.latestSetProp = p; return true; } if (process.env.NODE_ENV === 'development') { console.warn([qiankun] Set window.${p.toString()} while sandbox destroyed or inactive in ${name}!); } // 在 strict-mode 下,Proxy 的 handler.set 返回 false 會(huì)拋出 TypeError旺入,在沙箱卸載的情況下應(yīng)該忽略錯(cuò)誤 return true; }, get(target: FakeWindow, p: PropertyKey): any { if (p === Symbol.unscopables) return unscopables; // avoid who using window.window or window.self to escape the sandbox environment to touch the really window // see https://github.com/eligrey/FileSaver.js/blob/master/src/FileSaver.js#L13 if (p === 'window' || p === 'self') { return proxy; } if ( p === 'top' || p === 'parent' || (process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop')) ) { // if your master app in an iframe context, allow these props escape the sandbox if (rawWindow === rawWindow.parent) { return proxy; } return (rawWindow as any)[p]; } // proxy.hasOwnProperty would invoke getter firstly, then its value represented as rawWindow.hasOwnProperty if (p === 'hasOwnProperty') { return hasOwnProperty; } // mark the symbol to document while accessing as document.createElement could know is invoked by which sandbox for dynamic append patcher if (p === 'document' || p === 'eval') { setCurrentRunningSandboxProxy(proxy); // FIXME if you have any other good ideas // remove the mark in next tick, thus we can identify whether it in micro app or not // this approach is just a workaround, it could not cover all complex cases, such as the micro app runs in the same task context with master in some case nextTick(() => setCurrentRunningSandboxProxy(null)); switch (p) { case 'document': return document; case 'eval': // eslint-disable-next-line no-eval return eval; // no default } } // eslint-disable-next-line no-nested-ternary const value = propertiesWithGetter.has(p) ? (rawWindow as any)[p] : p in target ? (target as any)[p] : (rawWindow as any)[p]; return getTargetValue(rawWindow, value); }, // trap in operator // see https://github.com/styled-components/styled-components/blob/master/packages/styled-components/src/constants.js#L12 has(target: FakeWindow, p: string | number | symbol): boolean { return p in unscopables || p in target || p in rawWindow; }, getOwnPropertyDescriptor(target: FakeWindow, p: string | number | symbol): PropertyDescriptor | undefined { / as the descriptor of top/self/window/mockTop in raw window are configurable but not in proxy target, we need to get it from target to avoid TypeError see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/getOwnPropertyDescriptor > A property cannot be reported as non-configurable, if it does not exists as an own property of the target object or if it exists as a configurable own property of the target object. / if (target.hasOwnProperty(p)) { const descriptor = Object.getOwnPropertyDescriptor(target, p); descriptorTargetMap.set(p, 'target'); return descriptor; } if (rawWindow.hasOwnProperty(p)) { const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p); descriptorTargetMap.set(p, 'rawWindow'); // A property cannot be reported as non-configurable, if it does not exists as an own property of the target object if (descriptor && !descriptor.configurable) { descriptor.configurable = true; } return descriptor; } return undefined; }, // trap to support iterator with sandbox ownKeys(target: FakeWindow): PropertyKey[] { const keys = uniq(Reflect.ownKeys(rawWindow).concat(Reflect.ownKeys(target))); return keys; }, defineProperty(target: Window, p: PropertyKey, attributes: PropertyDescriptor): boolean { const from = descriptorTargetMap.get(p); / Descriptor must be defined to native window while it comes from native window via Object.getOwnPropertyDescriptor(window, p), otherwise it would cause a TypeError with illegal invocation. */ switch (from) { case 'rawWindow': return Reflect.defineProperty(rawWindow, p, attributes); default: return Reflect.defineProperty(target, p, attributes); } }, deleteProperty(target: FakeWindow, p: string | number | symbol): boolean { if (target.hasOwnProperty(p)) { // @ts-ignore delete target[p]; updatedValueSet.delete(p); return true; } return true; }, }); this.proxy = proxy; activeSandboxCount++; } } // 快照snapshot沙箱 export default class SnapshotSandbox implements SandBox { proxy: WindowProxy; name: string; type: SandBoxType; sandboxRunning = true; private windowSnapshot!: Window; private modifyPropsMap: Record<any, any> = {}; constructor(name: string) { this.name = name; this.proxy = window; this.type = SandBoxType.Snapshot; } active() { // 記錄當(dāng)前快照 this.windowSnapshot = {} as Window; iter(window, (prop) => { this.windowSnapshot[prop] = window[prop]; }); // 恢復(fù)之前的變更 Object.keys(this.modifyPropsMap).forEach((p: any) => { window[p] = this.modifyPropsMap[p]; }); this.sandboxRunning = true; } inactive() { this.modifyPropsMap = {}; iter(window, (prop) => { if (window[prop] !== this.windowSnapshot[prop]) { // 記錄變更兑凿,恢復(fù)環(huán)境 this.modifyPropsMap[prop] = window[prop]; window[prop] = this.windowSnapshot[prop]; } }); if (process.env.NODE_ENV === 'development') { console.info([qiankun:sandbox] ${this.name} origin window restore..., Object.keys(this.modifyPropsMap)); } this.sandboxRunning = false; } }</pre>

globalState.js

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background-image: none; background-size: auto; background-attachment: scroll; background-origin: padding-box; background-clip: border-box; background-color: rgba(0, 0, 0, 0); background-position: 0% 0%; background-repeat: repeat repeat;">// 觸發(fā)全局監(jiān)聽 function emitGlobal(state: Record<string, any>, prevState: Record<string, any>) { Object.keys(deps).forEach((id: string) => { if (deps[id] instanceof Function) { deps[id](cloneDeep(state), cloneDeep(prevState)); } }); } export function initGlobalState(state: Record<string, any> = {}) { if (state === globalState) { console.warn('[qiankun] state has not changed越走!'); } else { const prevGlobalState = cloneDeep(globalState); globalState = cloneDeep(state); emitGlobal(globalState, prevGlobalState); } return getMicroAppStateActions(global-${+new Date()}, true); } export function getMicroAppStateActions(id: string, isMaster?: boolean): MicroAppStateActions { return { /** * onGlobalStateChange 全局依賴監(jiān)聽 * * 收集 setState 時(shí)所需要觸發(fā)的依賴 * * 限制條件:每個(gè)子應(yīng)用只有一個(gè)激活狀態(tài)的全局監(jiān)聽庐氮,新監(jiān)聽覆蓋舊監(jiān)聽视哑,若只是監(jiān)聽部分屬性微猖,請使用 onGlobalStateChange * * 這么設(shè)計(jì)是為了減少全局監(jiān)聽濫用導(dǎo)致的內(nèi)存爆炸 * * 依賴數(shù)據(jù)結(jié)構(gòu)為: * { * {id}: callback * } * * @param callback * @param fireImmediately / onGlobalStateChange(callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) { if (!(callback instanceof Function)) { console.error('[qiankun] callback must be function!'); return; } if (deps[id]) { console.warn([qiankun] '${id}' global listener already exists before this, new listener will overwrite it.); } deps[id] = callback; const cloneState = cloneDeep(globalState); if (fireImmediately) { callback(cloneState, cloneState); } }, /* * setGlobalState 更新 store 數(shù)據(jù) * * 1. 對(duì)輸入 state 的第一層屬性做校驗(yàn)囤锉,只有初始化時(shí)聲明過的第一層(bucket)屬性才會(huì)被更改 * 2. 修改 store 并觸發(fā)全局監(jiān)聽 * * @param state */ setGlobalState(state: Record<string, any> = {}) { if (state === globalState) { console.warn('[qiankun] state has not changed芝囤!'); return false; } const changeKeys: string[] = []; const prevGlobalState = cloneDeep(globalState); globalState = cloneDeep( Object.keys(state).reduce((_globalState, changeKey) => { if (isMaster || _globalState.hasOwnProperty(changeKey)) { changeKeys.push(changeKey); return Object.assign(_globalState, { [changeKey]: state[changeKey] }); } console.warn([qiankun] '${changeKey}' not declared when init state秕脓!); return _globalState; }, globalState), ); if (changeKeys.length === 0) { console.warn('[qiankun] state has not changed灌灾!'); return false; } emitGlobal(globalState, prevGlobalState); return true; }, // 注銷該應(yīng)用下的依賴 offGlobalStateChange() { delete deps[id]; return true; }, }; }</pre>

icestark源碼

image

ice是淘系團(tuán)隊(duì)的一個(gè)全流程前端框架雕旨,其中包含了腳手架扮匠、組件庫、vscode插件凡涩、lowcode生成等相關(guān)生態(tài)棒搜,其中icestark是其中包含的相關(guān)微前端的應(yīng)用,這里不展開ice相關(guān)的架構(gòu)了活箕,簡單來說其本質(zhì)是以微內(nèi)核形態(tài)配合各種插件市場來構(gòu)成的強(qiáng)大生態(tài)系統(tǒng)帮非,本文重點(diǎn)在微前端,因而只討論微前端相關(guān)內(nèi)容

apps.ts

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background-image: none; background-size: auto; background-attachment: scroll; background-origin: padding-box; background-clip: border-box; background-color: rgba(0, 0, 0, 0); background-position: 0% 0%; background-repeat: repeat repeat;">export function registerMicroApp(appConfig: AppConfig, appLifecyle?: AppLifecylceOptions) { // check appConfig.name if (getAppNames().includes(appConfig.name)) { throw Error(name ${appConfig.name} already been regsitered); } // set activeRules const { activePath, hashType = false, exact = false, sensitive = false, strict = false } = appConfig; const activeRules: (ActiveFn | string | MatchOptions)[] = Array.isArray(activePath) ? activePath : [activePath]; const checkActive = activePath ? (url: string) => activeRules.map((activeRule: ActiveFn | string | MatchOptions) => { if (typeof activeRule === 'function' ) { return activeRule; } else { const pathOptions: MatchOptions = { hashType, exact, sensitive, strict }; const pathInfo = Object.prototype.toString.call(activeRule) === '[object Object]' ? { ...pathOptions, ...(activeRule as MatchOptions) } : { path: activeRule as string, ...pathOptions }; return (checkUrl: string) => matchActivePath(checkUrl, pathInfo); } }).some((activeRule: ActiveFn) => activeRule(url)) // active app when activePath is not specified : () => true; const microApp = { status: NOT_LOADED, ...appConfig, appLifecycle: appLifecyle, checkActive, }; microApps.push(microApp); } export function registerMicroApps(appConfigs: AppConfig[], appLifecyle?: AppLifecylceOptions) { appConfigs.forEach(appConfig => { registerMicroApp(appConfig, appLifecyle); }); } // 可以加載module粒度的應(yīng)用 export async function loadAppModule(appConfig: AppConfig) { const { onLoadingApp, onFinishLoading, fetch } = getAppConfig(appConfig.name)?.configuration || globalConfiguration; let lifecycle: ModuleLifeCycle = {}; onLoadingApp(appConfig); const appSandbox = createSandbox(appConfig.sandbox); const { url, container, entry, entryContent, name } = appConfig; const appAssets = url ? getUrlAssets(url) : await getEntryAssets({ root: container, entry, href: location.href, entryContent, assetsCacheKey: name, fetch, }); updateAppConfig(appConfig.name, { appAssets, appSandbox }); cacheLoadMode(appConfig); if (appConfig.umd) { await loadAndAppendCssAssets(appAssets); lifecycle = await loadUmdModule(appAssets.jsList, appSandbox); } else { await appendAssets(appAssets, appSandbox, fetch); lifecycle = { mount: getCache(AppLifeCycleEnum.AppEnter), unmount: getCache(AppLifeCycleEnum.AppLeave), }; setCache(AppLifeCycleEnum.AppEnter, null); setCache(AppLifeCycleEnum.AppLeave, null); } onFinishLoading(appConfig); return combineLifecyle(lifecycle, appConfig); } function capitalize(str: string) { if (typeof str !== 'string') return ''; return ${str.charAt(0).toUpperCase()}${str.slice(1)}; } async function callAppLifecycle(primaryKey: string, lifecycleKey: string, appConfig: AppConfig) { if (appConfig.appLifecycle && appConfig.appLifecycle[${primaryKey}${capitalize(lifecycleKey)}]) { await appConfig.appLifecycle${primaryKey}${capitalize(lifecycleKey)}; } } function combineLifecyle(lifecycle: ModuleLifeCycle, appConfig: AppConfig) { const combinedLifecyle = { ...lifecycle }; ['mount', 'unmount', 'update'].forEach((lifecycleKey) => { if (lifecycle[lifecycleKey]) { combinedLifecyle[lifecycleKey] = async (props) => { await callAppLifecycle('before', lifecycleKey, appConfig); await lifecyclelifecycleKey; await callAppLifecycle('after', lifecycleKey, appConfig); }; } }); return combinedLifecyle; } export async function mountMicroApp(appName: string) { const appConfig = getAppConfig(appName); // check current url before mount if (appConfig && appConfig.checkActive(window.location.href) && appConfig.status !== MOUNTED) { updateAppConfig(appName, { status: MOUNTED }); if (appConfig.mount) { await appConfig.mount({ container: appConfig.container, customProps: appConfig.props }); } } } export async function unmountMicroApp(appName: string) { const appConfig = getAppConfig(appName); if (appConfig && (appConfig.status === MOUNTED || appConfig.status === LOADING_ASSETS || appConfig.status === NOT_MOUNTED)) { // remove assets if app is not cached const { shouldAssetsRemove } = getAppConfig(appName)?.configuration || globalConfiguration; emptyAssets(shouldAssetsRemove, !appConfig.cached && appConfig.name); updateAppConfig(appName, { status: UNMOUNTED }); if (!appConfig.cached && appConfig.appSandbox) { appConfig.appSandbox.clear(); appConfig.appSandbox = null; } if (appConfig.unmount) { await appConfig.unmount({ container: appConfig.container, customProps: appConfig.props }); } } } // unload micro app, load app bundles when create micro app export async function unloadMicroApp(appName: string) { const appConfig = getAppConfig(appName); if (appConfig) { unmountMicroApp(appName); delete appConfig.mount; delete appConfig.unmount; delete appConfig.appAssets; updateAppConfig(appName, { status: NOT_LOADED }); } else { console.log([icestark] can not find app ${appName} when call unloadMicroApp); } } // remove app config from cache export function removeMicroApp(appName: string) { const appIndex = getAppNames().indexOf(appName); if (appIndex > -1) { // unload micro app in case of app is mounted unloadMicroApp(appName); microApps.splice(appIndex, 1); } else { console.log([icestark] can not find app ${appName} when call removeMicroApp); } } export function removeMicroApps(appNames: string[]) { appNames.forEach((appName) => { removeMicroApp(appName); }); } // clear all micro app configs export function clearMicroApps () { getAppNames().forEach(name => { unloadMicroApp(name); }); microApps = []; }</pre>

start.ts

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background-image: none; background-size: auto; background-attachment: scroll; background-origin: padding-box; background-clip: border-box; background-color: rgba(0, 0, 0, 0); background-position: 0% 0%; background-repeat: repeat repeat;">export function reroute (url: string, type: RouteType | 'init' | 'popstate'| 'hashchange' ) { const { pathname, query, hash } = urlParse(url, true); // trigger onRouteChange when url is changed if (lastUrl !== url) { globalConfiguration.onRouteChange(url, pathname, query, hash, type); const unmountApps = []; const activeApps = []; getMicroApps().forEach((microApp: AppConfig) => { const shouldBeActive = microApp.checkActive(url); if (shouldBeActive) { activeApps.push(microApp); } else { unmountApps.push(microApp); } }); // trigger onActiveApps when url is changed globalConfiguration.onActiveApps(activeApps); // call captured event after app mounted Promise.all( // call unmount apps unmountApps.map(async (unmountApp) => { if (unmountApp.status === MOUNTED || unmountApp.status === LOADING_ASSETS) { globalConfiguration.onAppLeave(unmountApp); } await unmountMicroApp(unmountApp.name); }).concat(activeApps.map(async (activeApp) => { if (activeApp.status !== MOUNTED) { globalConfiguration.onAppEnter(activeApp); } await createMicroApp(activeApp); })) ).then(() => { callCapturedEventListeners(); }); } lastUrl = url; }; function start(options?: StartConfiguration) { if (started) { console.log('icestark has been already started'); return; } started = true; recordAssets(); // update globalConfiguration Object.keys(options || {}).forEach((configKey) => { globalConfiguration[configKey] = options[configKey]; }); hijackHistory(); hijackEventListener(); // trigger init router globalConfiguration.reroute(location.href, 'init'); } function unload() { unHijackEventListener(); unHijackHistory(); started = false; // remove all assets added by micro apps emptyAssets(globalConfiguration.shouldAssetsRemove, true); clearMicroApps(); }</pre>

emp源碼

image
image

emp實(shí)現(xiàn)方式完全不同于類single-spa的方案讹蘑,其是利用的webpack5的模塊聯(lián)邦機(jī)制末盔,實(shí)現(xiàn)模塊與模塊之間的共享調(diào)用,YY的大佬們基于ts的xxx.d.ts的共享傳遞座慰,實(shí)現(xiàn)了類似微服務(wù)的service mesh的功能陨舱,emp提供了完整的腳手架功能

image

emp-cli

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background-image: none; background-size: auto; background-attachment: scroll; background-origin: padding-box; background-clip: border-box; background-color: rgba(0, 0, 0, 0); background-position: 0% 0%; background-repeat: repeat repeat;">const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin'); module.exports = (env, config, {analyze, empEnv, ts, progress, createName, createPath, hot}) => { const isDev = env === 'development' const conf = { plugin: { mf: { plugin: ModuleFederationPlugin, args: [{}], }, }, } if (ts) { createName = createName || 'index.d.ts' createPath = createPath ? resolveApp(createPath) : resolveApp('dist') conf.plugin.tunedts = { plugin: TuneDtsPlugin, args: [ { output: path.join(createPath, createName), path: createPath, name: createName, isDefault: true, }, ], } } config.merge(conf) }</pre>

emp-tune-dts-plugin

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background-image: none; background-size: auto; background-attachment: scroll; background-origin: padding-box; background-clip: border-box; background-color: rgba(0, 0, 0, 0); background-position: 0% 0%; background-repeat: repeat repeat;">function tuneType(createPath, createName, isDefault, operation = emptyFunc) { // 獲取 d.ts 文件 const filePath = ${createPath}/${createName} const fileData = fs.readFileSync(filePath, {encoding: 'utf-8'}) let newFileData = '' newFileData = fileData isDefault && (newFileData = defaultRepalce(fileData)) // } newFileData && (newFileData = operation(newFileData) ? operation(newFileData) : newFileData) // 覆蓋原有 index.d.ts fs.writeFileSync(filePath, newFileData, {encoding: 'utf-8'}) } class TuneDtsPlugin { constructor(options) { this.options = options || {} } apply(compiler) { const _options = this.options console.log('------------TuneDtsPlugin Working----------') if (compiler.options.output.path) { _options.path = compiler.options.output.path _options.output = ${compiler.options.output.path}/${_options.name} } compiler.hooks.afterEmit.tap(plugin, function () { setTimeout(function () { generateType(_options) }) }) } }</pre>

piral源碼

image

piral是一個(gè)基于react的微前端框架,其定義了兩個(gè)概念:一個(gè)是Piral版仔,這是給一個(gè)應(yīng)用的殼子(application shell)游盲,其承載著各種應(yīng)用,當(dāng)然也包括由pilets構(gòu)建共享的組件蛮粮,定義這些應(yīng)用何時(shí)被加載以及何時(shí)被集成益缎;另一個(gè)是Pilet,這是一個(gè)特殊的模塊(feature modules)然想,其承載著不同的一應(yīng)用莺奔,包含著獨(dú)立的資源,其決定了組件的加載時(shí)機(jī)变泄。Piral通過對(duì)加了一層pilets而進(jìn)行資源的隔離令哟,對(duì)沒有加這一層的則可進(jìn)行數(shù)據(jù)的共享

piral

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background-image: none; background-size: auto; background-attachment: scroll; background-origin: padding-box; background-clip: border-box; background-color: rgba(0, 0, 0, 0); background-position: 0% 0%; background-repeat: repeat repeat;">// render.tsx export function renderInstance(options?: PiralRenderOptions) { return runInstance((app, selector) => render(app, getContainer(selector)), options); } // run.tsx export function runInstance(runner: PiralRunner, options: PiralRenderOptions = {}) { const { selector = '#app', settings, layout, errors, middleware = noChange, ...config } = options; const { app, instance } = getAppInstance(middleware(config), { settings, layout, errors }); runner(app, selector); return instance; }</pre>

piral-base

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background-image: none; background-size: auto; background-attachment: scroll; background-origin: padding-box; background-clip: border-box; background-color: rgba(0, 0, 0, 0); background-position: 0% 0%; background-repeat: repeat repeat;">// dependency.ts export function compileDependency( name: string, content: string, link = '', dependencies?: AvailableDependencies, ): Promise<PiletApp> { const app = evalDependency(name, content, link, dependencies); return checkPiletAppAsync(name, app); } // fetch.ts export function defaultFetchDependency(url: string) { return fetch(url, { method: 'GET', cache: 'force-cache', }).then((m) => m.text()); }</pre>

piral-core

<pre class="cm-s-default" style="color: rgb(89, 89, 89); margin: 0px; padding: 0px; background-image: none; background-size: auto; background-attachment: scroll; background-origin: padding-box; background-clip: border-box; background-color: rgba(0, 0, 0, 0); background-position: 0% 0%; background-repeat: repeat repeat;">// actions export function initialize(ctx: GlobalStateContext, loading: boolean, error: Error | undefined, modules: Array<Pilet>) { ctx.dispatch((state) => ({ ...state, app: { ...state.app, error, loading, }, modules, })); } export function injectPilet(ctx: GlobalStateContext, pilet: Pilet) { ctx.dispatch((state) => ({ ...state, modules: replaceOrAddItem(state.modules, pilet, (m) => m.name === pilet.name), registry: removeNested<RegistryState, BaseRegistration>(state.registry, (m) => m.pilet === pilet.name), })); ctx.emit('unload-pilet', { name: pilet.name, }); } export function setComponent<TKey extends keyof ComponentsState>( ctx: GlobalStateContext, name: TKey, component: ComponentsState[TKey], ) { ctx.dispatch((state) => ({ ...state, components: withKey(state.components, name, component), })); } export function setErrorComponent<TKey extends keyof ErrorComponentsState>( ctx: GlobalStateContext, type: TKey, component: ErrorComponentsState[TKey], ) { ctx.dispatch((state) => ({ ...state, errorComponents: withKey(state.errorComponents, type, component), })); } export function setRoute<T = {}>( ctx: GlobalStateContext, path: string, component: ComponentType<RouteComponentProps<T>>, ) { ctx.dispatch((state) => ({ ...state, routes: withKey(state.routes, path, component), })); } export function includeProvider(ctx: GlobalStateContext, provider: JSX.Element) { const wrapper: React.FC = (props) => cloneElement(provider, props); ctx.dispatch((state) => ({ ...state, provider: !state.provider ? wrapper : (props) => createElement(state.provider, undefined, wrapper(props)), })); } // createGlobalState.ts export function createGlobalState(customState: NestedPartial<GlobalState> = {}) { const defaultState: GlobalState = { app: { error: undefined, loading: typeof window !== 'undefined', layout: 'desktop', }, components: { ErrorInfo: DefaultErrorInfo, LoadingIndicator: DefaultLoadingIndicator, Router: BrowserRouter, Layout: DefaultLayout, }, errorComponents: {}, registry: { extensions: {}, pages: {}, wrappers: {}, }, routes: {}, data: {}, portals: {}, modules: [], }; return Atom.of(extend(defaultState, customState)); }</pre>

總結(jié)

微前端落地實(shí)踐方案很多恼琼,想了解更多框架的同學(xué),可以看這篇文章2020 非称粮唬火的 11 個(gè)微前端框架晴竞。微前端的本質(zhì)在于資源的隔離與共享,這里的顆粒度既可以是應(yīng)用狠半,也可以是模塊噩死,或者是自己定義的抽象層,這些都是為了更好的“高內(nèi)聚神年,低耦合”甜滨。正如“軟件工程中沒有銀彈”所說的那樣,不存在一種通式通解能夠一下解決所有問題瘤袖,只有結(jié)合具體業(yè)務(wù)衣摩,選擇合適的技術(shù)方案,才能最大限度的發(fā)揮架構(gòu)的作用捂敌,切勿為了微而微艾扮!

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市占婉,隨后出現(xiàn)的幾起案子泡嘴,更是在濱河造成了極大的恐慌,老刑警劉巖逆济,帶你破解...
    沈念sama閱讀 217,657評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件酌予,死亡現(xiàn)場離奇詭異,居然都是意外死亡奖慌,警方通過查閱死者的電腦和手機(jī)抛虫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來简僧,“玉大人建椰,你說我怎么就攤上這事〉郝恚” “怎么了棉姐?”我有些...
    開封第一講書人閱讀 164,057評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長啦逆。 經(jīng)常有香客問我伞矩,道長,這世上最難降的妖魔是什么夏志? 我笑而不...
    開封第一講書人閱讀 58,509評(píng)論 1 293
  • 正文 為了忘掉前任乃坤,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘侥袜。我一直安慰自己,他們只是感情好溉贿,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評(píng)論 6 392
  • 文/花漫 我一把揭開白布枫吧。 她就那樣靜靜地躺著,像睡著了一般宇色。 火紅的嫁衣襯著肌膚如雪九杂。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,443評(píng)論 1 302
  • 那天宣蠕,我揣著相機(jī)與錄音例隆,去河邊找鬼。 笑死抢蚀,一個(gè)胖子當(dāng)著我的面吹牛镀层,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播皿曲,決...
    沈念sama閱讀 40,251評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼唱逢,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了屋休?” 一聲冷哼從身側(cè)響起坞古,我...
    開封第一講書人閱讀 39,129評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎劫樟,沒想到半個(gè)月后痪枫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,561評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡叠艳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評(píng)論 3 335
  • 正文 我和宋清朗相戀三年奶陈,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片附较。...
    茶點(diǎn)故事閱讀 39,902評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡尿瞭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出翅睛,到底是詐尸還是另有隱情声搁,我是刑警寧澤,帶...
    沈念sama閱讀 35,621評(píng)論 5 345
  • 正文 年R本政府宣布捕发,位于F島的核電站疏旨,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏扎酷。R本人自食惡果不足惜檐涝,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧谁榜,春花似錦幅聘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至巷怜,卻和暖如春葛超,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背延塑。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評(píng)論 1 269
  • 我被黑心中介騙來泰國打工绣张, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人关带。 一個(gè)月前我還...
    沈念sama閱讀 48,025評(píng)論 2 370
  • 正文 我出身青樓侥涵,卻偏偏與公主長得像,于是被迫代替她去往敵國和親宋雏。 傳聞我的和親對(duì)象是個(gè)殘疾皇子独令,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 前言 拓?fù)鋱D是數(shù)據(jù)可視化領(lǐng)域一種比較常見的展示類型,目前業(yè)界常見的可視化展現(xiàn)的方案有ECharts好芭、HighCha...
    維李設(shè)論閱讀 1,177評(píng)論 0 0
  • 前端 | flexiwan項(xiàng)目踩坑實(shí)踐.png 項(xiàng)目背景 flexiManage是以色列一家初創(chuàng)公司flexiWA...
    維李設(shè)論閱讀 969評(píng)論 0 0
  • 微前端工程之間的通訊 原理 使用發(fā)布訂閱者模式:一方訂閱燃箭,一方發(fā)布。 使用單例模式:一個(gè)工程內(nèi)使用同一個(gè)實(shí)例舍败。 微...
    Yong_bcf4閱讀 1,184評(píng)論 0 0
  • 久違的晴天招狸,家長會(huì)。 家長大會(huì)開好到教室時(shí)邻薯,離放學(xué)已經(jīng)沒多少時(shí)間了裙戏。班主任說已經(jīng)安排了三個(gè)家長分享經(jīng)驗(yàn)。 放學(xué)鈴聲...
    飄雪兒5閱讀 7,523評(píng)論 16 22
  • 今天感恩節(jié)哎厕诡,感謝一直在我身邊的親朋好友累榜。感恩相遇!感恩不離不棄灵嫌。 中午開了第一次的黨會(huì)壹罚,身份的轉(zhuǎn)變要...
    迷月閃星情閱讀 10,564評(píng)論 0 11