微前端,關於他的好和應用場景,不少師兄們也都介紹過了,那麼咱們使用的微前端方案qiankun是如何去作到應用的「微前端」的呢?html
說到前端微服務,確定不能不提他的幾個特性。前端
預加載ios
這篇分享,就會簡單的讀一讀qiankun 的源碼,從大概流程上,瞭解他的實現原理和技術方案。git
Arya- 公司的前端平臺微服務基座github
Arya接入了權限平臺的路由菜單和權限,能夠動態挑選具備微服務能力的子應用的指定頁面組合成一個新的平臺,方便各個系統權限的下發和功能的匯聚。bootstrap
/src/apis.tsapi
export function start(opts: FrameworkConfiguration = {}) { // 默認值設置 frameworkConfiguration = { prefetch: true, singular: true, sandbox: true, ...opts }; const { prefetch, sandbox, singular, urlRerouteOnly, ...importEntryOpts } = frameworkConfiguration; // 檢查 prefetch 屬性,若是須要預加載,則添加全局事件 single-spa:first-mount 監聽,在第一個子應用掛載後預加載其餘子應用資源,優化後續其餘子應用的加載速度。 if (prefetch) { doPrefetchStrategy(microApps, prefetch, importEntryOpts); } // 參數設置是否啓用沙箱運行環境,隔離 if (sandbox) { if (!window.Proxy) { console.warn('[qiankun] Miss window.Proxy, proxySandbox will degenerate into snapshotSandbox'); // 快照沙箱不支持非 singular 模式 if (!singular) { console.error('[qiankun] singular is forced to be true when sandbox enable but proxySandbox unavailable'); frameworkConfiguration.singular = true; } } } // 啓動主應用- single-spa startSingleSpa({ urlRerouteOnly }); frameworkStartedDefer.resolve(); }
/src/apis.tspromise
export function registerMicroApps<T extends object = {}>( apps: Array<RegistrableApp<T>>, lifeCycles?: FrameworkLifeCycles<T>, ) { // 防止重複註冊子應用 const unregisteredApps = apps.filter(app => !microApps.some(registeredApp => registeredApp.name === app.name)); microApps = [...microApps, ...unregisteredApps]; unregisteredApps.forEach(app => { const { name, activeRule, loader = noop, props, ...appConfig } = app; // 註冊子應用 registerApplication({ name, app: async () => { loader(true); await frameworkStartedDefer.promise; const { mount, ...otherMicroAppConfigs } = await loadApp( { name, props, ...appConfig }, frameworkConfiguration, lifeCycles, ); return { mount: [async () => loader(true), ...toArray(mount), async () => loader(false)], ...otherMicroAppConfigs, }; }, activeWhen: activeRule, customProps: props, }); }); }
13行, 調用single-spa 的 registerApplication 方法註冊子應用。瀏覽器
傳參:name、回調函數、activeRule 子應用激活的規則、props,主應用須要傳給子應用的數據。app
src/loader.ts
// get the entry html content and script executor const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);
GitHub地址:https://github.com/kuitos/imp...
export function importEntry(entry, opts = {}) { // ... // html entry if (typeof entry === 'string') { return importHTML(entry, { fetch, getPublicPath, getTemplate }); } // config entry if (Array.isArray(entry.scripts) || Array.isArray(entry.styles)) { const { scripts = [], styles = [], html = '' } = entry; const setStylePlaceholder2HTML = tpl => styles.reduceRight((html, styleSrc) => `${ genLinkReplaceSymbol(styleSrc) }${ html }`, tpl); const setScriptPlaceholder2HTML = tpl => scripts.reduce((html, scriptSrc) => `${ html }${ genScriptReplaceSymbol(scriptSrc) }`, tpl); return getEmbedHTML(getTemplate(setScriptPlaceholder2HTML(setStylePlaceholder2HTML(html))), styles, { fetch }).then(embedHTML => ({ // 這裏處理同 importHTML , 省略 }, })); } else { throw new SyntaxError('entry scripts or styles should be array!'); } }
src/loader.ts
async () => { if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) { return prevAppUnmountedDeferred.promise; } return undefined; },
單實例進行檢測。在單實例模式下,新的子應用掛載行爲會在舊的子應用卸載以後纔開始。
const render = getRender(appName, appContent, container, legacyRender); // 第一次加載設置應用可見區域 dom 結構 // 確保每次應用加載前容器 dom 結構已經設置完畢 render({ element, loading: true }, 'loading');
render 函數內中將拉取的資源掛載到指定容器內的節點。
const containerElement = document.createElement('div'); containerElement.innerHTML = appContent; // appContent always wrapped with a singular div const appElement = containerElement.firstChild as HTMLElement; const containerElement = typeof container === 'string' ? document.querySelector(container) : container; if (element) { rawAppendChild.call(containerElement, element); }
在這個階段,主應用已經將子應用基礎的 HTML 結構掛載在了主應用的某個容器內,接下來還須要執行子應用對應的 mount 方法(如 Vue.$mount)對子應用狀態進行掛載。
此時頁面還能夠根據 loading 參數開啓一個相似加載的效果,直至子應用所有內容加載完成。
src/loader.ts
let global = window; let mountSandbox = () => Promise.resolve(); let unmountSandbox = () => Promise.resolve(); if (sandbox) { const sandboxInstance = createSandbox( appName, containerGetter, Boolean(singular), enableScopedCSS, excludeAssetFilter, ); // 用沙箱的代理對象做爲接下來使用的全局對象 global = sandboxInstance.proxy as typeof window; mountSandbox = sandboxInstance.mount; unmountSandbox = sandboxInstance.unmount; }
這是沙箱核心判斷邏輯,若是關閉了 sandbox 選項,那麼全部子應用的沙箱環境都是 window,就很容易對全局狀態產生污染。
src/sandbox/index.ts
app 環境沙箱
render 沙箱
這麼設計的目的是爲了保證每一個子應用切換回來以後,還能運行在應用 bootstrap 以後的環境下。
let sandbox: SandBox; if (window.Proxy) { sandbox = singular ? new LegacySandbox(appName) : new ProxySandbox(appName); } else { sandbox = new SnapshotSandbox(appName); }
src/sandbox/legacy/sandbox.ts
const proxy = new Proxy(fakeWindow, { set(_: Window, p: PropertyKey, value: any): boolean { if (self.sandboxRunning) { if (!rawWindow.hasOwnProperty(p)) { addedPropsMapInSandbox.set(p, value); } else if (!modifiedPropsOriginalValueMapInSandbox.has(p)) { // 若是當前 window 對象存在該屬性,且 record map 中未記錄過,則記錄該屬性初始值 const originalValue = (rawWindow as any)[p]; modifiedPropsOriginalValueMapInSandbox.set(p, originalValue); } currentUpdatedPropsValueMap.set(p, value); // 必須從新設置 window 對象保證下次 get 時能拿到已更新的數據 (rawWindow as any)[p] = value; 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 會拋出 TypeError,在沙箱卸載的狀況下應該忽略錯誤 return true; }, get(_: Window, p: PropertyKey): any { if (p === 'top' || p === 'parent' || p === 'window' || p === 'self') { return proxy; } const value = (rawWindow as any)[p]; return getTargetValue(rawWindow, value); }, has(_: Window, p: string | number | symbol): boolean { return p in rawWindow; }, });
// 子應用腳本文件的執行過程: eval( // 這裏將 proxy 做爲 window 參數傳入 // 子應用的全局對象就是該子應用沙箱的 proxy 對象 (function(window) { /* 子應用腳本文件內容 */ })(proxy) );
當調用 get 從子應用 proxy/window 對象取值時,會直接從 window 對象中取值。對於非構造函數的取值將會對 this 指針綁定到 window 對象後,再返回函數。
LegacySandbox 的沙箱隔離是經過激活沙箱時還原子應用狀態,卸載時還原主應用狀態(子應用掛載前的全局狀態)實現的,具體源碼實如今 src/sandbox/legacy/sandbox.ts 中的 SingularProxySandbox 方法。
src/sandbox/proxySandbox.ts
constructor(name: string) { this.name = name; this.type = SandBoxType.Proxy; const { updatedValueSet } = this; const self = 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 (self.sandboxRunning) { // @ts-ignore target[p] = value; updatedValueSet.add(p); interceptSystemJsProps(p, value); 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 會拋出 TypeError,在沙箱卸載的狀況下應該忽略錯誤 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') { document[attachDocProxySymbol] = proxy; // 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 the complex scenarios, such as the micro app runs in the same task context with master in som case // fixme if you have any other good ideas nextTick(() => delete document[attachDocProxySymbol]); return document; } // eslint-disable-next-line no-bitwise const value = propertiesWithGetter.has(p) ? (rawWindow as any)[p] : (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'); return descriptor; } return undefined; }, // trap to support iterator with sandbox ownKeys(target: FakeWindow): PropertyKey[] { return uniq(Reflect.ownKeys(rawWindow).concat(Reflect.ownKeys(target))); }, 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; }
相比較而言,ProxySandbox 是最完備的沙箱模式,徹底隔離了對 window 對象的操做,也解決了快照模式中子應用運行期間仍然會對 window 形成污染的問題。
src/sandbox/snapshotSandbox.ts
不支持 window.Proxy 屬性時,將會使用 SnapshotSandbox 沙箱,這個沙箱主要有如下幾個步驟:
SnapshotSandbox 沙箱就是利用快照實現了對 window 對象狀態隔離的管理。相比較 ProxySandbox 而言,在子應用激活期間,SnapshotSandbox 將會對 window 對象形成污染,屬於一個對不支持 Proxy 屬性的瀏覽器的向下兼容方案。
src/sandbox/patchers/dynamicAppend.ts
避免主應用、子應用樣式污染。
子-子之間避免。
對動態添加的腳本進行劫持的主要目的就是爲了將動態腳本運行時的 window 對象替換成 proxy 代理對象,使子應用動態添加的腳本文件的運行上下文也替換成子應用自身。
src/loader.ts
unmountSandbox = sandboxInstance.unmount;
src/sandbox/index.ts
/** * 恢復 global 狀態,使其能回到應用加載以前的狀態 */ async unmount() { // 循環執行卸載函數-移除dom/樣式/腳本等;修改狀態 sideEffectsRebuilders = [...bootstrappingFreers, ...mountingFreers].map(free => free()); sandbox.inactive(); },
src/globalState.ts
qiankun內部提供了 initGlobalState 方法用於註冊 MicroAppStateActions 實例用於通訊,該實例有三個方法,分別是:
offGlobalStateChange:取消 觀察者 函數 - 該實例再也不響應 globalState 變化。
文|鬼滅關注得物技術,攜手走向技術的雲端