注册
web

真的有必要用微前端框架么?

前言


最近公司项目在用qiankun构建微前端的应用,深深体会到微前端的魅力,无框架限制,主应用统一管理,弹窗的统一位置等。如果是刚开始就植入微前端还好,不过基本上都是后期老项目植入微前端,各种拆分模块,也是一件很头疼的事情。


基石


我们为什么要用微前端


大的应用体量维护成本是很高的,拆分成单独的模块,由主应用处理登录等通用逻辑,子应用来只负责模块的业务实现,这样不管资源加载、按需加载、人员维护成本降低、增量升级、独立部署都有很好的体检提升。当然前提是体量非常大的web应用可以这么做,但是开始做的时候你会很头疼各种拆解带来的不确定性,但是长痛不如短痛。


Why Not Iframe


下面是我从qiankun文档摘抄的:


iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。那么为什么不用iframe呢 ?



  1. url 不同步。浏览器刷新 iframe url 状态丢失等(本地缓存不就行了?)
  2. UI 不同步,DOM 结构不共享(主应用控制不就行了?子应用通过postMessage传递数据给父应用)
  3. 一次性加载,慢!(个人感觉就是项目体积小了!跟iframe有啥区别么?)
  4. 全局上下文完全隔离,内存变量不共享。(当然这里通过postMessage是可以实现通信的!)

image.png


所以其实用iframe就够了,微前端是不是有点kpi的味道呢?当然学习下源码还是对自己有提升的,万一iframe没有,是不是可以手撸一个呢?


源码入口


image.png


最核心的就是手动加载loadMicroApp、registerMicroApps注册微应用,start开始构建这3个api,但其实qiankun的核心是基于single-spa框架封装的, 我们看下single-spa做了些什么,以及single-spa内部核心api的registerApplication做了什么


single-spa


single-spa是一个框架,用于将多个JavaScript微前端组合在一个前端应用程序中。使用单一页面中心构建前端可以带来许多好处,例如:



registerApplication 注册应用


export function reroute (pendingPromises = [], eventArguments) {
//...
const {
appsToUnload,
appsToUnmount,
appsToLoad,
appsToMount,
} = getAppChanges(); //返回不同生命周期的队列

//记录基础的应用信息
let appsThatChanged,
navigationIsCanceled = false,
oldUrl = currentUrl,
newUrl = (currentUrl = window.location.href);

//这里根据是否已挂载做处理
if (isStarted()) {
//....
} else {
appsThatChanged = appsToLoad;
return loadApps();
}
//加载apps
function loadApps () {
return Promise.resolve().then(() => {
const loadPromises = appsToLoad.map(toLoadPromise);

return (
Promise.all(loadPromises)
.then(callAllEventListeners)
// there are no mounted apps, before start() is called, so we always return []
.then(() => [])
.catch((err) => {
callAllEventListeners();
throw err;
})
);
});
}
//根据app状态改变发布对应的事件
function performAppChanges () {
return Promise.resolve().then(() => {
// https://github.com/single-spa/single-spa/issues/545
window.dispatchEvent(
new CustomEvent(
appsThatChanged.length === 0
? "single-spa:before-no-app-change"
: "single-spa:before-app-change",
getCustomEventDetail(true)
)
);
//...做了大量的自定义事件以及卸载事件
}
//....
}
复制代码

说实话这里的源码很绕,这里只摘取最关键的,在registerApplication内部,将qiankun的registerMicroApps的参数传入做些兼容判断,然后调用了一个核心的reroute方法, 这里删除了不必要的干扰信息,说白了single-spa做了spa的生命周期的管理,每个应用有单独的html做页面的加载,但是环境的隔绝是需要qiankun做的


getAppChanges 状态管理


export function getAppChanges () {
const appsToUnload = [],
appsToUnmount = [],
appsToLoad = [],
appsToMount = [];

// We re-attempt to download applications in LOAD_ERROR after a timeout of 200 milliseconds
const currentTime = new Date().getTime();

apps.forEach((app) => {
const appShouldBeActive =
app.status !== SKIP_BECAUSE_BROKEN && shouldBeActive(app);

switch (app.status) {
case LOAD_ERROR:
if (appShouldBeActive && currentTime - app.loadErrorTime >= 200) {
appsToLoad.push(app);
}
break;
case NOT_LOADED:
case LOADING_SOURCE_CODE:
if (appShouldBeActive) {
appsToLoad.push(app);
}
break;
case NOT_BOOTSTRAPPED:
case NOT_MOUNTED:
if (!appShouldBeActive && getAppUnloadInfo(toName(app))) {
appsToUnload.push(app);
} else if (appShouldBeActive) {
appsToMount.push(app);
}
break;
case MOUNTED:
if (!appShouldBeActive) {
appsToUnmount.push(app);
}
break;
// all other statuses are ignored
}
});

return { appsToUnload, appsToUnmount, appsToLoad, appsToMount };
}
复制代码

getAppChange返回了应用一旦改变那么不同生命周期的队列会更新


registerMicroApps注册微应用


简单的看下用法,qiankun的核心api并不多


image.png


image.png


registerMicroApps注册微应用中,传入apps的信息,如name、路由匹配规则、container挂载dom等,和生命周期的钩子lifeCycles。核心的应用状态管理在single-spa中,而qiankun在外层又做了统一的上层的主应用封装。这里比较重要的是loadApp,里面统一处理解决了全局变量混入的问题也就是沙箱隔离,样式隔离等。下面是loadApp的一部分核心逻辑


环境隔离


1. 全局变量的隔离


createSandboxContainer 创建沙箱


image.png


这里核心就是createSandboxContainer的用法,这里简单讲下几个核心的参数,initialAppWarpperGetter用于处理检查是否有无包裹的dom元素,scpredCSS是代表样式是否已经被隔离状态, useLooseSandbox和speedySandbox处理不同状态的沙箱。
,在里面核心的方法是patchAtBootstrapping做了不同的沙箱隔离方式的隔绝处理


patchAtBootstrapping 启动器


image.png


patchAtBootstrapping中的策略者模式,我们可以看到有3种沙箱的处理方式,legacyProxy、Proxy、Snapshot,并分别对应了pathLooseSandbox、pathStrictSandbox、patchLooseSandbox,下面简单解析下原理,理解即可。



Snapshot沙箱隔离



先将主应用的window拷贝一份,一旦微应用切换到主应用做回退,如果微应用切换,那么会提前生成微应用的diff过程的对象,然后回退。而缺陷就是diff属性量一旦过大会性能不好



Legacy沙箱隔离



那么与Snapshot最大的不同是用了Proxy来处理,set做记录,一旦应用切换就回退,相对于我不断循环遍历diff,性能好了不少



Proxy沙箱隔离



前面两种应用场景在于都是一个路由对应一个微应用,那么如果是多个微应用同时出现在一个页面中,那么环境是不是不可控了呢。这种情况就不能在window直接操作,而是要每个应用都要有一个独立的fakeWindow,这样区分环境后,数据处理尽量在fakeWindow上处理,而不是原生window


Proxy模式的核心的我们看下pathStrictSandbox源码


pathStrictSandbox 严格模式


image.png


Proxy代理模式的沙箱,通过Object.defineProperty来拦截对象属性,但是不可枚举可写入, 这样每次切换应用我都重新获取新的nativeGlobal


nativeGlobal 全局对象


export const nativeGlobal = new Function('return this')();
复制代码

通过new Function来更安全的返回全局对象


2. DOM的隔离


image.png


image.png


image.png


很明显这里是通过ShadowDOM来实现dom的隔离,我们常见的比如video、audio标签内部都是可以看到shadowDOM实现的,同时我们也可以看到做了兼容性的处理


3. 样式隔离


image.png
scopedCSS代表是否要隔离css,如果要隔离首先去判断将微应用的根元素挂载qiankun的属性标记,然后遍历所有style标签,css.process对每个内部的样式属性名做了模块化的处理,而appInstanceId就是做微应用样式隔离的id区分


通信


import-html-entry


qiankun用的是import-html-entry这个库的execSceipts方法来请求获得并解析脚本的,然后直接把html插入到容器里,所以应用间需要允许跨域才行,在importEntry你可以发现他使用了浏览器空闲的api,requestIdleCallback以及为基础实现预加载prefetch


image.png


image.png


image.png


initGlobalState 全局状态


image.png


image.png


image.png


我们主要看下initGlobalState,通过emitGlobal来触发更新全局状态,从上图可以看出核心通过deps发布订阅模式来管理每个微应用,然后更新状态。返回的onGlobalChange和setGlobalState来监听变化和触发通知。状态管理还是比较简单的。


总结


花了几天时间看了源码,收获还是挺大的,微前端其实主要有3个的核心点在于应用通信、应用的生命周期及状态管理、沙箱环境隔离。相对来说iframe足够满足我们业务需求了,微前端提供了一种思路还是不错的,但是真的有必要用qiankun么?


作者:谦宇
链接:https://juejin.cn/post/7201282972967944250
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册