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/[email protected]/dist/import-map-overrides.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/system.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/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.js
import { 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';
// 获取routes
const routes = constructRoutes(document.querySelector("#single-spa-layout"));
// 获取所有的子应用
const applications = constructApplications({
routes,
loadApp({ name }) {
return System.import(name); // SystemJS 引入入口JS
},
});
// 生成LayoutEngine
const 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,默认为 false
webpackExtractedCss: false,
// 是否 unmount 后被移除,默认为 true
shouldUnmount: 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,
]
<