项目合并技术•微前端
single-spa
初始化状态
- 未加载 -> 加载中 -> 已加载
- 未启动 -> 启动中 -> 已启动
- 未挂载 -> 挂载中 -> 已挂载
- 卸载中
- 销毁中
- 运行出错
路由劫持
window.addEventListener('hashchange', urlReroute);
window.addEventListener('popstate', urlReroute);
重写侦听器方法
const routingEventsListeningTo = ["hashchange", "popstate"];
const capturedEventListeners = {
hashchange: [],
popstate: [],
};
// 保存当前监听器
const originalAddEventListener = window.addEventListener;
const originalRemoveEventListener = window.removeEventListener;
// 重写监听器
window.addEventListener = function (eventName, fn) {
if (
routingEventsListeningTo.indexOf(eventName) >= 0 &&
!capturedEventListeners[eventName].some((listener) => listener == fn)
) {
capturedEventListeners[eventName].push(fn);
return;
}
return originalAddEventListener.apply(this, arguments);
};
window.removeEventListener = function (eventName, fn) {
if (routingEventsListeningTo.indexOf(eventName) >= 0) {
capturedEventListeners[eventName] = capturedEventListeners[
eventName
].filter((fn) => fn !== listenerFn);
return;
}
return originalAddEventListener.apply(this, arguments);
};
重写事件方法
window.history.pushState = patchedUpdateState(window.history.pushState);
window.history.replaceState = patchedUpdateState(window.history.replaceState);
关键状态初始化(是否启动基座、是否切换完成、队列中的任务、保存注册的应用)
startd = false
appChangeUnderway = false
peopleWaitingOnAppChange = []
apps = []
导出方法
exports.registerApplication = registerApplication;
exports.start = start;
Object.defineProperty(exports, '__esModule', { value: true });
核心方法
// 注册子应用
function registerApplication(appName, loadApp, activeWhen, customProps) {
apps.push({
name: appName,
loadApp,
activeWhen,
customProps,
status: NOT_LOADED, // 默认应用为未加载
});
reroute();
}
// 启动基座
function start() {
started = true;
reroute();
}
// 子应用管理
function reroute(pendingPromises = [], eventArguments) {
// 已经切换完成
if (appChangeUnderway) {
return new Promise((resolve, reject) => {
peopleWaitingOnAppChange.push({
resolve,
reject,
eventArguments,
});
});
}
// 将注册的应用按状态分为三个数组
const { appsToLoad, appsToMount, appsToUnmount } = getAppChanges();
// 如果调用了start()方法
if (started) {
appChangeUnderway = true;
// 启动、挂载应用
return performAppChanges();
} else {
// 加载应用
return loadApps();
}
}
隔离方案三要素
- 无技术栈限制
- 应用单独开发
- 多应用整合
方案分析
iframe(优点)
- 原生的硬隔离方案,完美解决 css 隔离、js 隔离问题
- 适用于接入第三方页面
iframe(问题)
- 浏览器刷新导致 url 状态丢失,后退、前进按钮无法使用(可解决)
- 全局上下文完全隔离,主应用的 cookie 要透传到各个子应用中实现免登效果(难解决)
- 子应用加载速度慢,每次进入子应用都是一次浏览器上下文重建、资源重新加载的过程(无法解决)
- ui 不同步问题,如 loading 效果(无法解决)
微前端(优点)
- 子应用加载速度快
微前端(问题)
- 应用的加载与切换:路由问题、应用入口、应用加载
- 应用的隔离与通信:JS隔离、CSS样式隔离、应用间通信
- qiankun:解决了协同工作的问题,封装了应用加载方案(import-html-entry),并给出了应用的隔离与通信的解决方案,同时提供了预加载功能
ES Module(问题)
- 兼容性问题,但可以通过编译工具解决
Web Components(问题)
- 浏览器的新特性,兼容性问题
EMP(优点)
- 基于webpack5 module Federation(mf)
- 跨应用状态共享
- 跨框架组件调用
- 第三方依赖共享
EMP(问题)
- 目前无法涵盖所有框架
微前端模式
路由问题
- 监听hashChange和popState两个原生事件
- 调用reroute函数
- 通过参数activeWhen判断需要加载的应用
- 如果应用已加载 ,则进行应用的加载或切换
- 如果应用未加载,则加载对应的应用
- 劫持pushState和replaceState两个原生方法,避免第三方库调用该方法时触发hashChange事件,single-spa意外重载
singleSpa.start({ urlRerouteOnly: true})
应用入口
采用协议入口,要求应用入口必须暴露三个生命周期钩子函数,且必须返回Promise
bootstarp:挂载前的准备工作
mount:应用挂载
unmount:应用卸载
其他生命周期:load/unload/update
弊端:手动实现应用加载逻辑,只能以JS文件为入口,无法直接以HTML文件为入口,因此不能直接加载JQuery应用
应用加载
- qiankun通过import-html-entry请求url,得到对应的HTML文件
- 解析内部所有的script和style标签,依次下载并替换到模板内
- 返回一个对象,对象内包含处理后的模板以及几个核心方法
getExternalScripts
getExternalStyleSheets
execScripts
JS隔离
- 将子应用全局变量挂载到window.proxy上
- 如果子应用代码内直接使用
window.name = 'test'
生成全局变量,则无法隔离JS污染 - 因为IE不支持proxy,所以IE下的快照策略无法支持多实例模式
CSS隔离
- shadowDom样式隔离:子应用根节点创建一个shadow root
- 某些UI框架弹出框直接挂载到document.body下,对全局造成污染
- 类似于scoped属性的样式隔离:子应用的根节点添加一个特定的随机属性
- 不支持@keyframes、@font-face、@import、@page
- shadowDom样式隔离:子应用根节点创建一个shadow root
应用通信
- 由主应用创建一个globalState的全局对象,内部包含一组用于通信的变量
- 两个分别用于修改变量值和监听变量变化的方法:
setGlobalState
和onGlobalStateChange