微前端?
?一個應用,當不斷迭代的時候,功能會越來越多,代碼量隨著也會變得越來越大。進而代碼之間的耦合性會變高,這樣導致開發和維護很糟心,動一發而牽全身。于是有了微前端來解這個問題,按功能可以將這個應用拆分成多個項目,每個項目都是獨立的倉庫,獨立的部署,然后利用微前端再組合成在一起。
較大的前端應用拆分為若干個可以獨立交付的前端應用。每個應用大小及復雜度相對可控。能降低應用之間的耦合度,獨立開發,獨立運行,獨立部署,提升每個團隊的自治能力。
實現原理?
?乾坤是基于single-spa框架的,而single-spa是一個將多個單頁面應用聚合為一個整體應用的 JavaScript 微前端框架,而乾坤在single-spa的基礎上主要做了資源的加載和應用之間的隔離。
- 下面是single-spa的使用示例。通過registerApplication來注冊子應用,然后當頁面的路由與activeWhen首次相匹配時,就會觸發app函數的執行。這個函數里面返回一些生命周期,在這些生命周期里做一些渲染。
- ·
import * as singleSpa from 'single-spa';//注冊應用 singleSpa.registerApplication({name: 'app1',//應用激活路由條件activeWhen: '/app1',app(){const domEl = document.getElementById('#container')return {//首次掛載時執行bootstrap(){},//掛載時執行mount(){domEl.innerHTML = 'App 1 is mounted!'}, //卸載時執行unmount(){domEl.innerHTML = ''}}} }) singleSpa.registerApplication({name: 'app2',activeWhen: '/app2',app(){const domEl = document.getElementById('#container')return {bootstrap(){},mount(){domEl.innerHTML = 'App 2 is mounted!'}, unmount(){domEl.innerHTML = ''}}} })singleSpa.start();
下面是乾坤的使用示例。在single-spa中配置的是函數,而在乾坤中配置的是文件地址。
-
import {registerMicroApps, start} from 'qiankun';registerMicroApps([{name: 'app1',entry: {scripts: ['/app1.js'],styles:['/app1.css']},container: '#container',activeRule: '/app1',},{name: 'app2',entry: {scripts: ['/app2.js']},container: '#container',activeRule: '/app2',},]);start();
解決跨域(主應用間路由跳轉,微應用如何設置跨域訪問,各生命周期鉤子如何執行)
header:{Access-Control-Allow-Origin:'*"}
?
沙盒?
沙盒主要作用就是隔離微應用之間的腳本和樣式影響,需要處理style、link、script類型的標簽。對于處理的時機第一個是在首次加載的時候,第二個是在微應用運行中。在運行中的處理方案就是乾坤重寫了下面這些原生的方法,這樣就可以監聽到新添加的節點,然后對style、link、script標簽進行處理。
if (//原始方法未被替換HTMLHeadElement.prototype.appendChild === rawHeadAppendChild &&HTMLBodyElement.prototype.appendChild === rawBodyAppendChild &&HTMLHeadElement.prototype.insertBefore === rawHeadInsertBefore
) {//替換原始方法HTMLHeadElement.prototype.appendChild = getOverwrittenAppendChildOrInsertBefore({rawDOMAppendOrInsertBefore: rawHeadAppendChild,containerConfigGetter,isInvokedByMicroApp,}) as typeof rawHeadAppendChild;...
}
?
腳本隔離(一種是快照拷貝的方式,一個是基于proxy的方式)?
腳本隔離的方式主要有兩種,一種是快照拷貝的方式,一個是基于proxy的方式。乾坤會根據當前環境是否支持proxy來決定用那種方式。
- 快照方式
下面是乾坤中關于快照隔離的代碼。在創建微應用的時候會實例化一個沙盒對象,它有兩個方法,active是在激活微應用的時候執行,而inactive是在離開微應用的時候執行。
整體的思路是在激活微應用時將當前的window對象拷貝存起來,然后從modifyPropsMap中恢復這個微應用上次修改的屬性到window中。在離開微應用時會與原有的window對象做對比,將有修改的屬性保存起來,以便再次進入這個微應用時進行數據恢復,然后把有修改的屬性值恢復到以前的狀態。
?
//遍歷對象屬性,用callbackFn回調
function iter(obj: typeof window, callbackFn: (prop: any) => void) {for (const prop in obj) {if (obj.hasOwnProperty(prop) {callbackFn(prop);}}
}
class SnapshotSandbox implements SandBox {active() {// 記錄當前快照this.windowSnapshot = {} as Window;iter(window, (prop) => {this.windowSnapshot[prop] = window[prop];});// 恢復之前的變更Object.keys(this.modifyPropsMap).forEach((p: any) => {window[p] = this.modifyPropsMap[p];});}inactive() {this.modifyPropsMap = {};iter(window, (prop) => {if (window[prop] !== this.windowSnapshot[prop]) {// 保存變更this.modifyPropsMap[prop] = window[prop];// 恢復到以前的狀態window[prop] = this.windowSnapshot[prop];}});}
}
2.proxy方式
乾坤中關于proxy的隔離方式有兩種,我們以最優的方案分析。下面中的代碼省略了很多,只講其中的原理。
微應用中的script內容都會加with(global)來執行,這里global是全局對象,如果是proxy的隔離方式那么他就是下面新創建的proxy對象。我們知道with可以改變里面代碼的作用域,也就是我們的微應用全局對象會變成下面的這個proxy。當設置屬性的時候會設置到proxy對象里,在讀取屬性時先從proxy里找,沒找到再從原始的window中找。也就是你在微應用里修改全局對象的屬性時不會在window中修改,而是在proxy對象中修改。因為不會破壞window對象,這樣就會隔離各個應用之間的數據影響。
?
class implements SandBox {constructor(name: string) {const rawWindow = window;const proxy = new Proxy(fakeWindow, {set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {//修改對象直接保存到proxy對象中target[p] = value;},get(target: FakeWindow, p: PropertyKey): any {// 從proxy對象中獲取,獲取不到從fakeWindow中獲取const value = p in target? (target as any)[p]: (rawWindow as any)[p];return value;}})}
}
樣式隔離?
默認情況下沙箱可以確保單實例場景子應用之間的樣式隔離(切換子應用時會卸載添加的樣式標簽),但是無法確保主應用跟子應用、或者多實例場景的子應用樣式隔離,需要手動增加配置參數才能激活下面的隔離。
- 域隔離
為每個css規則添加特定的前綴來起到隔離的作用,例如微應用中的樣式是p{color:#000},處理后為.app1 p {color:#000} 。
- 創建一個臨時的style節點用來后續處理
- 調用process來處理style規則
- 通過style的sheet屬性來獲取一條條規則
- 然后調用ruleStyle進行轉化,轉化是通過正則進行匹配然后替換
- 最后把轉化后的內容替換到原有的style節點中
?
class ScopedCSS { constructor() {//創建一個臨時style節點,用來處理樣式this.swapNode = document.createElement('style');rawDocumentBodyAppend.call(document.body, styleNode);}process(styleNode: HTMLStyleElement, prefix: string = '') {//style節點內容不為空if (styleNode.textContent !== '') {//獲取內部的樣式規則this.swapNode.appendChild(styleNode.textContent);const sheet = this.swapNode.sheet as any;//獲取替換后的cssconst css = this.ruleStyle(sheet, prefix);//應用替換后的cssstyleNode.textContent = css;}}private ruleStyle(rule: CSSStyleRule, prefix: string) {const rootSelectorRE = /((?:[^\w\-.#]|^)(body|html|:root))/gm;const selector = rule.selectorText.trim();let { cssText } = rule;//\s\S 匹配所有 這里可以匹配到如 body{cssText = cssText.replace(/^[\s\S]+{/, (selectors) =>// div,span{} 逗號拆分處理selectors.replace(/(^|,\n?)([^,]+)/g, (item, p, s) => {// handle div,body,span { ... }if (rootSelectorRE.test(item)) {return item.replace(rootSelectorRE, (m) => {const whitePrevChars = [',', '('];//處理前面有, (if (m && whitePrevChars.includes(m[0])) {return `${m[0]}${prefix}`;}//直接返回prefixreturn prefix;});}//p 匹配到逗號 , s為 span{ 后面這里會去掉空白return `${p}${prefix} ${s.replace(/^ */, '')}`;}),);return cssText;}
}
遇到過哪些問題?