# single-spa-source **Repository Path**: heIIoworld/single-spa-source ## Basic Information - **Project Name**: single-spa-source - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-11-17 - **Last Updated**: 2023-11-28 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ### single-spa - registerApplication : 注册应用实现应用的加载(获取钩子) - start : 启动应用和执行用户的钩子(可能钩子不存在,需要去加载) ### 沙箱 乾坤是建立在single-spa基础上,相较于single-spa,乾坤做了两件重要的事情,其一是加载资源,第二是进行资源隔离。而资源隔离又分为Js资源隔离和css资源隔离。 #### 乾坤Js隔离机制的发展史 我们把Js隔离机制常常称作沙箱,事实上,乾坤有三种Js隔离机制,并且在源代码中也是以 SnapshotSandbox、LegacySandbox、ProxySandbox三个类名来指代三种不同的隔离机制。下文我们统一以快照沙箱、支持单应用的代理沙箱、支持多应用的代理沙箱,来代表这三种不同的Js隔离机制。那么问题来了,隔离就隔离,怎么有这么多沙箱?一开始乾坤也只有一种沙箱叫“快照沙箱”,也就是由SnapshotSandbox类来实现的沙箱。这个沙箱有个缺点,就是需要遍历window上的所有属性,性能较差。随着ES6的普及,利用Proxy可以比较良好的解决这个问题,这就诞生了LegacySandbox,可以实现和快照沙箱一样的功能,但是却性能更好,和SnapshotSandbox一样,由于会污染全局的window,LegacySandbox也仅仅允许页面同时运行一个微应用,所以我们也称LegacySandbox为支持单应用的代理沙箱。从LegacySandbox这个类名可以看出,一开始肯定是不叫LegacySandbox,是因为有了更好的机制,才将这个名字强加给它了。那这个更好的机制是什么呢,就是ProxySandbox,它可以支持一个页面运行多个微应用,因此我们称ProxySandbox为支持多应用的代理沙箱。事实上,LegacySandbox在未来应该会消失,因为LegacySandbox可以做的事情,ProxySandbox都可以做,而SanpsshotSandbox因为向下兼容的原因反而会和ProxySandbox长期并存。下面我们就编码实现这三种沙箱机制的核心逻辑。 #### 编码实现三个沙箱的核心逻辑 ##### 1、快照沙箱-极简版 ```js class SnapshotSandBox{ windowSnapshot = {}; modifyPropsMap = {}; active(){ for(const prop in window){ this.windowSnapshot[prop] = window[prop]; } Object.keys(this.modifyPropsMap).forEach(prop=>{ window[prop] = this.modifyPropsMap[prop]; }); } inactive(){ for(const prop in window){ if(window[prop] !== this.windowSnapshot[prop]){ this.modifyPropsMap[prop] = window[prop]; window[prop] = this.windowSnapshot[prop]; } } } } // 验证: let snapshotSandBox = new SnapshotSandBox(); snapshotSandBox.active(); window.city = 'Beijing'; console.log("window.city-01:", window.city); snapshotSandBox.inactive(); console.log("window.city-02:", window.city); snapshotSandBox.active(); console.log("window.city-03:", window.city); snapshotSandBox.inactive(); //输出: //window.city-01: Beijing //window.city-02: undefined //window.city-03: Beijing ``` 从上面代码可以看出快照沙箱的核心逻辑很简单,就是在激活沙箱和沙箱失活的时候各做两件事情。 - 沙箱激活 就是此时我们的微应用处于运行中,这个阶段有可能对window上的属性进行操作改变; - 沙箱失活 就是此时我们的微应用已经停止了对window的影响 在沙箱激活的时候: - 记录window当时的状态(我们把这个状态称之为快照,也就是快照沙箱这个名称的来源); - 恢复上一次沙箱失活时记录的沙箱运行过程中对window做的状态改变,也就是上一次沙箱激活后对window做了哪些改变,现在也保持一样的改变。 在沙箱失活的时候: - 记录window上有哪些状态发生了变化(沙箱自激活开始,到失活的这段时间); - 清除沙箱在激活之后在window上改变的状态,从代码可以看出,就是让window此时的属性状态和刚激活时候的window的属性状态进行对比,不同的属性状态就以快照为准,恢复到未改变之前的状态。 从上面可以看出,快照沙箱存在两个重要的问题: - 会改变全局window的属性,如果同时运行多个微应用,多个应用同时改写window上的属性,势必会出现状态混乱,这也就是为什么快照沙箱无法支持多各微应用同时运行的原因。关于这个问题,下文中支持多应用的代理沙箱可以很好的解决这个问题; - 会通过for(prop in window){}的方式来遍历window上的所有属性,window属性众多,这其实是一件很耗费性能的事情。关于这个问题支持单应用的代理沙箱和支持多应用的代理沙箱都可以规避。 #### 2、支持单应用的代理沙箱-极简版 ```js class LegacySandBox{ addedPropsMapInSandbox = new Map(); modifiedPropsOriginalValueMapInSandbox = new Map(); currentUpdatedPropsValueMap = new Map(); proxyWindow; setWindowProp(prop, value, toDelete = false){ if(value === undefined && toDelete){ delete window[prop]; }else{ window[prop] = value; } } active(){ this.currentUpdatedPropsValueMap.forEach((value, prop)=>this.setWindowProp(prop, value)); } inactive(){ this.modifiedPropsOriginalValueMapInSandbox.forEach((value, prop)=>this.setWindowProp(prop, value)); this.addedPropsMapInSandbox.forEach((_, prop)=>this.setWindowProp(prop, undefined, true)); } constructor(){ const fakeWindow = Object.create(null); this.proxyWindow = new Proxy(fakeWindow,{ set:(target, prop, value, receiver)=>{ const originalVal = window[prop]; if(!window.hasOwnProperty(prop)){ this.addedPropsMapInSandbox.set(prop, value); }else if(!this.modifiedPropsOriginalValueMapInSandbox.has(prop)){ this.modifiedPropsOriginalValueMapInSandbox.set(prop, originalVal); } this.currentUpdatedPropsValueMap.set(prop, value); window[prop] = value; }, get:(target, prop, receiver)=>{ return target[prop]; } }); } } // 验证: let legacySandBox = new LegacySandBox(); legacySandBox.active(); legacySandBox.proxyWindow.city = 'Beijing'; console.log('window.city-01:', window.city); legacySandBox.inactive(); console.log('window.city-02:', window.city); legacySandBox.active(); console.log('window.city-03:', window.city); legacySandBox.inactive(); // 输出: // window.city-01: Beijing // window.city-02: undefined // window.city-03: Beijing ``` 从上面的代码可以看出,其实现的功能和快照沙箱是一模一样的,不同的是,通过三个变量来记住沙箱激活后window发生变化过的所有属性,这样在后续的状态还原时候就不再需要遍历window的所有属性来进行对比,提升了程序运行的性能。但是这仍然改变不了这种机制仍然污染了window的状态的事实,因此也就无法承担起同时支持多个微应用运行的任务。 #### 3、支持多应用的代理沙箱-极简版 ```js class ProxySandBox{ proxyWindow; isRunning = false; active(){ this.isRunning = true; } inactive(){ this.isRunning = false; } constructor(){ const fakeWindow = Object.create(null); this.proxyWindow = new Proxy(fakeWindow,{ set:(target, prop, value, receiver)=>{ if(this.isRunning){ target[prop] = value; } }, get:(target, prop, receiver)=>{ return prop in target ? target[prop] : window[prop]; } }); } } // 验证: let proxySandBox1 = new ProxySandBox(); let proxySandBox2 = new ProxySandBox(); proxySandBox1.active(); proxySandBox2.active(); proxySandBox1.proxyWindow.city = 'Beijing'; proxySandBox2.proxyWindow.city = 'Shanghai'; console.log('active:proxySandBox1:window.city:', proxySandBox1.proxyWindow.city); console.log('active:proxySandBox2:window.city:', proxySandBox2.proxyWindow.city); console.log('window:window.city:', window.city); proxySandBox1.inactive(); proxySandBox2.inactive(); console.log('inactive:proxySandBox1:window.city:', proxySandBox1.proxyWindow.city); console.log('inactive:proxySandBox2:window.city:', proxySandBox2.proxyWindow.city); console.log('window:window.city:', window.city); // 输出: // active:proxySandBox1:window.city: Beijing // active:proxySandBox2:window.city: Shanghai // window:window.city: undefined // inactive:proxySandBox1:window.city: Beijing // inactive:proxySandBox2:window.city: Shanghai // window:window.city: undefined ``` 从上面的代码可以发现,ProxySandbox,完全不存在状态恢复的逻辑,同时也不需要记录属性值的变化,因为所有的变化都是沙箱内部的变化,和window没有关系,window上的属性至始至终都没有受到过影响。我们可能会问,ProxySandbox已经这么好了,性能优良还支持多个微应用同时运行,那自然也支持单个微应用运行,那LegacySandbox存在还有什么意义呢,这个问题问得很好,其实本身在未来存在的意义也不大了,只不过是因为历史原因还在服役罢了,从Legacy这个单词就已经能推断出LegacySandbox在乾坤中的位置。我们可能还会继续问,那SnapshotSandbox存在还有什么意义呢,这个还真有不小作用,Proxy是新ES6的新事物,低版本浏览器无法兼容所以SnapshotSandbox还会长期存在。虽然这里极简版本逻辑很少,但是由于ProxySandbox要支持多个微应用运行,里面的逻辑会SnapshotsSandbox、LegacySandbox的都要丰富一些。 其实到了这里,如果读者朋友已经理解了上面的思路,就可以说已经理解了乾坤的Js隔离机制。下面我们来看看乾坤的源码具体是怎么实现的这三个沙箱机制。 ### RegExp RegExp.$1是RegExp的一个属性,指的是与正则表达式匹配的第一个子匹配(以括号为标志)字符串,以此类推,RegExp.$2,RegExp.$3,..RegExp.$99总共可以有99个匹配 如果你直接在控制台打印RegExp, 出现的一定是一个空字符串: ""。那么, 我们在什么时候可以使用RegExp.$1呢? 其实RegExp这个对象会在我们调用了正则表达式的方法后, 自动将最近一次的结果保存在里面, 所以如果我们在使用正则表达式时, 有用到分组, 那么就可以直接在调用完以后直接使用RegExp.$xx来使用捕获到的分组内容, 如下: ```js const r = /^(\d{4})-(\d{1,2})-(\d{1,2})$/ r.exec('2019-10-08') console.log(RegExp.$1) // 2019 console.log(RegExp.$2) // 10 console.log(RegExp.$3) // 08 ```