为什么需要微前端
1、微前端就是将不同的功能按照不同的维度拆分成多个子应用的。通过主应用来加载这些子应用。微前端的核心在于拆,拆完后在合,实现分而治之
2、微前端解决的问题
- 不同团队(技术栈不同),同时开发一个应用
- 每个团队开发的模块可以独立开发,独立不熟
- 实现增量迁移
3、如何实现微前端
就是将一个应用划分成若干个子应用,将子应用打包成一个个模块。当路径切换时加载不同的子应用。这样每个子应用都是独立的,技术栈也不用做限制了。从而解决了前端协同开发的问题。
4、实现微前端技术方案
-
采用何种方案进行引用拆分
-
采用何种方式进行通讯
-
应用之间如何进行隔离
-
1)iframe
- 微前端的最简单的方案,通过 iframe加载子应用
- 可以通过postMessage进行通讯
- 完美的沙箱机制自带应用隔离 缺点:用户体验差(弹框只能在iframe中、在内部切换刷新就会丢失状态)
-
2)web components
- 将前端应用程序分解为自定义的 HTML 元素
- 基于 CustomEvent 实现通讯
- Shadow DOM 天生的作用域隔离 缺点: 浏览器支持问题,学习成本,调试困难、修改样式困难等问题
-
3)single-spa
- single-spa 是通过路由解气实现加载(采用 SystemJS), 提供应用间公共组件加载以及公共业务逻辑处理。子应用需要暴露固定的钩子 bootStrap、mount、unMount 接入协议
- 基于 props 主子应用之间进行通讯 缺点: 学习成本、无沙箱机制、需要对原有的应用进行改造、子应用之间相同资源重复加载问题
-
4) Module federation
- 通过模块联邦将组建进行打包导出使用
- 共享模块之间的方式进行通讯
- 无 css沙箱和JS沙箱 缺点: 需要webpack5
5、微前端single-spa(底层实现)
- 乾坤是基于 single-spa实现,single-spa基于systemJs实现,这里我们先看下SystemJs大致实现流程(基于源码不多,直接贴代码)
<body>
<!-- systemJs规范 -->
<script type="systemjs-importmap">
{ // 这里模拟 webpack打包的 react 项目
"imports": {
"react-dom": "https://cdn.bootcdn.net/ajax/libs/react-dom/18.2.0/cjs/react-dom-server-legacy.browser.development.js",
"react": "https://cdn.bootcdn.net/ajax/libs/react-is/18.2.0/cjs/react-is.development.js"
}
}
</script>
<script>
// 1) system 是如何定义的,先看打包后的结果, System.register(参数一:依赖列表, 参数二: 回调函数==返回值==>(setters,execute) )
// 2) react、react-dom加载后调用 setters将对应的结果赋值给 webpack
// 3) 执行调用逻辑,执行页面渲染
// 解析 importMap,生成映射表
const newMapUrl = {};
function processScripts() {
Array.from(document.querySelectorAll("script")).forEach((script) => {
if (script.type === "systemjs-importmap") {
const imports = JSON.parse(script.innerHTML).imports;
Object.entries(imports).forEach(([key, val]) => {
newMapUrl[key] = val;
});
}
});
}
// 加载资源
function load(id) {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = newMapUrl[id] || id; // 支持 cdn 查找
script.async = true;
document.head.appendChild(script);
script.addEventListener("load", function () {
// 先拷贝,在置空
let _lastRegister = lastRegister;
lastRegister = undefined;
resolve(); // 此时会执行代码
});
});
}
let set = new Set(); // 快照:先保存window上的属性
function saveGlobalProperty() {
for (let key in window) {
set.add(key);
}
}
saveGlobalProperty();
function getLastGlobalProperty() {
// 其次看下window上是否添加了新属性
// 获取时进行对比
for (let k in window) {
if (set.has(k)) continue;
set.add(k);
return window[k];
}
}
let lastRegister;
// 模块规范, 用来加载system模块
class SystemJs {
import(id) {
// 这个id原则上可以是一个第三方路径 如:cdn
return Promise.resolve(processScripts())
.then(() => {
// 1、去当前路径查找对应资源
// console.log(newMapUrl);
const lasteSepIndex = location.href.lastIndexOf("/");
const baseUrl = location.href.slice(0, lasteSepIndex + 1);
if (id.startsWith("./")) {
return baseUrl + id.slice(2);
}
})
.then((id) => {
// 根据文件路径加载资源
// console.log(id);
let exectue;
return load(id)
.then((register) => {
console.log(register);
// 这里的第一项是回调函数,执行完返回 setter、execute
// 这里解构别名
let { setters, exectue: exec } = register[1](() => {});
exectue = exec;
// execute 真正执行的渲染逻辑
// setters 是用来保存家在后的资源,加载资源调用 setters
return [register[0], setters];
})
.then(([registeration, setters]) => {
return Promise.all(
registeration.map((dep, i) => {
load(dep).then(() => {
// 拿到是函数, 加载资源,将家在后的模块传递给这个setter
// 模块加载完后会在window 上添加属性, window.React window.ReactDOM
// 获取window上最后添加的属性
setters[i](getLastGlobalProperty());
});
})
);
})
.then(() => {
// 文件加载完之后
exectue();
});
});
}
register(deps, declare) {
console.log("‘文件加载完毕");
// 将会掉的结果保存
lastRegister = [deps, declare];
}
}
const System = new SystemJs();
System.import("./index.js").then((res) => {
// 这里呢就要返回一个promise
});
// 先加载依赖列表, 再去加载真正的逻辑, 内部是通过 创建script 脚本加载资源。 然后就是 给window保存快照,保存先后状态
</script>
</body>
总结:SystemJs 是基于自己的规范 systemjs-importmap, 创建script 标签进行引入文件(JSONP形式),然后插入到head中,文件加载完之后执行文件。 这里源码已经附上,可以在自己项目打包后的dist文件夹下使用。如有疑问可进行交流关注微信公共号分享更多源码。