概述

乾坤相对于single-spa做了两件重要的事: 加载资源,资源隔离
资源隔离分为:js隔离和css隔离
加载资源使用的是import-html-entry库,通过fetch拿到html资源,然后通过eval执行js代码,动态加载依赖的js和css文件

核心思想

监听路由变化

  • 监听hashChange,popState事件
  • 拦截window.history.pushState,window.history.replaceState

    匹配子路由

    根据路由匹配到对应的子应用

    加载子应用

    通过import-html-entry获取资源,拿到template和execScripts,以及mount和unmount,bootstrap生命周期函数

    渲染子应用

    js隔离

    SnapshotSandbox

    ```javascript class SnapshotSandBox{ windowSnapshot = {}; modifyPropsMap = {}; active(){
    1. for(const prop in window){
    2. this.windowSnapshot[prop] = window[prop];
    3. }
    4. Object.keys(this.modifyPropsMap).forEach(prop=>{
    5. window[prop] = this.modifyPropsMap[prop];
    6. });
    } inactive(){
    1. for(const prop in window){
    2. if(window[prop] !== this.windowSnapshot[prop]){
    3. this.modifyPropsMap[prop] = window[prop];
    4. window[prop] = this.windowSnapshot[prop];
    5. }
    6. }
    } } // 验证: 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();

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

  1. 从上面的代码可以看出,快照沙箱的核心逻辑很简单,就是在激活沙箱和沙箱失活的时候各做两件事情。
  2. > 从上面的代码可以看出,快照沙箱的核心逻辑很简单,就是在激活沙箱和沙箱失活的时候各做两件事情。
  3. 在沙箱激活的时候:
  4. - 记录window当时的状态(我们把这个状态称之为快照,也就是快照沙箱这个名称的来源);
  5. - 恢复上一次沙箱失活时记录的沙箱运行过程中对window做的状态改变,也就是上一次沙箱激活后对window做了哪些改变,现在也保持一样的改变。
  6. 在沙箱失活的时候:
  7. - 记录window上有哪些状态发生了变化(沙箱自激活开始,到失活的这段时间);
  8. - 清除沙箱在激活之后在window上改变的状态,从代码可以看出,就是让window此时的属性状态和刚激活时候的window的属性状态进行对比,不同的属性状态就以快照为准,恢复到未改变之前的状态。
  9. 从上面可以看出,快照沙箱存在两个重要的问题:
  10. - 会改变全局window的属性,如果同时运行多个微应用,多个应用同时改写window上的属性,势必会出现状态混乱,这也就是为什么快照沙箱无法支持多各微应用同时运行的原因。关于这个问题,下文中**支持多应用的代理沙箱**可以很好的解决这个问题;
  11. - 会通过for(prop in window){}的方式来遍历window上的所有属性,window属性众多,这其实是一件很耗费性能的事情。关于这个问题**支持单应用的代理沙箱**和**支持多应用的代理沙箱**都可以规避。
  12. <a name="Fm8wE"></a>
  13. ## LegacySandbox
  14. ```javascript
  15. class LegacySandBox{
  16. addedPropsMapInSandbox = new Map();
  17. modifiedPropsOriginalValueMapInSandbox = new Map();
  18. currentUpdatedPropsValueMap = new Map();
  19. proxyWindow;
  20. setWindowProp(prop, value, toDelete = false){
  21. if(value === undefined && toDelete){
  22. delete window[prop];
  23. }else{
  24. window[prop] = value;
  25. }
  26. }
  27. active(){
  28. this.currentUpdatedPropsValueMap.forEach((value, prop)=>this.setWindowProp(prop, value));
  29. }
  30. inactive(){
  31. this.modifiedPropsOriginalValueMapInSandbox.forEach((value, prop)=>this.setWindowProp(prop, value));
  32. this.addedPropsMapInSandbox.forEach((_, prop)=>this.setWindowProp(prop, undefined, true));
  33. }
  34. constructor(){
  35. const fakeWindow = Object.create(null);
  36. this.proxyWindow = new Proxy(fakeWindow,{
  37. set:(target, prop, value, receiver)=>{
  38. const originalVal = window[prop];
  39. if(!window.hasOwnProperty(prop)){
  40. this.addedPropsMapInSandbox.set(prop, value);
  41. }else if(!this.modifiedPropsOriginalValueMapInSandbox.has(prop)){
  42. this.modifiedPropsOriginalValueMapInSandbox.set(prop, originalVal);
  43. }
  44. this.currentUpdatedPropsValueMap.set(prop, value);
  45. window[prop] = value;
  46. },
  47. get:(target, prop, receiver)=>{
  48. return target[prop];
  49. }
  50. });
  51. }
  52. }
  53. // 验证:
  54. let legacySandBox = new LegacySandBox();
  55. legacySandBox.active();
  56. legacySandBox.proxyWindow.city = 'Beijing';
  57. console.log('window.city-01:', window.city);
  58. legacySandBox.inactive();
  59. console.log('window.city-02:', window.city);
  60. legacySandBox.active();
  61. console.log('window.city-03:', window.city);
  62. legacySandBox.inactive();
  63. // 输出:
  64. // window.city-01: Beijing
  65. // window.city-02: undefined
  66. // window.city-03: Beijing

从上面的代码可以看出,其实现的功能和快照沙箱是一模一样的,不同的是,通过三个变量来记住沙箱激活后window发生变化过的所有属性,这样在后续的状态还原时候就不再需要遍历window的所有属性来进行对比,提升了程序运行的性能。但是这仍然改变不了这种机制仍然污染了window的状态的事实,因此也就无法承担起同时支持多个微应用运行的任务。

ProxySandbox

  1. class ProxySandBox{
  2. proxyWindow;
  3. isRunning = false;
  4. active(){
  5. this.isRunning = true;
  6. }
  7. inactive(){
  8. this.isRunning = false;
  9. }
  10. constructor(){
  11. const fakeWindow = Object.create(null);
  12. this.proxyWindow = new Proxy(fakeWindow,{
  13. set:(target, prop, value, receiver)=>{
  14. if(this.isRunning){
  15. target[prop] = value;
  16. }
  17. },
  18. get:(target, prop, receiver)=>{
  19. return prop in target ? target[prop] : window[prop];
  20. }
  21. });
  22. }
  23. }
  24. // 验证:
  25. let proxySandBox1 = new ProxySandBox();
  26. let proxySandBox2 = new ProxySandBox();
  27. proxySandBox1.active();
  28. proxySandBox2.active();
  29. proxySandBox1.proxyWindow.city = 'Beijing';
  30. proxySandBox2.proxyWindow.city = 'Shanghai';
  31. console.log('active:proxySandBox1:window.city:', proxySandBox1.proxyWindow.city);
  32. console.log('active:proxySandBox2:window.city:', proxySandBox2.proxyWindow.city);
  33. console.log('window:window.city:', window.city);
  34. proxySandBox1.inactive();
  35. proxySandBox2.inactive();
  36. console.log('inactive:proxySandBox1:window.city:', proxySandBox1.proxyWindow.city);
  37. console.log('inactive:proxySandBox2:window.city:', proxySandBox2.proxyWindow.city);
  38. console.log('window:window.city:', window.city);
  39. // 输出:
  40. // active:proxySandBox1:window.city: Beijing
  41. // active:proxySandBox2:window.city: Shanghai
  42. // window:window.city: undefined
  43. // inactive:proxySandBox1:window.city: Beijing
  44. // inactive:proxySandBox2:window.city: Shanghai
  45. // window:window.city: undefined

从上面的代码可以发现,ProxySandbox,完全不存在状态恢复的逻辑,同时也不需要记录属性值的变化,因为所有的变化都是沙箱内部的变化,和window没有关系,window上的属性至始至终都没有受到过影响。我们可能会问,ProxySandbox已经这么好了,性能优良还支持多个微应用同时运行,那自然也支持单个微应用运行,那LegacySandbox存在还有什么意义呢,这个问题问得很好,其实本身在未来存在的意义也不大了,只不过是因为历史原因还在服役罢了,从Legacy这个单词就已经能推断出LegacySandbox在乾坤中的位置。我们可能还会继续问,那SnapshotSandbox存在还有什么意义呢,这个还真有不小作用,Proxy是新ES6的新事物,低版本浏览器无法兼容所以SnapshotSandbox还会长期存在。虽然这里极简版本逻辑很少,但是由于ProxySandbox要支持多个微应用运行,里面的逻辑会SnapshotsSandbox、LegacySandbox的都要丰富一些。
其实到了这里,如果读者朋友已经理解了上面的思路,就可以说已经理解了乾坤的Js隔离机制。下面我们来看看乾坤的源码具体是怎么实现的这三个沙箱机制。

css隔离

Shadow Dom样式隔离:strictStyleIsolation

为子应用增加特殊规则的选择器来限定影响范围:experimentalStyleIsolation

路由实现原理

window.POWERED_BY_QIANKUN

在beforeReload将POWERED_BY_QIANKUN改成true,然后走mount生命周期

window.INJECTED_PUBLIC_PATH_BY_QIANKUN

针对图片等路径问题INJECTED_PUBLIC_PATH_BY_QIANKUN