1. iframe
這里指的是每個微應用獨立開發部署,通過 iframe 的方式將這些應用嵌入到父應用系統中,幾乎所有微前端的框架最開始都考慮過 iframe,但最后都放棄,或者使用部分功能,原因主要有:
-
url 不同步。瀏覽器刷新 iframe url 狀態丟失、后退前進按鈕無法使用。
-
UI 不同步,DOM 結構不共享。想象一下屏幕右下角 1/4 的 frame 里來一個帶遮罩層的彈框,同時我們要求這個彈框要瀏覽器居中顯示,還要瀏覽器重置大小時自動居中。
-
全局上下文完全隔離,內存變量不共享。iframe 內外系統的通信、數據同步等需求,主應用的 cookie 要透傳到根域名都不同的子應用中實現免登效果。
-
慢。每次子應用進入都是一次瀏覽器上下文重建、資源重新加載的過程。
2. single-spa
single-spa 是一個基礎的微前端框架,通俗點說,提供了生命周期的概念,并負責調度子應用的生命周期 挾持 url 變化事件和函數,url 變化時匹配對應子應用,并執行生命周期流程,完整的生命周期流程為:
2.1. Root Config
index.html:靜態資源、子應用入口聲明。
// index.html
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>Polyglot Microfrontends</title><meta name="importmap-type" content="systemjs-importmap" /><script type="systemjs-importmap" src="https://storage.googleapis.com/polyglot_microfrontends.app/importmap.json"></script><!-- if (isLocal) { --><script type="systemjs-importmap">{"imports": {"@polyglot-mf/root-config": "/localhost:9000/polyglot-mf-root-config.js"}}</script><!-- } --><script src="https://cdn.jsdelivr.net/npm/import-map-overrides@2.2.0/dist/import-map-overrides.js"></script><script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/system.min.js"></script><script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/extras/amd.min.js"></script>
</head>
<body><script>System.import('@polyglot-mf/root-config');System.import('@polyglot-mf/styleguide');</script><import-map-overrides-full show-when-local-storage="devtools" dev-libs></import-map-overrides-full>
</body>
</html>
main.js:子應用注冊及啟動。
// main.jsimport { registerApplication, start } from "single-spa";registerApplication({name: "@polyglot-mf/navbar",app: () => System.import("@polyglot-mf/navbar"),activeWhen: "/",
});registerApplication({name: "@polyglot-mf/clients",app: () => System.import("@polyglot-mf/clients"),activeWhen: "/clients",
});registerApplication({name: "@polyglot-mf/account-settings",app: () => loadWithoutAMD("@polyglot-mf/account-settings"),activeWhen: "/settings",
});start();// A lot of angularjs libs are compiled to UMD, and if you don't process them with webpack
// the UMD calls to window.define() can be problematic.
function loadWithoutAMD(name) {return Promise.resolve().then(() => {let globalDefine = window.define;delete window.define;return System.import(name).then((module) => {window.define = globalDefine;return module;});});
}
single-spa提倡在瀏覽器中直接使用微前端應用,而不是通過構建工具進行打包。在single-spa中,參考的是SystemJS的思路,從而支持在瀏覽器中使用import、export。其實通過構建工具也可以實現類似的功能,webpack 可以將 ES6 的模塊語法轉換為瀏覽器可以理解的格式,并進行打包和優化。
2.2. single-spa-layout
指定single-spa在index.html中哪里渲染指定的子應用,constructApplications,constructRoutes及constructLayoutEngine 是針對定義的layout中的元素獲取屬性,再批量注冊。
<html><head><template id="single-spa-layout"><single-spa-router><nav class="topnav"><application name="@organization/nav"></application></nav><div class="main-content"><route path="settings"><application name="@organization/settings"></application></route><route path="clients"><application name="@organization/clients"></application></route></div><footer><application name="@organization/footer"></application></footer></single-spa-router></template><script>// 注冊import { registerApplication, start } from 'single-spa';import {constructApplications,constructRoutes,constructLayoutEngine} from 'single-spa-layout';// 獲取routesconst routes = constructRoutes(document.querySelector("#single-spa-layout"));// 獲取所有的子應用const applications = constructApplications({routes,loadApp({ name }) {return System.import(name); // SystemJS 引入入口JS},});// 生成LayoutEngineconst layoutEngine = constructLayoutEngine(routes, applications);// 批量注冊子應用applications.forEach(registerApplication);// 啟動主應用start();</script></head></html>
2.3. 子應用注冊
single-spa針對子應用不同類型的子應用(如Vue、React等)都進行封裝,但核心還是bootstrap、mount、unmount生命周期鉤子。
import SubApp from './index.tsx'export const bootstrap = () => {}export const mount = () => {// 使用 React 來渲染子應用的根組件ReactDOM.render(<SubApp />, document.getElementById('root'));
}export const unmount = () => {}
2.4. 樣式隔離
提供子應用CSS的引入和移除:single-spa-css
// 代碼塊
import singleSpaCss from 'single-spa-css';const cssLifecycles = singleSpaCss({// 這里放你導出的 CSS,如果 webpackExtractedCss 為 true,可以不指定cssUrls: ["https://example.com/main.css"],// 是否要使用從 Webpack 導出的 CSS,默認為 falsewebpackExtractedCss: false,// 是否 unmount 后被移除,默認為 trueshouldUnmount: true,// 超時,不廢話了,都懂的timeout: 5000
})const reactLifecycles = singleSpaReact({...})// 加入到子應用的 bootstrap 里
export const bootstrap = [cssLifecycles.bootstrap,reactLifecycles.bootstrap
]export const mount = [// 加入到子應用的 mount 里,一定要在前面,不然 mount 后會有樣式閃一下的問題cssLifecycles.mount,reactLifecycles.mount
]export const unmount = [// 和 mount 同理reactLifecycles.unmount,cssLifecycles.unmount
]
子應用間CSS樣式隔離,推薦使用scoped CSS和shadowDOM.
2.5. JS隔離
給每個子應用添加全局變量,加入時添加,移除是去除:single-spa-leaked-globals.
// 代碼塊
import singleSpaLeakedGlobals from 'single-spa-leaked-globals';// 其它 single-spa-xxx 提供的生命周期函數
const frameworkLifecycles = ...const leakedGlobalsLifecycles = singleSpaLeakedGlobals({globalVariableNames: ['$', 'jQuery', '_'], // 新添加的全局變量
})export const bootstrap = [leakedGlobalsLifecycles.bootstrap, // 放在第一位frameworkLifecycles.bootstrap,
]export const mount = [leakedGlobalsLifecycles.mount, // mount 時添加全局變量,如果之前有記錄在案的,直接恢復frameworkLifecycles.mount,
]export const unmount = [leakedGlobalsLifecycles.unmount, // 刪掉新添加的全局變量frameworkLifecycles.unmount,
]
<