微前端,關於他的好和應用場景,不少師兄們也都介紹過了,那麼咱們使用的微前端方案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,
});
});
}
複製代碼
src/loader.ts瀏覽器
// get the entry html content and script executor
const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);
複製代碼
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.tsmarkdown
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');
複製代碼
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
這麼設計的目的是爲了保證每一個子應用切換回來以後,還能運行在應用 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)
);
複製代碼
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;
}
複製代碼
src/sandbox/snapshotSandbox.ts
不支持 window.Proxy 屬性時,將會使用 SnapshotSandbox 沙箱,這個沙箱主要有如下幾個步驟:
2. 把window快照內的屬性所有綁定在 modifyPropsMap 上,用於後續恢復變動。 3. 記錄變動,卸載時若是不同,就恢復改變以前的window屬性值。
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 實例用於通訊,該實例有三個方法,分別是:
文|鬼滅 關注得物技術,攜手走向技術的雲端