初识微前端
微前端是什么
概念: 微前端是指存在于浏览器中的微服务。
微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将单页面前端应用由单一的单体应用转变为把多个小型前端应用聚合为一体的应用。这就意味着前端应用的拆分,拆分后的应用实现应用自治、单一职责、技术栈无关三大特性,再进行基座模式或自由组合的模式进行聚合,达到微前端的目的。
自由组组织模式指的就是:系统内部子系统之间能自行按照某种规则形成一定的结构或功能。
微前端的几个基本要素: 技术栈无关、应用隔离、独立开发。
核心:拆,合
微前端背景
在前端框架、技术、概念层出不穷,且随着前端标准的演进,前端已经具备更好的性能和开发效率,但是随之而来的是应用的复杂度更高、涉及的团队规模更广、更高的性能要求,应用复杂度已经成为阻塞业务发展的重要瓶颈。
微前端就是诞生在这日益复杂化的场景中。
为什么使用微前端
为了解决团队平台系统多且相互独立,系统体量大且页面多,开发效率低、接入成本高。
当前应用痛点:
- 项目中的组件和功能模块会越来越多,导致整个项目的打包速度变慢;
- 因为文件夹的数量会随着功能模块的增多而增多,查找代码会变得越来越慢;
- 如果只改动其中一个模块的情况,需要把整个项目重新打包上线;
- 所有的项目都基本只能使用同一技术框架,不便引入新技术栈。如:react、vue等;
微前端优势:
- 简单、松耦合的代码库
-
- 微前端架构倾向于编写和维护更小、更简单、更容易开发的项目。
- 技术栈无关,各项目可以使用不同的技术栈。
- 增量升级
-
- 支持渐进式重构,先让新旧代码和谐共存,再逐步转化旧代码,直到整个重构完成。
- 独立部署
-
- 每一个子应用都具备独立开发,持续部署,独立运行的能力。
- 团队自治
-
- 各子项目之间不存在依赖关系,保持隔离。
- 单一职责,每个子项目只做和自己相关的业务工作。
微前端现有方案
浅谈single-spa
qiankun是基于single-spa的二次封装,因此在谈qiankun之前,我们先来简单了解一下single-spa。
single-spa的核心就是定义了一套协议。协议包含主应用的配置信息和子应用的生命周期,通过这套协议,主应用可以方便的知道在什么情况下激活哪个子应用。
生命周期:
简单梳理一下single-spa的整个流程:
从上面的图里面,我们可能会产生下面四个问题?
(1)主应用如何注册微应用?
single-spa中提供了registerApplication方法来注册子应用。这个方法中接受几个特定的参数:
singleSpa.registerApplication({ //注册微前端服务
name: 'vueApp',
app: () => {return ...}, // 加载你的子应用
activeWhen: '/vueApp',//url 匹配规则,表示啥时候开始走这个子应用的生命周期
customProps: { // 自定义 props,从子应用的 bootstrap, mount, unmount 回调可以拿到
uid:'6018990034'
}
});
singleSpa.start() // 启动主应用
在single-spa中并未实现加载子应用的方法,需要使用者自己实现比如通过动态创建script或者可以使用System.import()
。
⚠️:SystemJS 可以在浏览器里可以使用 ES6 的 import/export 并支持动态引入。
(2)主应用什么时候调度微应用的生命周期?
主应用加载完成子应用后,获取到子应用中暴露出的生命周期。
window['singleDemo'] = {
bootstrap,
mount,
unmount
}
上面这种暴露生命周期的方式,也就是single-spa中子应用对外暴露变量的方式。使用不当会导致全局环境的污染。
因此的appName比如唯一,否则会覆盖同名的子应用,qiankun里面的沙箱机制就很好的解决了这种问题。
那上面的生命周期什么时候调度呢?
一般来说主应用注册并加载完微应用,并不会立即初始化。而是等到执行完start后。
当window.location.href匹配到url时,去执行start方法后进行初始化(调用子应用的bootStrap),开始走对应子App的这一套生命周期。所以,single-spa还要监听url的变化,然后执行子app的生命周期流程。
(3)主应用如何监听路由以及控制路由跳转的?
在上面我们了解到,主应用监听路由url的切换,匹配到相应的路由,就根据对应路由去挂载和卸载微应用。
那么如何监听这个路由的跳转呢?
(4)主应用如何挂载及卸载微应用的?
子应用挂载时,需要在mount方法中添加挂载逻辑;
子应用卸载时,需要在unmount方法中添加卸载逻辑,在update方法中添加更新逻辑。
single-spa的挂载、更新、卸载并未提供,而是需要用户自定实现。
single-spa 只起控制状态的作用,它自己本身不亲自操刀的,无论下载、挂载、卸载等,这样也能做到更好的扩展性,用户想怎么下载、挂载、卸载,他们自己来决定,只要你传入规范的参数即可。
single-spa为不同技术栈提供了一些逻辑抽象封装来对子应用进行包装。比如:single-spa-vue\single-spa-react等。这些封装的模版里面对子应用进行包装,然后在对应的生命周期钩子函数执行子应用挂载卸载更新等操作。
总结:
一般来说,微前端需要解决的问题分为两大类:
- 应用的加载与切换:路由问题、应用入口、应用加载
- 应用的隔离与通信:js隔离、css样式隔离、应用间通信
而single-spa则很好地解决了 路由问题、应用入口 两个问题,但是应用的加载并未实现。因此qiankun在此基础上封装了一个应用加载方案,并给出了js隔离、css样式隔离和应用间通信三个问题的解决方案,同时提供了预加载功能。
qiankun
qiankun 使用 import-html-entry 插件将子应用的 html 作为入口,框架会将 HTML document 作为子节点塞到主框架的容器中。就算子应用更新了,其入口 html 文件的 url 始终不会变,并且完整的包含了所有的初始化资源 url,所以不用再自行维护子应用的资源列表了。并且对旧有的项目作为子应用接入成本几乎为零,开发体验与独立开发时保持不变,相较于 single-spa 的 js entry 而言更加灵活、方便、体验更好。
qiankun框架中,子应用不需要关注qiankun框架,无需引用其包,只需按照标准实现导出接口即可
1.子应用集成
qiankun有两种集成微应用的方式:基于路由配置、手动加载微应用
基于路由配置微应用
将微应用关联到一些url规则的方式,实现当浏览器url发生变化时,自动加载相应的微应用。
registerMicroApps(apps, lifeCycles?)注册子应用
apps:微应用的一些注册信息
lifeCycles:选填
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([ {
name: 'app-react',
entry: '//localhost:8080',
container: '#container',
activeRule: '/react',
props: {
name: 'kuitos',
},
}]);
start(); //开启qiankun
手动加载微应用
适用于手动加载/手动卸载一个微应用的场景
onMounted(() => {
state.microapp = loadMicroApp(
{
name: 'app-react',
entry: '//app.xxx.com',
container: '#app-react-container',
props: {
data: { ...$store.state, componentId: $componentId }
}
},
// {
// singular: true
// sandbox:false //默认是开启状态
// }
)
})
onUnmounted(() => {
state.microapp.unmount()
})
⚠️:基座中展示的子应用,关闭路由页面并未直接卸载子应用实例,仍然占据内存,并且下次打开也不是全新的。因此我们需要在应用卸载子应用。
2.运行时沙箱
loadApp时执行createSandbox,生成运行时沙箱(样式沙箱和js沙箱),qiankun 框架默认开启预加载、单例模式、样式沙箱
样式沙箱
样式沙箱包含了严格沙箱模式(默认开启)和实验性沙箱模式,两种模式不可共存。
shadow DOM严格模式
shadow DOM可以将一个隐藏的、独立的DOM附加到一个元素上,即微应用的容器。
- Shadow host:一个常规 DOM 节点,Shadow DOM 会被附加到这个节点上。
- Shadow tree:Shadow DOM 内部的 DOM 树。
- Shadow boundary:Shadow DOM 结束的地方,也是常规 DOM 开始的地方。
- Shadow root: Shadow tree 的根节点。
我们常见的标签 、用的就是shadow dom。
实现:
<div id="shadow-dom"></div>
<div class="wrapper">
<style>
p{
color: #000;
}
</style>
<p>外部部文本</p>
</div>
<script>
function shadowDOMIsolation(htmlString) {
//(1)拿到当前元素的内容
htmlString = htmlString.trim();
//(2)创建 shadowDOM容器
const containerDom = document.createElement("div");
//(3)将内容放入shadowDom容器
containerDom.innerHTML = htmlString;
//注意根元素只有一个
const appElement = containerDom.firstChild;
const { innerHTML } = appElement;
//(4)清楚这个元素,便于后面追加shadowDOM
appElement.innerHTML=""
let shadow;
if (appElement.attachShadow) {
shadow = appElement.attachShadow({
mode: "open",
});
} else {
shadow = appElement.createShadowRoot();
}
//追加shadowDom
shadow.innerHTML = innerHTML;
return appElement;
}
const shadowDOMSection = document.querySelector("#shadow-dom");
const appElement = shadowDOMIsolation(`
<div class="wrapper">
<style>p { color: purple }</style>
<p>内部文本</p>
</div>
`);
shadowDOMSection.appendChild(appElement);
</script>
上面代码主要做了下面几件事:
- 把当前元素的内容拿出来
- 生成 shadowDOM
- 再刚刚的内容放入这个 shadow DOM
- 清除这个元素,并追加 shadow DOM 即可
Scoped css 实验性模式
原理:将微应用中的style全部提取出来,将所有的选择器进行替换。
在目前的阶段,该功能还不支持动态的、使用 标签来插入外联的样式,但考虑在未来支持这部分场景。现阶段仅支持style 这种内联标签的情况 。
const styleNodes = element.querySelectorAll('style') || [];
forEach(styleNodes, (stylesheetElement: HTMLStyleElement) => {
css.process(element!, stylesheetElement, appName);
});
/*拿到样式节点中的所有样式规则,然后重写样式选择器
* 含有根元素选择器的情况:用前缀替换掉选择器中的根元素选择器部分,
* 普通选择器:将前缀插到第一个选择器的后面Ï
*/
process(styleNode: HTMLStyleElement, prefix: string = '') {
// 样式节点不为空,即 <style>xx</style>
if (styleNode.textContent !== '') {
// 创建一个文本节点,内容为 style 节点内的样式内容
const textNode = document.createTextNode(styleNode.textContent || '');
// swapNode 是 ScopedCss 类实例化时创建的一个空 style 节点,将样式内容添加到这个节点下
this.swapNode.appendChild(textNode);
const sheet = this.swapNode.sheet as any; // type is missing
/**
* 得到所有的样式规则,比如
* [
* {selectorText: "body", style: CSSStyleDeclaration, styleMap: StylePropertyMap, type: 1, cssText: "body { background: rgb(255, 255, 255); margin: 0px; }", …}
* {selectorText: "#oneGoogleBar", style: CSSStyleDeclaration, styleMap: StylePropertyMap, type: 1, cssText: "#oneGoogleBar { height: 56px; }", …}
* {selectorText: "#backgroundImage", style: CSSStyleDeclaration, styleMap: StylePropertyMap, type: 1, cssText: "#backgroundImage { border: none; height: 100%; poi…xed; top: 0px; visibility: hidden; width: 100%; }", …}
* {selectorText: "[show-background-image] #backgroundImage {xx}"
* ]
*/
const rules = arrayify<CSSRule>(sheet?.cssRules ?? []);
/**
* 重写样式选择器
* 含有根元素选择器的情况:用【前缀】替换掉选择器中的根元素选择器部分,
* 普通选择器:将前缀插到第一个选择器的后面
*/
const css = this.rewrite(rules, prefix);
// 用重写后的样式替换原来的样式
// eslint-disable-next-line no-param-reassign
styleNode.textContent = css;
// cleanup
this.swapNode.removeChild(textNode);
return;
}
......
}
实现步骤:
(1)获取样式节点
(2)创建容器swapNode
(3)为容器swapNode添加textNode
(4)获取到textNode的根div元素,为其打上data-app-name=appName的标记
(5)用重写后的样式替换原来的样式
<style>
p{
color:red;
}
</style>
<div data-app-name="my-test">
<p>文本内容</p>
</div>
<!-- scoped css模式下样式变成: -->
div[data-app-name="my-test"] p{
color:red
}
cssRule:
https://www.wenjiangs.com/wiki/en-US/docs/Web/API/CSSRule
js沙箱
qiankun 的 js 沙箱分三种: proxySandBox、legacySandBox、snapshotSandBox 。
proxySandBox
原理:基于Proxy实现的多例模式下的沙箱。创建变量 fakeWindow(虚拟的 window ),并通过Proxy代理 fakeWindow对象,所有更改都基于fakeWindow,从而保证每个ProxySandbox实例之间属性互不影响。
激活时:
(1)被激活的沙箱数+1,开启沙箱运行标识sandboxRunning:true
失活时:
(1)被激活的沙箱数-1,开启沙箱运行标识sandboxRunning:false
proxy如何获取属性值以及修改添加属性具体看下图:
设置全局变量时:先判断fakeWindow上是否有该属性,若无则更改window;若有该值则直接修改fakeWindow。
获取全局变量时:先判断该属性是否为原生属性,如果是原生属性则直接从window上获取,非原生属性,则优先从fakeWindow上获取。
总结:此模式最大的特点是,子应用的 window 是一个代理对象,不是真正的 window,子应用对 window 的操作,实际上是对 fakeWindow 进行操作,而不是操作真实 window
legacySandBox
legacySandbox是一种单例沙箱,这个模式和proxySandBox类似基于proxy实现的。
legacySandbox原理:基于 Proxy 实现的单例模式下的沙箱,直接操作原生 window 对象,并记录 window 对象的增删改查,在每次微应用切换时初始化 window 对象。
*激活时:将 window 对象恢复到上次即将失活时的状态(遍历currentUpdatedPropsValueMap)
- 失活时:将 window 对象恢复为初始状态遍历addedPropsMapinSandbox和modifiedPropsOrginalMapInSandbox
关于在legacySandbox模式下,获取全局属性,设置全局属性。legacySandbox为了记录在修改或者添加属性时window的变更,设置了三个变量池,具体看下图:
设置全局变量时:
(1)window上不存在该属性,则向addedPropsMapInSandbox添加该属性
(2)存在该属性,但modifiedPropsOriginalValueMapInSandbox中不存在该属性,则记录该初始值
(3)记录新增和修改的属性currentUpdatedPropsValueMap,直接设置原生 window 对象,因为是单例模式,不会有其它的影响
获取全局变量时:直接从window上获取
与proxySandBox的区别:
legacySandbox子应用对 window 对象修改时,实际上修改的就是真实 window,这个代理 window 的作用是维护三个状态池,分别用于子应用卸载时还原主应用的状态和子应用加载时还原子应用的状态
优缺点:
优点:采用代理的方式修改 window, 不用再遍历 window, 性能得到提升
缺点:兼容性不如proxySandBox,只能支持加载一个程序。(单例模式)
snapshotSandBox快照模式
在浏览器不支持 proxy 的情况下,就会使用此模式。
原理:基于diff方式实现的沙箱。把主应用的 window 对象做浅拷贝windowSnapshot,将windowSnapshot的变更存成一个 Hash Map。之后无论微应用对 window 做任何改动,当要在恢复环境时,把这个 Hash Map 又应用到 window 上。
当微应用mount时:
(1)将上一次的变更记录modifyPropsMap应用到微应用的全局window,无变更记录则跳过
(2)对主应用的window对象做浅拷贝,用于后面还原主应用的window
当微应用umount时:
(1)将微应用的window与快照window做Diff,Diff的结果modifyPropsMap用于下次恢复微应用环境的依据。
(2)根据当前的快照对象windowSnapshot,还原window
举个例子说明一下:
window:{a:1,b:2}
windowSnap:{a:1,b:2}
变动后的window:{a:1,b:2,c:3}
对于window和windowSnap,生成diff后的对象modifyPropsMap
根据windowSnap还原window
3.资源预加载
(1)直接配置 prefetch 属性
(2)调用prefetchApps方法
import { prefetchApps } from 'qiankun';
prefetchApps([{ name: 'qianshuju_qp', entry: '//qp.zhuanspirit.com' }])
手动加载微应用
import { loadMicroApp,prefetchApps } from 'qiankun';
//第一种
loadMicroApp({
name: 'child-app',
entry: 'http://localhost:7100',
container: '#child-app-container',
prefetch: true, // 开启 Prefetch
}).then(
() => console.log('child-app 加载成功!')
);
当调用 loadMicroApp 方法时,qiankun 会遍历子应用的 entry,将其中的 prefetch 资源添加到主应用的 head 元素中。当用户访问子应用时,浏览器会自动加载这些资源,以便更快地加载子应用。
需要注意的是,开启 Prefetch 会增加网络带宽的消耗,因此我们应该根据实际情况选择是否开启 Prefetch,避免资源的浪费。
基于路由配置微应用
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([ {
name: 'app-react',
entry: '//localhost:8080',
container: '#container',
activeRule: '/appreact',
}, ]);
start({ prefetch: ['app-react','app-vue'] });//在这里配置或者如果是多个实用prefetchApps()
预加载的好处:可以加速微应用的打开速度。通过在浏览器空闲时间预加载未打开的微应用资源,可以减少用户等待时间,提高用户体验。
4.应用间通信
从微前端的设计初衷来看,我们需要尽可能少的进行应用间的通信。使我们的微前端架构可以更加灵活可控,但是由于业务需要还是会存在应用间通信的。
简单介绍一下qiankun官方给出的方案:
Actions 通信方案是通过全局状态池和观察者函数进行应用间通信,比较适合业务划分清晰,应用间通信较少的微前端应用场景。
qiankun 内部提供了 initGlobalState 方法用于注册 MicroAppStateActions 实例用于通信,该实例有三个方法,分别是:
setGlobalState:设置 globalState - 设置新的值时,内部将执行浅检查,如果检查到 globalState 发生改变则触发通知,通知到所有的观察者函数;
onGlobalStateChange:注册观察者函数 - 响应 globalState 变化,在 globalState 发生改变时触发该观察者函数;
offGlobalStateChange:取消观察者函数 - 该实例不再响应 globalState 变化。
actions通信图解
Actions 通信方案也存在一些优缺点,优点如下:
- 使用简单;
- 官方支持性高;
- 适合通信较少的业务场景;
缺点如下:
- 子应用独立运行时,需要额外配置无 Actions 时的逻辑;
- 子应用需要先了解状态池的细节,再进行通信;
- 由于状态池无法跟踪,通信场景较多时,容易出现状态混乱、维护困难等问题;
问题汇总
(1)弹窗的样式问题怎么解决?
子应用中给弹窗设置了样式,加载到主应用后,主应用中查看弹窗,发现样式失效。
因为比如element的弹窗默认是挂在 body 上的,(shadowDOM外部无法影响内部,内部也无法影响外部子)所以子应用中设置的全局样式会不生效。
那么另一种沙箱scoped CSS可以吗?答案是肯定的,不行。看了前面样式scoped css添加样式后的代码,也是给容器内部的元素添加样式前缀,并未给外部添加。
【解决方式】
(1)打包的时候,给项目样式添加自定义前缀。
(2)vue使用scoped css \react 使用modules css均是实现组件级别的样式隔离。
(3)既然默认挂载在body上,可以修改挂载位置。
(2)路由跳转问题
qiankun的子应用的router由于是子应用自己的路由,所有的跳转均基于子应用的base,因此没法直接通过 或者用 router.push/router.replace 跳转。
解决方式:
- /window.location.href链接可以跳转过去,但是会刷新页面,用户体验并不好
- 将主应用的路由实例通过 props 传给子应用,子应用用这个路由实例跳转。
- 路由模式为 history 模式时,通过 history.pushState() 方式跳转
(3)本地加载正常,上线后无法加载,报错跨域问题
原因:父子应用的域名不一样,某些业务场景下,cros同源策略可能会引发跨域问题
解决:找运维配置nigix解决跨域问题
(4)线上子应用不显示字体图标 解决:webpack中配置url-loader,把图标转为base64格式
常规webpack配置
module.exports = {
module: {
rules: [
{
test: /.(png|jpe?g|gif|webp|woff2?|eot|ttf|otf)$/i,
use: [
{
loader: 'url-loader',
options: {limit: 999999999,}, // 此处随便
},
],
},
],
},
};
(5)子应用中修改window上面的属性,不生效
原因:微应用挂载window的 是 proxy 代理出来的 window,并不是真实的window,所以修改会被隔离掉
如果需要修改可以:
在修改window对象之前先获取到qiankun提供的sandbox对象,然后通过sandbox对象来修改window对象上的属性。例如:
const { sandbox } = window.__POWERED_BY_QIANKUN__;
// 修改 window 上的属性
sandbox.window.xxx = 'new value';
无论是 CSS 还是 JavaScript 沙箱都不是十全十美的,我们只能通过各种约束来避免沙箱出现问题的可能。例如:建立团队前缀,命名空间 CSS、事件、本地存储和 Cookie,以避免冲突并明确所有权。
(6)localStorage、sessionStorage应用覆盖问题
原因:父子应用都是同一个 window,所以 localStorage、sessionStorage、cookie, 这些方法就会造成数据覆盖问题:注意微应用之间数据冲突、数据覆盖问题,这里改写一个 setItem getItme 解决这个问题
(7)使用基于路由的配置,打开后页面空白,控制台无报错
问题1:start()启动时机不对,在main.js中启动,页面挂载的容器并未生成,导致挂载容器失败。
问题2:在路由页面的created中,调用start()方法,首次正常显示,切换路由失败,因为每次切换路由相当于重新加载一次页面,挂载的id虽然一致,但是页面路由切换被销毁重建因此加载失败
解决:
- 全局注册(registerMicroApps)子应用改为局部注册(loadMicroApp)
- 在app.vue中写挂载的容器。判断路由是属于子路由则展示容器,否则展示
(8)子应用生命周期导出失败,测试环境无法正常加载子应用、本地和线上正常。
排查思路:
(1)推动运维对部署的文件进行排查
(2)nginx配置排查
(3)物理机/docker之间的差异问题
(4)代码断点查找问题
最终发现问题:beetle的脚本插入逻辑与乾坤的子应用读取逻辑冲突
了解qiankun读取查找主应用的逻辑代码:
- 找带有entry属性的
- 找不到把最后一个
如果两种都没有找到,则需要检查修改qiankun子应用的配置文件,将微应用的 name 和 Webpack 的 output.library 设为一致。
(9)qiankunCss样式问题
Qiankun的CSS沙箱主要是通过一种叫做Scoped CSS的技术实现的。在这种技术中,每个子应用的CSS都会被添加一个独特的属性选择器,这样它们就不会影响到其他应用的样式。然而,这种方法也有一些潜在的问题:
- CSS样式污染:虽然Qiankun有自己的样式隔离机制,但这个机制并不完全。例如,子应用的样式仍然可能会影响到全局的样式,如body、html标签或者全局CSS类。这是因为,CSS沙箱无法阻止子应用修改全局CSS规则。
- 性能问题:为了实现样式隔离,Qiankun需要遍历并修改所有的CSS规则,这在某些情况下可能会对性能产生影响。
- 样式覆盖问题:由于Qiankun是通过为每个子应用的CSS规则添加一个独特的属性选择器来实现样式隔离的,所以如果子应用中有使用了!important的样式规则,可能无法被正确的隔离。
- 第三方库的样式隔离:如果子应用使用了一些第三方库,这些库的样式可能会泄露到全局环境中,从而影响到其他子应用或者主应用的样式。
- 动态添加的样式隔离:如果子应用在运行时动态添加了一些样式(例如,通过document.createElement(‘style’) 或document.styleSheets[0].insertRule() ),那么这些样式可能无法被正确的隔离。
总结
总是微前端是什么呢?一句话:在路由变化的时候,去加载对应子应用的代码,并在容器内跑起来。
简单说一下微前端在乾数据使用后带来的感受:
好处,分为以下几点:
- 缩小项目打包体积
- 解决系统上线拥挤问题
- 用户使用无感知
- 海盗乾派业务拆分、项目拆分拼装更灵活
- 技术栈逐步统一
也是有很多麻烦之处,需要消耗一定成本:
- 避免样式污染问题,需要制定一定的规范。
- 如果你也想要tab切换不刷新(使用keep-alive),那需要做的工作更多,主要是处理缓存,防止堆内存溢出(用chrome自带的performance monitor查看),还有项目间切换时路由钩子等等的处理。
就目前来看,基本没有什么问题~
最后我想说:
无论是那种微前端方案,都会存在自己的适配用户。而采用微前端后,也并不会让系统的复杂度凭空消失,而是会由之前的代码层面的设计转向了系统架构设计划分的设计挑战。
应用场景上,一般微前端还是应用在B端,C端应用比较少。主要原因是移动端应用一般不会特别复杂。当然也有一些例外:工具类的C端管理项目等。
并不是所有场景都适合微前端,尤其是项目规模小、数量少的不建议使用,微前端也并不是所有系统的归宿,应该由场景、业务发展以及价值去决定。