微前端框架 Qiankun 沙箱原理
为什么需要微前端
微前端其实诞生两个大的背景下,在提倡拥抱变化的前端社区可以看到新的框架、技术、概念层出不穷,并且随着WEB标准的演进,前端应用已经具备更好的性能、更快的开发效率。但随着而来的是应用的复杂程度更高、涉及的团队规模更广、更高的性能要求,应用复杂度已经成为阻塞业务发展的重要瓶颈。如何让现有系统拥抱最新技术提高生产力、并且解耦单体应用,是现在前端工程不得不面临的挑战。
如果你遇到以下的情况,可能你需要微前端:
- 你的单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用后,随之而来的应用不可维护的问题
- 作为一个门户网站,需要集成很多的系统,这些系统由不同的团队维护、有风格各异的代码、有形形色色的技术栈,为了聚合只能采取iframe或者使用MPA的形式进行聚合
为什么需要沙箱
在微前端的场景,由于多个独立的应用被组织到了一起,在没有类似iframe的原生隔离下,势必会出现冲突,如全局变量冲突、样式冲突,这些冲突可能会导致应用样式异常,甚至功能不可用。所以想让微前端达到生产可用的程度,让每个子应用之间达到一定程度隔离的沙箱机制是必不可少的。
如何实现一个沙箱
手动执行代码
常规的脚本加载,是通过script标签去执行的,要实现沙箱,因为需要控制沙箱的开启和关闭,我们就需要精确掌握脚本的执行时机,所以我们需要寻找一种合适的能手动执行代码的方法
首先我们想到的是eval,由于eval有安全、性能等问题,同时也不利于调试,所以在以前我们听到的都是不推荐使用eval这个api。
但是在微前端的沙箱场景,eval确实是一个比较好的解决方案,比如qiankun就采用了eval作为代码执行器。
new Function通过传入一个string作为函数的的主体同时返回一个新函数,可以作为eval的一个替代品
对比eval,有两点比较重要的不同:
- 不能访问当前环境的变量,但是可以访问全局变量,安全性更高
- 仅需要处理传入的字符串一次,后面重复执行都是同一个函数,而eval需要每次都处理,性能更高
快照沙箱
顾名思义,即在某个阶段给当前的运行环境打一个快照,再在需要的时候把快照恢复,从而实现隔离。
这个东西的原理和操作系统中的CPU进程切换很像,每个进程获得CPU使用权时把PCB(进程控制块)中的上下文装载到对应的寄存器中,然后CPU开始继续执行,等到失去使用权时再将当前寄存器中的信息重新保存回PCB中。快照沙箱也是这样,对于每一个子应用,运行时将其内部保存的上下文加载到对应的变量上,销毁时再将当前浏览器环境中各个变量的值保存到快照中。
我们来看一下qiankun中快照沙箱的源码,源码上添加了我的注释
1 | /** |
代理沙箱
代理沙箱主要是通过为每个沙盒创建一个fakeWindow,然后为这个fakeWindow设置代理,通过代理来访问这个fakeWindow,所以它的源码主要分为两部分:
- 通过
createFakeWindow
函数创建一个fakeWindow,这个fakeWindow上只会克隆window对象上可以修改的属性 - 创建ProxySandbox类,内部使用
createFakeWindow
创建的fakeWindow
我们先看一下createFakeWindow
,说起来这个函数做的事情就是它注释上说的:copy the non-configurable property of global to fakeWindow
1 | function createFakeWindow(globalContext: Window) { |
然后是ProxySandbox的实现了,比较关键的就是它的构造函数中通过proxy劫持了对fakeWindow的set方法,然后通过这种方式去统计有那些属性发生了改变,同时如果是globalVariableWhiteList
中的成员,那么就将其descriptor存储到globalWhitelistPrevDescriptor
中,在inactive的时候恢复
我们首先看下这个globalVariableWhiteList
是什么
1 | const variableWhiteListInDev = |
然后看一下ProxySandbox的大致结构,这里我先把Proxy隐藏,后面再一个个介绍:
1 | export default class ProxySandbox implements SandBox { |
这里我们暂时看不出这latestSetProp, globalWhitelistPrevDescriptor
几个变量的作用,我们继续看一下Proxy中的各个配置:
set
set拦截其中会检测fakeWindow和globalContext是否都有当前要修改的属性,如果有,那么直接给fakeWindow上的该属性赋值,如果没有,只有globalContext上有,看一下这个属性是否是writable或者有set方法,只有这两个有其一,才表明这个属性本身是可以修改的,我们给fakeWindow上添加这个属性并修改才是有意义的
同时set方法中也会查看当前修改的属性是否是globalVariableWhiteList中的一个,如果是,将其descriptor添加到globalWhitelistPrevDescriptor中
1 | set: (target: FakeWindow, p: PropertyKey, value: any): boolean => { |
get
这个get其实就是做了很多的检测,比如如果要获取的属性是window或者self,这种,返回proxy本身这种,具体很多分支大家可以看一下下面的代码
1 | get: (target: FakeWindow, p: PropertyKey): any => { |
其他
1 | has(target: FakeWindow, p: string | number | symbol): boolean { |
patchers
劫持interval
这部分功能主要是用于拦截sandbox运行期间的定时器,收集有哪些定时器,并能够在sandbox退出事把所有定时器清空
1 | import { noop } from 'lodash'; |
劫持eventListener
这个的功能与劫持interval相同,这里是劫持所有的eventListener,收集所有的事件监听器,并提供函数能在sandbox退出时清空所有的事件监听器
1 | import { noop } from 'lodash'; |
劫持historyListener(针对umi框架)
1 | import { isFunction, noop } from 'lodash'; |
patch sandbox
1 | export function patchLooseSandbox( |
patchers 集成导出
简单总结下patcher是做什么的:
- 执行过程中会对一些全局的定时器或者监听器或者某些全局变量进行拦截统计,我们统称为副作用,然后返回free函数用于子应用卸载时(即unmount)调用
- free函数内部会对上一部统计到的副作用进行清空,同时如果有部分副作用是需要下次子应用重新激活时重新启动的,放到free函数最终返回的rebuild中,最终会在mount时调用rebuild
1 | import type { Freer, SandBox } from '../../interfaces'; |
Sandbox集合
1 | import type { Freer, Rebuilder, SandBox } from '../interfaces'; |