1.微服务概念
微服务:微服务架构(通常简称为微服务)是指开发应用所用的一种架构形式。通过微服务,可将大型应用分解成多个独立的组件,其中每个组件都有各自的责任领域。在处理一个用户请求时,基于微服务的应用可能会调用许多内部微服务来共同生成其响应。
通常跟微服务相对的是单体应用,即将所有功能都打包成在一个独立单元的应用程序。从单体应用到微服务并不是一蹴而就的,这是一个逐渐演变的过程
知乎上一个小案例加深印象:https://www.zhiimportmaphu.com/question/65502802
个人的理解是将拆分的思想贯彻到底(模块也是拆!数据库也是拆!),分工明确,将每个功能细致化分配任务。再加上兼容一下老应用的逻辑,和新框架开发的代码的一套架构理念
但是缺点就是:在微服务架构中,一个服务故障可能会产生雪崩效用,导致整个系统故障
- 微服务架构整个应用分散成多个服务,定位故障点非常困难。
- 稳定性下降。服务数量变多导致其中一个服务出现故障的概率增大,并且一个服务故障可能导致整个系统挂掉。事实上,在大访问量的生产场景下,故障总是会出现的。
- 服务数量非常多,部署、管理的工作量很大。
- 开发方面:如何保证各个服务在持续开发的情况下仍然保持协同合作。
- 测试方面:服务拆分后,几乎所有功能都会涉及多个服务。原本单个程序的测试变为服务间调用的测试。测试变得更加复杂。
所以使用微服务的架构要有要有强大的监控体系
2.沙箱(sand box)
传统意义上:Sandbox(沙箱)是指一种技术,在这种技术中,软件运行在操作系统受限制的环境中。由于该软件在受限制的环境中运行,即使一个闯入该软件的入侵者也不能无限制访问操作系统提供设施;获得该软件控制权的黑客造成的损失也是有限的
然而,在计算机安全中,沙箱(Sandbox)是一种用于隔离正在运行程序的安全机制,通常用于执行未经测试或不受信任的程序或代码,它会为待执行的程序创建一个独立的执行环境,内部程序的执行不会影响到外部程序的运行
抽象化:对于操作系统来说,浏览器就是一个沙箱,对于浏览器来说,每个页面就是一个独立的沙箱
实际运用:
- Vue 模板表达式的计算是运行在一个沙盒之中的,在模板字符串中的表达式只能获取部分全局对象,这一点官方文档有提到,详情可参阅源码。
- CodeSanbox ,在线编辑器
- 许多应用程序带的插件,有很多的限制条件,这些应用程序在运行插件时需要遵循宿主程序制定的运行规则,插件的运行环境和规则就是一个沙箱
- ….
从知乎这篇文章可以看到沙箱的基本介绍https://zhuanlan.zhihu.com/p/428039764
里面的《简陋沙箱》、《非常简陋的沙箱(With)》、《没那么简陋的沙箱(With+Proxy)》循序渐进,都可以帮助读者加深印象
里面主要通过new Function
、eval
、with
修改上下文作用域
然后就是天然沙箱:iframe
、和 ifame+with+proxy
的结合,但是现在简单的沙箱都有可能被使用者使用某种代码漏洞,进行 “沙箱逃逸”,从而获取沙箱之外环境的内容进行读取、修改等,这时候就要去找开源库了
JS隔离
Js 沙箱做的事情可以用两句话概括:
- 为每一个子应用创建一个专属的 “window 对象”(虚假的);
- 执行子应用时,将新建的 “window 对象” 作为子应用脚本的全局变量,子应用对全局变量的读写操作都作用到这个 “window 对象”中。
沙箱类型:
LegacySandbox
(依赖 Proxy),父子状态隔离,但是window不隔离ProxySandbox
(依赖 Proxy)稳定后会取代LegacySandbox
。SnapshotSandbox
(不依赖Proxy)
参考字节跳动的技术文章:https://juejin.cn/post/7099339595233361934#heading-24
3.微前端
微前端(Micro-Frontends)同样是一套体系架构,是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以无关技术栈地独立运行、独立开发、独立部署。
在如今越来越注重拆分和细化的时代,微前端的到来并没有在意料之外,而微前端的意义就:
- 将这些庞大应用进行拆分,并随之解耦,每个部分可以单独进行维护和部署,提升效率。
- 整合历史系统,将老架构框架项目兼容性放在新的架构体系中
当然,没有免费的午餐——一切都是有代价的,一些微前端实现可能会导致依赖项重复,但是我们相信这些风险是可以控制的,而且微前端的好处往往超过成本。 ——–Cam Jackson
微前端解决的问题:
实际上,一个典型的基于vue-router的Vue应用与这种架构存在着很大的相似性
微前端方案的核心是“主从”思想,即包括一个基座(MainApp
)应用和若干个微(MicroApp
)应用,基座应用大多数是一个前端SPA
项目,主要负责应用注册,路由映射,消息下发等,而微应用是独立前端项目,这些项目不限于采用React,Vue,Angular
或者JQuery
开发,每个微应用注册到基座应用中,由基座进行管理,但是如果脱离基座也是可以单独访问
方案 | 描述 |
---|---|
NPM式 | 子工程以NPM包的形式发布源码;打包构建发布还是由基座工程管理,打包时集成。 |
iframe式 | 子工程可以使用不同技术栈;子工程之间完全独立,无任何依赖;基座工程和子工程需要建立通信机制;无单页应用体验;路由地址管理困难。 |
通用中心路由基座式 | 子工程可以使用不同技术栈;子工程之间完全独立,无任何依赖;统一由基座工程进行管理,按照DOM节点的注册、挂载、卸载来完成。比如qiankun之类的,qiankun 甚至能加载jQuery编写的页面 |
特定中心路由基座式 | 子业务线之间使用相同技术栈;基座工程和子工程可以单独开发单独部署;子工程有能力复用基座工程的公共基建。比如React-router、Vue-router |
(图片源自美团技术团队的文章,而美团用的是基于React的中心路由基座式微前端)
但是微前端仍需解决:
- 路由切换的分发问题。
- 主微应用的隔离问题。
- 通信问题。
路由转发:
通过http请求获取所需的微应用html、css、js,然后可以采用eval的形式运行代码,而分发过程使用router对url进行监听,然后采用一些路由方法(hash方法或者
pushState
方法等)发送路由信息给微应用,让微应用通过它自己的路由进行接收Single-Spa
虽然实现了自己的一套路由来监听子工程的切换,但是还需要特定的模块管理系统,比如systemjs
来辅助,这导致了改造成本和添加一些额外的库,所以美团直接用的是React-Router
(react-router-dom
)
隔离问题:
对于css污染,可以采用CSS Module或者webpack的postcss插件,在打包时添加特定的前缀
对于Js代码,我们害怕一些全局对象在不同微应用的不同表现,我们可以采用沙箱隔离机制,消除冲突和影响
通信问题:
- 我们可以用发布订阅通信机制,事件由实践中心统一分发
子应用打包的方式
微前端架构模式下,子应用打包的方式,基本分为两种:
两者的优缺点也很明显:
很显然,要实现真正的技术栈无关跟独立部署两个核心目标,大部分场景下我们需要使用运行时加载子应用这种方案。
JS Entry vs HTML Entry
如果是 JS Entry 方案,主框架需要在子应用加载之前构建好相应的容器节点(比如这里的 “#root” 节点),不然子应用加载时会因为找不到 container 报错
<!-- 子应用 index.html --> <script src="//unpkg/antd.min.js"></script> <body> <main id="root"></main> </body> // 子应用入口 ReactDOM.render(<App/>, document.getElementById('root'))
JS entry打包问题(可以看 why SystemJS 部分)
HTML entry无法抽取公共依赖(很麻烦)
其他:
(webpack5有一个联邦模块功能(mf),对于微前端的公共依赖加载是比较好的解决方案。)
一些微前端的框架
Mooa:基于Angular的微前端服务框架
Single-Spa:最早的微前端框架,兼容多种前端技术栈。
Qiankun:基于Single-Spa,阿里系开源微前端框架。
Icestark:阿里飞冰微前端框架,兼容多种前端技术栈。
Ara Framework:由服务端渲染延伸出的微前端框架。
micro-app:京东微前端框架,借鉴了WebComponent的思想,通过CustomElement结合自定义的ShadowDom,将微前端封装成一个类WebComponent组件,从而实现微前端的组件化渲染
你可能就不需要微前端
存在以下场景时,你可能就不需要微前端:
- 你/你的团队 具备系统内所有架构组件的话语权
简单来说就是,系统里的所有组件都是由一个小的团队开发的。 - 你/你的团队 有足够动力去治理、改造这个系统中的所有组件
直接改造存量系统的收益大于新老系统混杂带来的问题。 - 系统及组织架构上,各部件之间本身就是强耦合、自洽、不可分离的
系统本身就是一个最小单元的「架构量子」,拆分的成本高于治理的成本。 - 极高的产品体验要求,对任何产品交互上的不一致零容忍
不允许交互上不一致的情况出现,这基本上从产品上否决了渐进式升级的技术策略
满足以下几点,你才确实可能需要微前端
系统本身是需要集成和被集成的 一般有两种情况:
- 旧的系统不能下,新的需求还在来。
没有一家商业公司会同意工程师以单纯的技术升级的理由,直接下线一个有着一定用户的存量系统的。而你大概又不能简单通过 iframe 这种「靠谱的」手段完成新功能的接入,因为产品说需要「弹个框弹到中间」 - 你的系统需要有一套支持动态插拔的机制。
这个机制可以是一套精心设计的插件体系,但一旦出现接入应用或被接入应用年代够久远、改造成本过高的场景,可能后面还是会过渡到各种微前端的玩法。
- 旧的系统不能下,新的需求还在来。
系统中的部件具备足够清晰的服务边界
通过微前端手段划分服务边界,将复杂度隔离在不同的系统单元中,从而避免因熵增速度不一致带来的代码腐化的传染,以及研发节奏差异带来的工程协同上的问题。
4.iframe
iframe是一个天然的沙箱
优点:
- 非常简单,使用没有任何心智负担
- 隔离完美,无论是 js、css、dom 都完全隔离开来
- 多应用激活,页面上可以摆放多个
iframe
来组合业务(适合单页面多应用) - 如果页面里有iframe的话,内部的iframe,如果iframe是和标签页属于同一域,那就共用渲染进程,否则会给这个iframe一个单独的渲染进程,所以单页面下(没有路由管理),可能比qiankun等通过拉取代码单线程运行要快
但是它有很多弊端:
- url 不同步,每次进来都要加载,url状态不能保留
- DOM 结构不共享。比如子应用里有一个 Modal(弹窗),显示的时候只能在那一小块地方展示,不能全屏展示
- 无法跟随浏览器前进后退
- 天生的硬隔离,无法与主应用进行资源共享,内存变量不共享,交流也很困难
- 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程,白屏时间太长,对于SPA 应用应用来说无法接受
当然,腾讯提出了一个“完美”的iframe微前端方案,可以看一看基于 iframe 的全新微前端方案(腾讯PCG)
主要是
利用shadow dom插拔html、css来解决dom割裂问题(比如toast)
在iframe中执行js,然后让iframe的路由状态同步到浏览器的路由解决路由问题和通信问题(同域下通信比较简单)
然后内部做了一些缓存(实例化子应用),降低子应用资源加载的成本
而官方也谈及了wujie的不足之处:
- 内存占用较高,为了降低子应用的白屏时间,将未激活子应用的
shadowRoot
和iframe
常驻内存并且保活模式下每张页面都需要独占一个wujie
实例,内存开销较大 - 兼容性一般,目前用到了浏览器的
shadowRoot
和proxy
能力,并且没有做降级方案 iframe
劫持document
到shadowRoot
时,某些第三方库可能无法兼容导致穿透
5.single-spa
Single SPA 将自己定义为一种“前端微服务 Javascript 框架”,它将生命周期应用于每个应用程序。每个应用程序都可以响应 url 路由事件,并且知道如何从 DOM 引导,加载和卸载自身
single-spa 仅仅是一个子应用生命周期的调度者。single-spa 为应用定义了 boostrap, load, mount, unmount 四个生命周期回调:
值得注意的是
- Register 不是生命周期,指的是调用
registerApplication
函数这一步 - Load 是开始加载子应用,怎么加载由开发者自己实现(等会会说到)
- Unload 钩子只能通过调用
unloadApplication
函数才会被调用
当触发到 window.location.href
匹配到 url 时,开始走对应子 App 的这一套生命周期嘛。所以,single-spa 还要监听 url 的变化,然后执行子 app 的生命周期流程。
到此,我们就有了 single-spa 的大致框架了,无非就两件事:
实现一套生命周期,在 load 时加载子 app,由开发者自己玩,别的生命周期里要干嘛的,还是由开发者造的子应用自己玩
监听 url 的变化,url 变化时,会使得某个子 app 变成 active 状态,然后走整套生命周期
根据传入的参数
activeWhen
判断哪个应用需要加载,哪个应用需要卸载或清除,并将其push到对应的数组如果应用已经启动,则进行应用加载或切换。针对应用的不同状态,直接执行应用自身暴露出的生命周期钩子函数即可。
如果应用未启动,则只去下载
appsToLoad
中的应用。
该图作者的评价:
传统 SPA 和 Single SPA 应用程序之间的主要区别在于它们能够与其他应用程序共存,并且它们各自没有自己的 HTML 页面。
single-spa 的理念是希望主应用可以做到非常非常简单的和轻量,简单到只要一个 index.html + 一个 main.js 就可以完成微前端工程,连 Webpack 都不需要,直接在浏览器里执行
在single-spa的角度,微前端就是“微模块加载器”,它主要解决的是:如何实现前端的“微服务化”,从而让应用、组件、逻辑都成为可共享的微服务,这从single-spa关于微前端的概述中可以看出:
翻译过来反别是微应用、微组件、微模块,而single-spa
要求它们都以SystemJS
的形式打包,所以本质上都是微模块
关于single-spa一部分实践可以看这个,但是由于这里的代码的很多包都比较久远,最好下载和它一致的包再进行实践 github
SystemJS
1.浏览器里用import/export。
SystemJS 的好处和优势有且仅有一点:那就是那就是不需要打包工具,直接在浏览器里使用 ES6 的 import/export,也就是说它是一个允许ES module模块运行在旧版本ES环境的库。
Single SPA 中使用到了SystemJS,SystemJS
是一个运行时加载模块的工具(动态导入和导入映射),是现阶段下(浏览器尚未正式支持importMap
)原生ES Module
的完全替代品,对比Webpack 之类的,
其实你也可以理解为SystemJS是为了解决轻量级,不用打包,即可做到 import Vue from 'vue'
形式去导包
importmap的形式,兼容性差
<script type="importmap">
{
"imports": {
"dayjs": "https://cdn.skypack.dev/dayjs@1.10.7",
}
}
</script>
systemjs形式
<script type="systemjs-importmap">
{
"imports": {
"@react-mf/root-config": "//localhost:9000/react-mf-root-config.js"
}
}
</script>
SystemJS
动态加载的模块必须是SystemJS
模块或者UMD
模块。
2.引入公共依赖
SystemJS 另一个好处就是可以通过 importmap 引入公共依赖。
假如,我们有三个子应用,它们都有公共依赖项 antd,那每个子应用打包出来都会有一份 antd 的代码,就显示很冗余。(qiankun就有这种冗余)
一个解决方法就是在主应用里,通过 importmap 直接把 antd 代码引入进来,子应用在 Webpack 设置 external 把 antd 打包时排除掉。子应用打包就不会把 antd 打包进去了,体积也变小了。
对于公共样式的处理;
- 将公共的 CSS 放到 importmap 里,也可以理解为在 index.html 里直接加个 link 获取 antd 的 CSS 完事
- 将所有的公共的 UI 库都 import 到 utility 里,将 antd 所有内容都 export,再把 utility 包放到 importmap 里,然后
import { Button } from '@your-org-name/utility';
去引入里面的组件
其实上面两个方法都大同小异,思路都是在主应用一波引入,只是一个统一引入CSS,另一个统一引入 UI 库。
JS隔离
在single-spa使用
import singleSpaLeakedGlobals from 'single-spa-leaked-globals';
const leakedGlobalsLifecycles = singleSpaLeakedGlobals({
globalVariableNames: ['$', 'jQuery', '_'], // 新添加的全局变量
})
在 mount A 子应用时,正常添加全局变量,比如 jQuery 的 $
, lodash 的 _
。在 unmount A 子应用时,用一个对象记录之前给 window 添加的全局变量,并把 A 应用里添加 window 的变量都删掉。下一次再 mount A 应用时,把记录的全局变量重新加回来就好了。
但是,这个库的局限性在于:每个 url 只能加一个子 app,如果多个子 app 之间还是会访问同一个 window 对象,也因此会互相干扰,并不能做到完美的 JS 沙箱。
CSS隔离
而关于css的样式隔离,官方的建议是
- 使用 Scoped CSS
- 要是嫌麻烦,可以在子应用 Webpack 使用 PostCSS Prefix Selector 给样式自动加前缀
- 亦或者开发者自己用shadow-dom来处理…
why SystemJS
都2022年了,怎么还要在浏览器环境写 JS 呢?不上个 Webpack 都不好意思说自己是前端开发了…
那为什么 single-spa 还要推荐 SystemJS 呢?但是根据知乎上优质答主写代码的海怪猜测是因为 single-spa 希望主应用应该就一个空壳子,只需要管内容要放在哪个地方,所有的功能、交互都应该交由 index.html 来统一管理。(直接在html里写满script想想就可怕)
这也表明了:
资源罗列:在systemjs中,我们必须手动实现应用加载逻辑,挨个罗列子应用需要加载的资源,这在大型项目里是十分困难的(特别是使用了文件名hash时);另外它只能以js文件为入口,无法直接以html为入口,这使得嵌入子应用变得很困难,也正因此,single-spa不能直接加载jQuery应用。
资源加载优化:将整个微应用打包成一个 JS 文件,常见的打包优化基本上都没了,比如:按需加载、首屏资源加载优化、css 独立打包等优化措施。
js名称:项目发布以后出现了 bug ,修复之后需要更新上线,为了清除浏览器缓存带来的影响,一般文件名会带上 chunkcontent,微应用发布之后文件名(
js bundle
)都会发生变化,这时候还需要更新主应用中微应用配置,然后重新编译主应用然后发布,这是不能容忍的。- 你可以理解为每次文件名一变化:
jlkasjfdlkj.jalkjdsflk.js
,这样一来,每次子应用一发布,入口 JS 文件名肯定又要改了,导致主应用引入的 JS url 又得改了
- 你可以理解为每次文件名一变化:
所以,为了填平 single-spa 遗留下来的坑,阿里基于 single-spa 造出了 qiankun 微前端框架,真正实现了微前端的所有特性.
6.Qiankun
qiankun
是基于single-spa
开发的
Single-spa控制路由和应用入口 + qiankun JS、CSS隔离和应用通信 + import-html-entry控制应用加载(single-spa用的是system.js)
qiankun
基于single-spa
,加强了微应用集成能力,却抛弃了微模块的能力。所以,它们的区别就是微服务的粒度,qiankun的所能服务的粒度是应用级别,而single-spa
则是模块级别。它们都能将前端进行拆分,只是拆分的粒度不同罢了。
- 微应用加载器:“微”的粒度是应用,也就是
HTML
,它只能做到应用级别的分享 - 微模块加载器:“微”的粒度是模块,也就是
JS
模块,它能做到模块级别的分享
两个框架出现的背景:
qiankun:阿里内部有大量年久失修的项目,业务侧急需工具去把他们快速、安全的集成到一起。在这个角度,乾坤根本没有做模块联邦的需求,它的需求仅仅是如何快速、安全的把项目集成起来。所以乾坤是想做一个微前端工具。
single-spa:学习后端的微服务,实现前端的微服务化,让应用、组件以及逻辑都成为可共享的微服务,实现真正意义上的微前端。所以single-spa
是想做一个game-changer
。
css隔离
目前qiankun主要提供了两种样式隔离方案
一种是基于shadowDom的;
- 在配置的时候当启用
strictStyleIsolation
时,qiankun将采用shadowDom的方式进行样式隔离,即为子应用的根节点创建一个shadow root,整个应用的所有DOM将形成一棵shadow tree,它内部所有节点的样式对树外面的节点无效,所有就实现了样式隔离。 const supportShadowDOM = !!document.head.attachShadow || !!(document.head as any).createShadowRoot;
if (strictStyleIsolation) { if (!supportShadowDOM) { // 报错 // ... } else { // 清除原有的内容 const { innerHTML } = appElement; appElement.innerHTML = ''; let shadow: ShadowRoot; if (appElement.attachShadow) { // 添加 shadow DOM 节点 shadow = appElement.attachShadow({ mode: 'open' }); } else { // deprecated 的操作 // ... } // 在 shadow DOM 节点添加内容 shadow.innerHTML = innerHTML; } }
- 但是这种方案是存在缺陷的。因为某些UI框架可能会生成一些弹出框直接挂载到document.body下,此时由于脱离了shadow tree,所以它的样式仍然会对全局造成污染。
- 基于 ShadowDOM 的严格样式隔离并不是一个可以无脑使用的方案,大部分情况下都需要接入应用做一些适配后才能正常在 ShadowDOM 中运行起来(比如 react 场景下需要解决这些 问题,使用者需要清楚开启了
strictStyleIsolation
意味着什么。后续 qiankun 会提供更多官方实践文档帮助用户能快速的将应用改造成可以运行在 ShadowDOM 环境的微应用
- 在配置的时候当启用
另一种则是实验性的,思路类似于Vue中的scoped属性,给每个子应用的根节点添加一个特殊属性,用作对所有css选择器的约束。
qiankun首先判断当前sandbox是否支持scope css,支持的话才添加
export function isEnableScopedCSS(sandbox: FrameworkConfiguration['sandbox']) { if (typeof sandbox !== 'object') { return false; } if (sandbox.strictStyleIsolation) { return false; } return !!sandbox.experimentalStyleIsolation; }
在配置的时候当启用
experimentalStyleIsolation
。这种方案的策略是为子应用的根节点添加一个特定的随机属性// 假如子应用名字叫 child // 转换前 .app-main { font-size: 14px; } // 转换后 div[data-qiankun="child"] .app-main { font-size: 14px; }
shadowdom的css隔离开启方式:start的时候或者手动加载子应用的时候传入option,option包含sanbox属性,sanbox的
strictStyleIsolation
为truescope开启方式:start的时候或者手动加载子应用的时候传入option,option包含sanbox属性,sanbox的
experimentalStyleIsolation
为truestart({ sandbox: { strictStyleIsolation?: boolean, experimentalStyleIsolation?: true } }) loadMicroApp(app, { sandbox: { strictStyleIsolation?: boolean, experimentalStyleIsolation?: true } })
注意事项: 对于scope的css隔离方案,目前 @keyframes, @font-face, @import, @page 等规则不会支持 (i.e. 不会被改写)
js隔离
qiankun通过import-html-entry
,可以对html入口进行解析,并获得一个可以执行脚本的方法execScripts
(上面提及到的 execScript
)。
qiankun默认启用沙箱,也就是上面css隔离中 start里的那个sanbox选项,此时会开启js隔离严格模式 strictGlobal
第一步创建一个沙箱容器 sandboxContainer
三种沙箱:
首先初始化一个沙箱ProxySandbox,里面创造一个fakeWindow,它拥有window的所有属性,并且通过new Proxy返回一个fakeWindow的代理对象
SnapshotSandbox:
针对IE11不支持proxy,所以qiankun通过快照策略来隔离js,缺点是无法支持多实例场景。(可以理解为使用single-spa的js隔离方法)
if (window.Proxy) { sandbox = useLooseSandbox ? new LegacySandbox(appName, globalContext) : new ProxySandbox(appName, globalContext); } else { sandbox = new SnapshotSandbox(appName); }
class SnapshotSandbox { ... active() { // 记录当前快照 this.windowSnapshot = {} as Window; getKeys(window).forEach(key => { this.windowSnapshot[key] = window[key]; }) // 恢复之前的变更 getKeys(this.modifyPropsMap).forEach((key) => { window[key] = this.modifyPropsMap[key]; }); this.sandboxRunning = true; } inactive() { this.modifyPropsMap = {}; // 记录变更,恢复环境 getKeys(window).forEach((key) => { if (window[key] !== this.windowSnapshot[key]) { this.modifyPropsMap[key] = window[key]; window[key] = this.windowSnapshot[key]; } }); this.sandboxRunning = false; } }
LegacySandbox: 其实看到源码还有个LegacySandbox,不过后面废弃了,因为没有了sanbox配置没有了loose这个配置
const useLooseSandbox = typeof sandbox === 'object' && !!sandbox.loose;
const sandbox = useLooseSandbox ? new LegacySandbox(appName, globalContext) : new ProxySandbox(appName, globalContext);
执行js代码:
通过html-import-entry得到js执行函数: execScripts
我们可以看到他通过传入proxy,执行 getExecutableScript
得到js code,其中使用 function(){}(with(window){fn.bind(window.proxy)(window.proxy, window.proxy, window.proxy)})
var code = getExecutableScript(scriptSrc, rawCode, proxy, strictGlobal);
evalCode(scriptSrc, code);
function getExecutableScript(scriptSrc, scriptText, proxy, strictGlobal) {
var sourceUrl = isInlineCode(scriptSrc) ? '' : "//# sourceURL=".concat(scriptSrc, "\n"); // 通过这种方式获取全局 window,因为 script 也是在全局作用域下运行的,所以我们通过 window.proxy 绑定时也必须确保绑定到全局 window 上
// 否则在嵌套场景下, window.proxy 设置的是内层应用的 window,而代码其实是在全局作用域运行的,会导致闭包里的 window.proxy 取的是最外层的微应用的 proxy
var globalWindow = (0, eval)('window');
globalWindow.proxy = proxy; // TODO 通过 strictGlobal 方式切换 with 闭包,待 with 方式坑趟平后再合并
return strictGlobal ? ";(function(window, self, globalThis){with(window){;".concat(scriptText, "\n").concat(sourceUrl, "}}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);") : ";(function(window, self, globalThis){;".concat(scriptText, "\n").concat(sourceUrl, "}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);");
} // for prefetch
此时with语法内的window就是proxy:把解析出的scriptText(即脚本字符串)用with(window){}
包裹起来,然后把window.proxy作为函数的第一个参数传进来,所以with语法内的window实际上是window.proxy。此时我们的js代码执行时,全局变量都会被挂载到沙箱的proxy当中去。
然后再通过 evalCode函数全局执行js代码
export function evalCode(scriptSrc, code) {
var key = scriptSrc;
if (!evalCache[key]) {
var functionWrappedCode = "window.__TEMP_EVAL_FUNC__ = function(){".concat(code, "}");
(0, eval)(functionWrappedCode);
evalCache[key] = window.__TEMP_EVAL_FUNC__;
delete window.__TEMP_EVAL_FUNC__;
}
var evalFunc = evalCache[key];
evalFunc.call(window);
}
应用通信
一般来说,微前端中各个应用之前的通信都是比较少的。当然部分呢少量的通信是在所难免的,qiankun官方提供了一个简要的方案,思路是基于一个全局的globalState对象。这个对象由基座应用负责创建,内部包含一组用于通信的变量,以及两个分别用于修改变量值和监听变量变化的方法:setGlobalState和onGlobalStateChange。
飞冰JS隔离
飞冰也是一款阿里的微前端方案:对于css污染(css隔离),由于微应用是不同的路由,所以暂时没有考虑(实验性的shadow dom方案)
js隔离
- 规范
- 微应用避免改变全局状态
- 基于 Proxy 的运行沙箱
参考于:
更多关于微前端的相关介绍,推荐大家可以去看这几篇文章:(qiankun官网的推荐)
为什么不使用iframe:https://www.yuque.com/kuitos/gky7yw/gesexv