Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

qiankun 2.x 运行时沙箱 源码分析 - 掘金 #22

Open
zepang opened this issue Jan 9, 2022 · 0 comments
Open

qiankun 2.x 运行时沙箱 源码分析 - 掘金 #22

zepang opened this issue Jan 9, 2022 · 0 comments

Comments

@zepang
Copy link
Owner

zepang commented Jan 9, 2022

沙箱 这个词想必大家应该不陌生,即使陌生,读完这篇文章也就不那么陌生了

沙箱 (Sandboxie) ,又叫沙盘,即是一个虚拟系统程序,允许你在沙盘环境中运行浏览器或其他程序,因此运行所产生的变化可以随后删除。它创造了一个类似沙盒的独立作业环境,在其内部运行的程序并不能对硬盘产生永久性的影响。 在网络安全中,沙箱指在隔离环境中,用以测试不受信任的文件或应用程序等行为的工具

而今天要说的沙箱来自 qiankun 的实现,是为了解决微前端方案中的隔离问题,qiankun 目前可以说的最好的 微前端 实现方案吧,它基于 single-spa 做了二次封装,解决了 single-spa 遗留的众多问题, 运行时沙箱 就是其中之一

single-spa 虽好,但却存在一些需要在框架层面解决但却没有解决的问题,比如为每个微应用提供一个干净、独立的运行环境

JS 全局对象污染是一个很常见的现象,比如:微应用 A 在全局对象上添加了一个自己特有的属性 window.A,这时候切换到微应用 B,这时候如何保证 window对象是干净的呢?答案就是 qiankun 实现的 运行时沙箱

先上总结,运行时沙箱分为 JS 沙箱样式沙箱

JS 沙箱

JS 沙箱,通过 proxy 代理 window 对象,记录 window 对象上属性的增删改查

  • 单例模式

    直接代理了原生 window 对象,记录原生 window 对象的增删改查,当 window 对象激活时恢复 window 对象到上次即将失活时的状态,失活时恢复 window 对象到初始初始状态

  • 多例模式

    代理了一个全新的对象,这个对象是复制的 window 对象的一部分不可配置属性,所有的更改都是基于这个 fakeWindow 对象,从而保证多个实例之间属性互不影响

将这个 proxy 作为微应用的全局对象,所有的操作都在这个 proxy 对象上,这就是 JS 沙箱的原理

样式沙箱

通过增强多例模式下的 createElement 方法,负责创建元素并劫持 script、link、style 三个标签的创建动作

增强 appendChild、insertBefore 方法,负责添加元素,并劫持 script、link、style 三个标签的添加动作,根据是否是主应用调用决定标签是插入到主应用还是微应用,并且将 proxy 对象传递给微应用,作为其全局对象,以达到 JS 隔离的目的

初始化完成后返回一个 free 函数,会在微应用卸载时被调用,负责清除 patch、缓存动态添加的样式(因为微应用被卸载后所有的相关 DOM 元素都会被删掉)

free 函数执行完成后返回 rebuild 函数,在微应用重新挂载时会被调用,负责向微应用添加刚才缓存的动态样式

其实严格来说这个样式沙箱有点名不副实,真正的样式隔离是 严格样式隔离模式 和 scoped css 模式 提供的,当然如果开启了 scoped css,样式沙箱中动态添加的样式也会经过 scoped css 的处理

回到正题,样式沙箱实际做的事情其实很简单,就是将动态添加的 script、link、style 这三个元素插入到对的位置,属于主应用的插入主应用,属于微应用的插入到对应的微应用中,方便微应用卸载的时候一起删除,

当然样式沙箱还额外做了两件事:

  • 在卸载之前为动态添加样式做缓存,在微应用重新挂载时再插入到微应用内
  • 将 proxy 对象传递给 execScripts 函数,将其设置为微应用的执行上下文

以上内容就是对运行时沙箱的一个总结,更加详细的实现过程,可继续阅读下面的源码分析部分

接下来就进入令人头昏脑胀的源码分析部分,说实话,运行时沙箱这段代码还是有一些难度的,我在阅读 qiankun 源码的时候,这部分内容反反复复阅读了好几遍,github

入口位置 - createSandbox

export function createSandbox(
  appName: string,
  elementGetter: () => HTMLElement | ShadowRoot,
  singular: boolean,
  scopedCSS: boolean,
  excludeAssetFilter?: (url: string) => boolean,
) {
  
  let sandbox: SandBox;
  if (window.Proxy) {
    sandbox = singular ? new LegacySandbox(appName) : new ProxySandbox(appName);
  } else {
    
    sandbox = new SnapshotSandbox(appName);
  }

  
  const bootstrappingFreers = patchAtBootstrapping(
    appName,
    elementGetter,
    sandbox,
    singular,
    scopedCSS,
    excludeAssetFilter,
  );
  
  
  let mountingFreers: Freer[] = [];

  let sideEffectsRebuilders: Rebuilder[] = [];

  return {
    proxy: sandbox.proxy,

    
    async mount() {
      

      
      sandbox.active();

      const sideEffectsRebuildersAtBootstrapping = sideEffectsRebuilders.slice(0, bootstrappingFreers.length);
      const sideEffectsRebuildersAtMounting = sideEffectsRebuilders.slice(bootstrappingFreers.length);

      
      if (sideEffectsRebuildersAtBootstrapping.length) {
        
        sideEffectsRebuildersAtBootstrapping.forEach(rebuild => rebuild());
      }

      
      
      mountingFreers = patchAtMounting(appName, elementGetter, sandbox, singular, scopedCSS, excludeAssetFilter);

      
      
      
      if (sideEffectsRebuildersAtMounting.length) {
        sideEffectsRebuildersAtMounting.forEach(rebuild => rebuild());
      }

      
      sideEffectsRebuilders = [];
    },

    
    
    async unmount() {
      
      
      
      sideEffectsRebuilders = [...bootstrappingFreers, ...mountingFreers].map(free => free());

      sandbox.inactive();
    },
  };
}

JS 沙箱

SingularProxySandbox 单例 JS 沙箱

export default class SingularProxySandbox implements SandBox {
  
  private addedPropsMapInSandbox = new Map<PropertyKey, any>();

  
  private modifiedPropsOriginalValueMapInSandbox = new Map<PropertyKey, any>();

  
  private currentUpdatedPropsValueMap = new Map<PropertyKey, any>();

  name: string;

  proxy: WindowProxy;

  type: SandBoxType;

  sandboxRunning = true;

  
  active() {
    
    if (!this.sandboxRunning) {
      this.currentUpdatedPropsValueMap.forEach((v, p) => setWindowProp(p, v));
    }

    
    this.sandboxRunning = true;
  }

  
  inactive() {
    
    if (process.env.NODE_ENV === 'development') {
      console.info(`[qiankun:sandbox] ${this.name} modified global properties restore...`, [
        ...this.addedPropsMapInSandbox.keys(),
        ...this.modifiedPropsOriginalValueMapInSandbox.keys(),
      ]);
    }

    
    
    this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => setWindowProp(p, v));
    
    this.addedPropsMapInSandbox.forEach((_, p) => setWindowProp(p, undefined, true));

    
    this.sandboxRunning = false;
  }

  constructor(name: string) {
    this.name = name;
    this.type = SandBoxType.LegacyProxy;
    const { addedPropsMapInSandbox, modifiedPropsOriginalValueMapInSandbox, currentUpdatedPropsValueMap } = this;

    const self = this;
    const rawWindow = window;
    const fakeWindow = Object.create(null) as Window;

    const proxy = new Proxy(fakeWindow, {
      set(_: Window, p: PropertyKey, value: any): boolean {
        if (self.sandboxRunning) {
          if (!rawWindow.hasOwnProperty(p)) {
            
            addedPropsMapInSandbox.set(p, value);
          } else if (!modifiedPropsOriginalValueMapInSandbox.has(p)) {
            
            const originalValue = (rawWindow as any)[p];
            modifiedPropsOriginalValueMapInSandbox.set(p, originalValue);
          }

          currentUpdatedPropsValueMap.set(p, value);
          
          
          (rawWindow as any)[p] = value;

          return true;
        }

        if (process.env.NODE_ENV === 'development') {
          console.warn(`[qiankun] Set window.${p.toString()} while sandbox destroyed or inactive in ${name}!`);
        }

        
        return true;
      },

      get(_: Window, p: PropertyKey): any {
        
        
        
        if (p === 'top' || p === 'parent' || p === 'window' || p === 'self') {
          return proxy;
        }

        
        const value = (rawWindow as any)[p];
        return getTargetValue(rawWindow, value);
      },

      
      
      has(_: Window, p: string | number | symbol): boolean {
        return p in rawWindow;
      },
    });

    this.proxy = proxy;
  }
}


function setWindowProp(prop: PropertyKey, value: any, toDelete?: boolean) {
  if (value === undefined && toDelete) {
    
    delete (window as any)[prop];
  } else if (isPropConfigurable(window, prop) && typeof prop !== 'symbol') {
    
    Object.defineProperty(window, prop, { writable: true, configurable: true });
    (window as any)[prop] = value;
  }
}

ProxySandbox 多例 JS 沙箱

let activeSandboxCount = 0;


export default class ProxySandbox implements SandBox {
  
  private updatedValueSet = new Set<PropertyKey>();

  name: string;

  type: SandBoxType;

  proxy: WindowProxy;

  sandboxRunning = true;

  
  active() {
    
    if (!this.sandboxRunning) activeSandboxCount++;
    this.sandboxRunning = true;
  }

  
  inactive() {
    if (process.env.NODE_ENV === 'development') {
      console.info(`[qiankun:sandbox] ${this.name} modified global properties restore...`, [
        ...this.updatedValueSet.keys(),
      ]);
    }

    
    clearSystemJsProps(this.proxy, --activeSandboxCount === 0);

    this.sandboxRunning = false;
  }

  constructor(name: string) {
    this.name = name;
    this.type = SandBoxType.Proxy;
    const { updatedValueSet } = this;

    const self = this;
    const rawWindow = window;
    
    const { fakeWindow, propertiesWithGetter } = createFakeWindow(rawWindow);

    const descriptorTargetMap = new Map<PropertyKey, SymbolTarget>();
    
    const hasOwnProperty = (key: PropertyKey) => fakeWindow.hasOwnProperty(key) || rawWindow.hasOwnProperty(key);

    const proxy = new Proxy(fakeWindow, {
      set(target: FakeWindow, p: PropertyKey, value: any): boolean {
        
        if (self.sandboxRunning) {
          
          
          target[p] = value;
          
          updatedValueSet.add(p);

          
          interceptSystemJsProps(p, value);

          return true;
        }

        if (process.env.NODE_ENV === 'development') {
          console.warn(`[qiankun] Set window.${p.toString()} while sandbox destroyed or inactive in ${name}!`);
        }

        
        return true;
      },

      
      get(target: FakeWindow, p: PropertyKey): any {
        if (p === Symbol.unscopables) return unscopables;

        
        
        if (p === 'window' || p === 'self') {
          return proxy;
        }

        if (
          p === 'top' ||
          p === 'parent' ||
          (process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop'))
        ) {
          
          if (rawWindow === rawWindow.parent) {
            return proxy;
          }
          return (rawWindow as any)[p];
        }

        
        if (p === 'hasOwnProperty') {
          return hasOwnProperty;
        }

        
        if (p === 'document') {
          document[attachDocProxySymbol] = proxy;
          
          
          
          nextTick(() => delete document[attachDocProxySymbol]);
          return document;
        }
        

        
        
        const value = propertiesWithGetter.has(p) ? (rawWindow as any)[p] : (target as any)[p] || (rawWindow as any)[p];
        return getTargetValue(rawWindow, value);
      },

      
      
      has(target: FakeWindow, p: string | number | symbol): boolean {
        return p in unscopables || p in target || p in rawWindow;
      },

      getOwnPropertyDescriptor(target: FakeWindow, p: string | number | symbol): PropertyDescriptor | undefined {
        
        if (target.hasOwnProperty(p)) {
          const descriptor = Object.getOwnPropertyDescriptor(target, p);
          descriptorTargetMap.set(p, 'target');
          return descriptor;
        }

        if (rawWindow.hasOwnProperty(p)) {
          const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p);
          descriptorTargetMap.set(p, 'rawWindow');
          
          if (descriptor && !descriptor.configurable) {
            descriptor.configurable = true;
          }
          return descriptor;
        }

        return undefined;
      },

      
      ownKeys(target: FakeWindow): PropertyKey[] {
        return uniq(Reflect.ownKeys(rawWindow).concat(Reflect.ownKeys(target)));
      },

      defineProperty(target: Window, p: PropertyKey, attributes: PropertyDescriptor): boolean {
        const from = descriptorTargetMap.get(p);
        
        switch (from) {
          case 'rawWindow':
            return Reflect.defineProperty(rawWindow, p, attributes);
          default:
            return Reflect.defineProperty(target, p, attributes);
        }
      },

      deleteProperty(target: FakeWindow, p: string | number | symbol): boolean {
        if (target.hasOwnProperty(p)) {
          
          delete target[p];
          updatedValueSet.delete(p);

          return true;
        }

        return true;
      },
    });

    this.proxy = proxy;
  }
}

createFakeWindow

function createFakeWindow(global: Window) {
  
  
  const propertiesWithGetter = new Map<PropertyKey, boolean>();
  
  const fakeWindow = {} as FakeWindow;

  
  Object.getOwnPropertyNames(global)
    
    .filter(p => {
      const descriptor = Object.getOwnPropertyDescriptor(global, p);
      return !descriptor?.configurable;
    })
    .forEach(p => {
      
      const descriptor = Object.getOwnPropertyDescriptor(global, p);
      if (descriptor) {
        
        const hasGetter = Object.prototype.hasOwnProperty.call(descriptor, 'get');

        
        if (
          p === 'top' ||
          p === 'parent' ||
          p === 'self' ||
          p === 'window' ||
          (process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop'))
        ) {
          
          descriptor.configurable = true;
          
          if (!hasGetter) {
            
            descriptor.writable = true;
          }
        }

        
        if (hasGetter) propertiesWithGetter.set(p, true);

        
        
        
        rawObjectDefineProperty(fakeWindow, p, Object.freeze(descriptor));
      }
    });

  return {
    fakeWindow,
    propertiesWithGetter,
  };
}
            

SnapshotSandbox

function iter(obj: object, callbackFn: (prop: any) => void) {
  
  for (const prop in obj) {
    if (obj.hasOwnProperty(prop)) {
      callbackFn(prop);
    }
  }
}


export default class SnapshotSandbox implements SandBox {
  proxy: WindowProxy;

  name: string;

  type: SandBoxType;

  sandboxRunning = true;

  private windowSnapshot!: Window;

  private modifyPropsMap: Record<any, any> = {};

  constructor(name: string) {
    this.name = name;
    this.proxy = window;
    this.type = SandBoxType.Snapshot;
  }

  active() {
    
    this.windowSnapshot = {} as Window;
    iter(window, prop => {
      this.windowSnapshot[prop] = window[prop];
    });

    
    Object.keys(this.modifyPropsMap).forEach((p: any) => {
      window[p] = this.modifyPropsMap[p];
    });

    this.sandboxRunning = true;
  }

  inactive() {
    this.modifyPropsMap = {};

    iter(window, prop => {
      if (window[prop] !== this.windowSnapshot[prop]) {
        
        this.modifyPropsMap[prop] = window[prop];
        window[prop] = this.windowSnapshot[prop];
      }
    });

    if (process.env.NODE_ENV === 'development') {
      console.info(`[qiankun:sandbox] ${this.name} origin window restore...`, Object.keys(this.modifyPropsMap));
    }

    this.sandboxRunning = false;
  }
}

样式沙箱

patchAtBootstrapping

export function patchAtBootstrapping(
  appName: string,
  elementGetter: () => HTMLElement | ShadowRoot,
  sandbox: SandBox,
  singular: boolean,
  scopedCSS: boolean,
  excludeAssetFilter?: Function,
): Freer[] {
  
  const basePatchers = [
    () => patchDynamicAppend(appName, elementGetter, sandbox.proxy, false, singular, scopedCSS, excludeAssetFilter),
  ];

  
  const patchersInSandbox = {
    [SandBoxType.LegacyProxy]: basePatchers,
    [SandBoxType.Proxy]: basePatchers,
    [SandBoxType.Snapshot]: basePatchers,
  };

  
  return patchersInSandbox[sandbox.type]?.map(patch => patch());
}

patch

export default function patch(
  appName: string,
  appWrapperGetter: () => HTMLElement | ShadowRoot,
  proxy: Window,
  mounting = true,
  singular = true,
  scopedCSS = false,
  excludeAssetFilter?: CallableFunction,
): Freer {
  
  let dynamicStyleSheetElements: Array<HTMLLinkElement | HTMLStyleElement> = [];

  
  const unpatchDocumentCreate = patchDocumentCreateElement(
    appName,
    appWrapperGetter,
    singular,
    proxy,
    dynamicStyleSheetElements,
  );

  
  
  const unpatchDynamicAppendPrototypeFunctions = patchHTMLDynamicAppendPrototypeFunctions(
    appName,
    appWrapperGetter,
    proxy,
    singular,
    scopedCSS,
    dynamicStyleSheetElements,
    excludeAssetFilter,
  );

  
  if (!mounting) bootstrappingPatchCount++;
  
  if (mounting) mountingPatchCount++;

  
  return function free() {
    
    if (!mounting && bootstrappingPatchCount !== 0) bootstrappingPatchCount--;
    if (mounting) mountingPatchCount--;

    
    const allMicroAppUnmounted = mountingPatchCount === 0 && bootstrappingPatchCount === 0;
    
    unpatchDynamicAppendPrototypeFunctions(allMicroAppUnmounted);
    unpatchDocumentCreate(allMicroAppUnmounted);

    
    dynamicStyleSheetElements.forEach(stylesheetElement => {
      if (stylesheetElement instanceof HTMLStyleElement && isStyledComponentsLike(stylesheetElement)) {
        if (stylesheetElement.sheet) {
          
          setCachedRules(stylesheetElement, (stylesheetElement.sheet as CSSStyleSheet).cssRules);
        }
      }
    });

    
    return function rebuild() {
      
      dynamicStyleSheetElements.forEach(stylesheetElement => {
        
        document.head.appendChild.call(appWrapperGetter(), stylesheetElement);

        
        if (stylesheetElement instanceof HTMLStyleElement && isStyledComponentsLike(stylesheetElement)) {
          const cssRules = getCachedRules(stylesheetElement);
          if (cssRules) {
            
            for (let i = 0; i < cssRules.length; i++) {
              const cssRule = cssRules[i];
              (stylesheetElement.sheet as CSSStyleSheet).insertRule(cssRule.cssText);
            }
          }
        }
      });

      
      if (mounting) {
        dynamicStyleSheetElements = [];
      }
    };
  };
}

patchDocumentCreateElement

function patchDocumentCreateElement(
  appName: string,
  appWrapperGetter: () => HTMLElement | ShadowRoot,
  singular: boolean,
  proxy: Window,
  dynamicStyleSheetElements: HTMLStyleElement[],
) {
  
  if (singular) {
    return noop;
  }

  
  proxyContainerInfoMapper.set(proxy, { appName, proxy, appWrapperGetter, dynamicStyleSheetElements, singular });

  
  if (Document.prototype.createElement === rawDocumentCreateElement) {
    Document.prototype.createElement = function createElement<K extends keyof HTMLElementTagNameMap>(
      this: Document,
      tagName: K,
      options?: ElementCreationOptions,
    ): HTMLElement {
      
      const element = rawDocumentCreateElement.call(this, tagName, options);
      
      if (isHijackingTag(tagName)) {
        
        
        const proxyContainerInfo = proxyContainerInfoMapper.get(this[attachDocProxySymbol]);
        if (proxyContainerInfo) {
          Object.defineProperty(element, attachElementContainerSymbol, {
            value: proxyContainerInfo,
            enumerable: false,
          });
        }
      }

      
      return element;
    };
  }

  
  return function unpatch(recoverPrototype: boolean) {
    proxyContainerInfoMapper.delete(proxy);
    if (recoverPrototype) {
      Document.prototype.createElement = rawDocumentCreateElement;
    }
  };
}

patchTHMLDynamicAppendPrototypeFunctions

function patchHTMLDynamicAppendPrototypeFunctions(
  appName: string,
  appWrapperGetter: () => HTMLElement | ShadowRoot,
  proxy: Window,
  singular = true,
  scopedCSS = false,
  dynamicStyleSheetElements: HTMLStyleElement[],
  excludeAssetFilter?: CallableFunction,
) {
  
  if (
    HTMLHeadElement.prototype.appendChild === rawHeadAppendChild &&
    HTMLBodyElement.prototype.appendChild === rawBodyAppendChild &&
    HTMLHeadElement.prototype.insertBefore === rawHeadInsertBefore
  ) {
    
    HTMLHeadElement.prototype.appendChild = getOverwrittenAppendChildOrInsertBefore({
      rawDOMAppendOrInsertBefore: rawHeadAppendChild,
      appName,
      appWrapperGetter,
      proxy,
      singular,
      dynamicStyleSheetElements,
      scopedCSS,
      excludeAssetFilter,
    }) as typeof rawHeadAppendChild;
    HTMLBodyElement.prototype.appendChild = getOverwrittenAppendChildOrInsertBefore({
      rawDOMAppendOrInsertBefore: rawBodyAppendChild,
      appName,
      appWrapperGetter,
      proxy,
      singular,
      dynamicStyleSheetElements,
      scopedCSS,
      excludeAssetFilter,
    }) as typeof rawBodyAppendChild;

    HTMLHeadElement.prototype.insertBefore = getOverwrittenAppendChildOrInsertBefore({
      rawDOMAppendOrInsertBefore: rawHeadInsertBefore as any,
      appName,
      appWrapperGetter,
      proxy,
      singular,
      dynamicStyleSheetElements,
      scopedCSS,
      excludeAssetFilter,
    }) as typeof rawHeadInsertBefore;
  }

  
  if (
    HTMLHeadElement.prototype.removeChild === rawHeadRemoveChild &&
    HTMLBodyElement.prototype.removeChild === rawBodyRemoveChild
  ) {
    HTMLHeadElement.prototype.removeChild = getNewRemoveChild({
      appWrapperGetter,
      headOrBodyRemoveChild: rawHeadRemoveChild,
    });
    HTMLBodyElement.prototype.removeChild = getNewRemoveChild({
      appWrapperGetter,
      headOrBodyRemoveChild: rawBodyRemoveChild,
    });
  }

  return function unpatch(recoverPrototype: boolean) {
    if (recoverPrototype) {
      HTMLHeadElement.prototype.appendChild = rawHeadAppendChild;
      HTMLHeadElement.prototype.removeChild = rawHeadRemoveChild;
      HTMLBodyElement.prototype.appendChild = rawBodyAppendChild;
      HTMLBodyElement.prototype.removeChild = rawBodyRemoveChild;

      HTMLHeadElement.prototype.insertBefore = rawHeadInsertBefore;
    }
  };
}

getOverwrittenAppendChildOrInsertBefore

function getOverwrittenAppendChildOrInsertBefore(opts: {
  appName: string;
  proxy: WindowProxy;
  singular: boolean;
  dynamicStyleSheetElements: HTMLStyleElement[];
  appWrapperGetter: CallableFunction;
  rawDOMAppendOrInsertBefore: <T extends Node>(newChild: T, refChild?: Node | null) => T;
  scopedCSS: boolean;
  excludeAssetFilter?: CallableFunction;
}) {
  return function appendChildOrInsertBefore<T extends Node>(
    this: HTMLHeadElement | HTMLBodyElement,
    newChild: T,
    refChild?: Node | null,
  ) {
    
    let element = newChild as any;
    
    const { rawDOMAppendOrInsertBefore } = opts;
    if (element.tagName) {
      
      
      let { appName, appWrapperGetter, proxy, singular, dynamicStyleSheetElements } = opts;
      const { scopedCSS, excludeAssetFilter } = opts;

      
      const storedContainerInfo = element[attachElementContainerSymbol];
      if (storedContainerInfo) {
        
        appName = storedContainerInfo.appName;
        
        singular = storedContainerInfo.singular;
        
        appWrapperGetter = storedContainerInfo.appWrapperGetter;
        
        dynamicStyleSheetElements = storedContainerInfo.dynamicStyleSheetElements;
        
        proxy = storedContainerInfo.proxy;
      }

      const invokedByMicroApp = singular
        ? 
          
          
          
          
          
          checkActivityFunctions(window.location).some(name => name === appName)
        : 
          !!storedContainerInfo;

      switch (element.tagName) {
        
        case LINK_TAG_NAME:
        case STYLE_TAG_NAME: {
          
          const stylesheetElement: HTMLLinkElement | HTMLStyleElement = newChild as any;
          
          const { href } = stylesheetElement as HTMLLinkElement;
          if (!invokedByMicroApp || (excludeAssetFilter && href && excludeAssetFilter(href))) {
            
            
            return rawDOMAppendOrInsertBefore.call(this, element, refChild) as T;
          }

          
          const mountDOM = appWrapperGetter();

          
          if (scopedCSS) {
            css.process(mountDOM, stylesheetElement, appName);
          }

          
          
          dynamicStyleSheetElements.push(stylesheetElement);
          
          const referenceNode = mountDOM.contains(refChild) ? refChild : null;
          
          return rawDOMAppendOrInsertBefore.call(mountDOM, stylesheetElement, referenceNode);
        }

        
        case SCRIPT_TAG_NAME: {
          
          const { src, text } = element as HTMLScriptElement;
          
          if (!invokedByMicroApp || (excludeAssetFilter && src && excludeAssetFilter(src))) {
            
            return rawDOMAppendOrInsertBefore.call(this, element, refChild) as T;
          }

          
          const mountDOM = appWrapperGetter();
          
          const { fetch } = frameworkConfiguration;
          
          const referenceNode = mountDOM.contains(refChild) ? refChild : null;

          
          if (src) {
            
            execScripts(null, [src], proxy, {
              fetch,
              strictGlobal: !singular,
              beforeExec: () => {
                Object.defineProperty(document, 'currentScript', {
                  get(): any {
                    return element;
                  },
                  configurable: true,
                });
              },
              success: () => {
                
                
                
                
                const loadEvent = new CustomEvent('load');
                if (isFunction(element.onload)) {
                  element.onload(patchCustomEvent(loadEvent, () => element));
                } else {
                  element.dispatchEvent(loadEvent);
                }

                element = null;
              },
              error: () => {
                const errorEvent = new CustomEvent('error');
                if (isFunction(element.onerror)) {
                  element.onerror(patchCustomEvent(errorEvent, () => element));
                } else {
                  element.dispatchEvent(errorEvent);
                }

                element = null;
              },
            });

            
            const dynamicScriptCommentElement = document.createComment(`dynamic script ${src} replaced by qiankun`);
            return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicScriptCommentElement, referenceNode);
          }

          
          execScripts(null, [`<script>${text}</script>`], proxy, {
            strictGlobal: !singular,
            success: element.onload,
            error: element.onerror,
          });
          
          const dynamicInlineScriptCommentElement = document.createComment('dynamic inline script replaced by qiankun');
          return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicInlineScriptCommentElement, referenceNode);
        }

        default:
          break;
      }
    }

    
    return rawDOMAppendOrInsertBefore.call(this, element, refChild);
  };
}

getNewRemoveChild

function getNewRemoveChild(opts: {
  appWrapperGetter: CallableFunction;
  headOrBodyRemoveChild: typeof HTMLElement.prototype.removeChild;
}) {
  return function removeChild<T extends Node>(this: HTMLHeadElement | HTMLBodyElement, child: T) {
    
    const { headOrBodyRemoveChild } = opts;
    try {
      const { tagName } = child as any;
      
      if (isHijackingTag(tagName)) {
        
        let { appWrapperGetter } = opts;

        
        const storedContainerInfo = (child as any)[attachElementContainerSymbol];
        if (storedContainerInfo) {
          
          
          appWrapperGetter = storedContainerInfo.appWrapperGetter;
        }

        
        
        const container = appWrapperGetter();
        if (container.contains(child)) {
          return rawRemoveChild.call(container, child) as T;
        }
      }
    } catch (e) {
      console.warn(e);
    }

    
    return headOrBodyRemoveChild.call(this, child) as T;
  };
}

patchAtMounting

在微应用的挂载阶段会被调用,主要负责给各个全局变量(方法)打 patch

export function patchAtMounting(
  appName: string,
  elementGetter: () => HTMLElement | ShadowRoot,
  sandbox: SandBox,
  singular: boolean,
  scopedCSS: boolean,
  excludeAssetFilter?: Function,
): Freer[] {
  const basePatchers = [
    
    () => patchInterval(sandbox.proxy),
    
    () => patchWindowListener(sandbox.proxy),
    
    () => patchHistoryListener(),
    
    () => patchDynamicAppend(appName, elementGetter, sandbox.proxy, true, singular, scopedCSS, excludeAssetFilter),
  ];

  const patchersInSandbox = {
    [SandBoxType.LegacyProxy]: [...basePatchers],
    [SandBoxType.Proxy]: [...basePatchers],
    [SandBoxType.Snapshot]: basePatchers,
  };

  return patchersInSandbox[sandbox.type]?.map(patch => patch());
}

patch => patchInterval

export default function patch(global: Window) {
  let intervals: number[] = [];

  
  global.clearInterval = (intervalId: number) => {
    intervals = intervals.filter(id => id !== intervalId);
    return rawWindowClearInterval(intervalId);
  };

  
  global.setInterval = (handler: Function, timeout?: number, ...args: any[]) => {
    const intervalId = rawWindowInterval(handler, timeout, ...args);
    intervals = [...intervals, intervalId];
    return intervalId;
  };

  
  return function free() {
    intervals.forEach(id => global.clearInterval(id));
    global.setInterval = rawWindowInterval;
    global.clearInterval = rawWindowClearInterval;

    return noop;
  };
}

patch => patchWindowListener

export default function patch(global: WindowProxy) {
  
  const listenerMap = new Map<string, EventListenerOrEventListenerObject[]>();

  
  global.addEventListener = (
    type: string,
    listener: EventListenerOrEventListenerObject,
    options?: boolean | AddEventListenerOptions,
  ) => {
    
    const listeners = listenerMap.get(type) || [];
    
    listenerMap.set(type, [...listeners, listener]);
    
    return rawAddEventListener.call(window, type, listener, options);
  };

  
  global.removeEventListener = (
    type: string,
    listener: EventListenerOrEventListenerObject,
    options?: boolean | AddEventListenerOptions,
  ) => {
    
    const storedTypeListeners = listenerMap.get(type);
    if (storedTypeListeners && storedTypeListeners.length && storedTypeListeners.indexOf(listener) !== -1) {
      storedTypeListeners.splice(storedTypeListeners.indexOf(listener), 1);
    }
    
    return rawRemoveEventListener.call(window, type, listener, options);
  };

  
  return function free() {
    
    listenerMap.forEach((listeners, type) =>
      [...listeners].forEach(listener => global.removeEventListener(type, listener)),
    );
    
    global.addEventListener = rawAddEventListener;
    global.removeEventListener = rawRemoveEventListener;

    return noop;
  };
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant