乾坤框架解析 - 沙箱机制

1:如何配置进行控制

registerMicroApps方法里面调用loadApp会传入frameworkConfiguration, 这个对象是乾坤框架对外提供的api用于控制应用加载,切换,沙箱等行为
loadMicroApp方法参数会传入configuration?: FrameworkConfiguration

FrameworkConfiguration 里面可配置的属性如下:

export let frameworkConfiguration: FrameworkConfiguration = {};
ts:   FrameworkConfiguration = QiankunSpecialOpts & ImportEntryOpts & StartOpts;
QiankunSpecialOpts :
{
  prefetch
  sandbox?:
    | boolean
    | {
        strictStyleIsolation?: boolean;
        experimentalStyleIsolation?: boolean;
        patchers?: Patcher[];
      };
  /*
    with singular mode, any app will wait to load until other apps are unmouting
    it is useful for the scenario that only one sub app shown at one time
  */
  singular?: boolean | ((app: LoadableApp) => Promise);
  excludeAssetFilter?: (url: string) => boolean; // skip some scripts or links intercept, like JSONP
}
ImportEntryOpts :   
{
        fetch?: typeof window.fetch;
    getPublicPath?: (entry: Entry) => string;
    getTemplate?: (tpl: string) => string;
}

StartOpts: 
{
  urlRerouteOnly?: boolean;
}

2:在什么时候,什么地方调用运行

loader.ts 里面 加载完子应用方法 里面

export async function loadApp(
  app: LoadableApp,
  configuration: FrameworkConfiguration = {},
  lifeCycles?: FrameworkLifeCycles,
): Promise {
  .......
  const { singular = false, sandbox = true, excludeAssetFilter, ...importEntryOpts } = configuration;
  // get the entry html content and script executor
  // 核心代码,加载app对应的入口文件,以及将文件处理成模板,和可执行脚本信息对象
  const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);
  ......
  // 根据appContent 返回对应的对象,这里dom对象里面是有沙箱机制的
  let element: HTMLElement | null = createElement(appContent, strictStyleIsolation);
  .......
// 创建沙箱
  if (sandbox) {
    const sandboxInstance = createSandbox(
      appName,
      containerGetter,
      Boolean(singular),
      enableScopedCSS,
      excludeAssetFilter,
    );
    // 用沙箱的代理对象作为接下来使用的全局对象
    global = sandboxInstance.proxy as typeof window;
  }
 // 执行子应用的脚本
  const scriptExports: any = await execScripts(global, !singular);
  ...........
  return parcelConfig;

}

在createElement里面, 利用shadow dom 构造独立的代码片段,类型iframe

// 创建元素,appContent 为里面的内容,strictStyleIsolation与沙箱有关
// 根据appContent 返回对应的对象,这里dom对象里面是有沙箱机制的
function createElement(appContent: string, strictStyleIsolation: boolean): HTMLElement {
  .........
  // 如果元素支持沙箱,否则创建沙箱
      if (appElement.attachShadow) {
        shadow = appElement.attachShadow({ mode: 'open' });
      } else {
        // createShadowRoot was proposed in initial spec, which has then been deprecated
        shadow = (appElement as any).createShadowRoot();
      }
      shadow.innerHTML = innerHTML;
  ..........
  return appElement;
}

shadow dom 会隔离css以及利用document不会找到里面的dom,但是对于js脚本而已并没有做到绝对的隔离,比如window.setInterval等里面的方法以及属性还是会与外界相互影响,此时乾坤框架createSandbox实现了对脚本的隔离。
沙箱分为3种:
1:singular=true,如果是单一应用切换则用LegacySandbox,
2:singular=false, 如果一个页面包含多个子应用则用ProxySandbox
3:如果浏览器不支持window.Proxy,则兼容用SnapshotSandbox

ProxySandbox - 多子应用情况

1: fakewindow + window的组合,每次new ProxySandbox() 会创建fakewindow实例作为proxy
2:set时值放到fakewindow里面,get时先从fakewindow里面取,取不到就到window里面取
2:判断是特殊属性比如不可配置,编辑,修改的属性,就直接从window里面取

export default class ProxySandbox implements SandBox {
    /** window 值变更记录 */
    private updatedValueSet = new Set();
    name: string;
    type: SandBoxType;
    proxy: WindowProxy;
    sandboxRunning = true; // 沙箱状态
    active() {
      // 记录激活的沙箱数量
      if (!this.sandboxRunning) activeSandboxCount++;
      this.sandboxRunning = true;
    }
    inactive() {
      .........
      this.sandboxRunning = false;
    }
  
    constructor(name: string) {
      // 变量配置,这里rowWindow = window
       .........
      // 将不可编辑的特殊属性记录到propertiesWithGetter
      const { fakeWindow, propertiesWithGetter } = createFakeWindow(rawWindow);
      .........
  
      const proxy = new Proxy(fakeWindow, {
        set(target: FakeWindow, p: PropertyKey, value: any): boolean {
          // 如果本实例的沙箱正在运行
          if (self.sandboxRunning) {
            .........
            // @ts-ignore
            target[p] = value;
            // 记录修改的值
            updatedValueSet.add(p);
            .........
          }
          .........
          // 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的情况下应该忽略错误
          return true;
        },
  
        get(target: FakeWindow, p: PropertyKey): any {
            // 一些特殊属性,如[window,self,top,hasOwnProperty,document] 的特殊处理以及返回
            .........
  
          // eslint-disable-next-line no-bitwise
          // 有getter的属性,直接访问window.p, 否则访问fakewindow.p
          // 如果没有不可编辑且具有getter的属性,就先从fakewindow里面取,取不到就从window里面取
          const value = propertiesWithGetter.has(p) ? (rawWindow as any)[p] : (target as any)[p] || (rawWindow as any)[p];
  
          return getTargetValue(rawWindow, value);
        },
  
        // trap in operator
        // see https://github.com/styled-components/styled-components/blob/master/packages/styled-components/src/constants.js#L12
        has() {},
        // 获取 FakeWindow || window 里面的自有属性
        getOwnPropertyDescriptor() {}
  
        // trap to support iterator with sandbox
        // FakeWindow + window 里面的不重复的属性canvcat
        ownKeys() {}
        // 首先看这个属性是从哪来的,从window里面来的就在window定义
        defineProperty() {},
        // 这里只删除 fakeWindow里面的属性
        deleteProperty() {},
      });
  
      this.proxy = proxy;
    }
  }

图示如下:

proxy.png

LegacySandbox - 单应用的情况,之后会使用ProxySandbox替代

/**
 * 基于 Proxy 实现的沙箱
 * TODO: 为了兼容性 singular 模式下依旧使用该沙箱,等新沙箱稳定之后再切换
 * 生成一个代替window对象的委托,set,get时实际操作的window对象属性同时记录操作行为,active,inactive时释放操作行为使window对象还原
 */
export default class SingularProxySandbox implements SandBox {
    /** 沙箱期间新增的全局变量 */
    private addedPropsMapInSandbox = new Map();
  
    /** 沙箱期间更新的全局变量 */
    private modifiedPropsOriginalValueMapInSandbox = new Map();
  
    /** 持续记录更新的(新增和修改的)全局变量的 map,用于在任意时刻做 snapshot */
    private currentUpdatedPropsValueMap = new Map();
  
    .......
  
    active() {
        .......
        // 根据之前修改的记录重新修改window的属性,即还原沙箱之前的状态
        this.currentUpdatedPropsValueMap.forEach((v, p) => setWindowProp(p, v));
        .......
    }
  
    inactive() {
        .......
      // renderSandboxSnapshot = snapshot(currentUpdatedPropsValueMapForSnapshot);
      // restore global props to initial snapshot
      // 将沙箱期间修改的属性还原为原先的属性
      this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => setWindowProp(p, v));
      // 将沙箱期间新增的全局变量消除
      this.addedPropsMapInSandbox.forEach((_, p) => setWindowProp(p, undefined, true));
      this.sandboxRunning = false;
    }
  
    constructor(name: string) {
      .......

      const proxy = new Proxy(fakeWindow, {
        // 在fakeWindow.p = v 执行前,会将p,v增加/编辑队列记录
        set(_: Window, p: PropertyKey, value: any): boolean {
            .......
            // 新增p属性放入新增队列
            addedPropsMapInSandbox.set(p, value);
            .......

            .......
            // 如果当前 window 对象存在该属性,且 record map 中未记录过,则记录该属性初始值
            const originalValue = (rawWindow as any)[p];
            // 记录原始属性
            modifiedPropsOriginalValueMapInSandbox.set(p, originalValue);
            // 记录修改属性以及修改后的值
            currentUpdatedPropsValueMap.set(p, value);
            // 设置值
            (rawWindow as any)[p] = value;

            .......
  
          // 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的情况下应该忽略错误
          return true;
        },
  
        get(_: Window, p: PropertyKey): any {
            // 特殊属性处理
          .......
          const value = (rawWindow as any)[p];
          return getTargetValue(rawWindow, value);
        },
  
        .......
        },
      });
  
      this.proxy = proxy;
    }
  }

SnapshotSandbox- 不兼容window.Proxy的情况

基于 diff 方式实现的沙箱,用于不支持 Proxy 的低版本浏览器

  • 代理实质为window,get,set,修改的是window的属性,但是active的时候,会生成window的快照,inactive的时候会根据快照还原

3:有什么用处,出于什么原因要设计沙箱机制
页面上多个子应用会造成 全局变量等Js冲突,Css&DOM冲突:样式文件相互影响,dom可能带有相同的class,id造成选中困难
Css&DOM冲突 可以用shadow dom来解决,但是js目前只能使用ProxySandbox脚本hack

5:流程图+结构图


createSandBox.png

沙箱补丁

启动阶段补丁:patchAtBootstrapping,生成沙箱createSandbox()的时候执行,在loadApp()加载应用文件,生成shadow dom后执行,之后才是导出并执行应用的启动阶段
挂载阶段补丁:生成沙箱createSandbox()的时候导出patchAtMounting,在应用的mount阶段执行

patchAtBootstrapping 主要是对dom的创建,插入,移除等原生方法进行了一层封装,主要有插入style后的css的样式生效以及scoped的隔离逻辑,插入script后自动执行脚本功能逻辑

const basePatchers = [
    () => patchDynamicAppend(false),
  ]
// 执行并返回资源释放,原生方法还原的接口
return basePatchers .map(patch => patch()); 

patchAtMounting 除了上述patchAtBootstrapping的功能外,对Interval,addEventListener,historyListener等方法的封装

const basePatchers = [
    () => patchInterval(sandbox.proxy),
    () => patchWindowListener(sandbox.proxy),
    () => patchHistoryListener(),
    () => patchDynamicAppend(true),
  ]
// 执行并返回资源释放,原生方法还原的接口
return basePatchers .map(patch => patch());

调用时机:

loadApp() {
    .......
    // 创建沙箱
  if (sandbox) {
    const sandboxInstance = createSandbox(...); // 里面已经执行了  patchAtBootstrapping
    // 用沙箱的代理对象作为接下来使用的全局对象
    global = sandboxInstance.proxy as typeof window;

    mountSandbox = sandboxInstance.mount;

    unmountSandbox = sandboxInstance.unmount;
  }
  .......
  // 返回封装好的生命周期钩子
  return {
      bootstrap: [...],
      mount: [..., mountSandbox, ...],
      unmount: [..., unmountSandbox, ...]
  }  
}

补丁在沙箱中如何执行:

createSandbox(...) {
    sandbox = [LegacySandbox, ProxySandbox, SnapshotSandbox] // 根据条件选择里面一种

    // some side effect could be be invoked while bootstrapping, 
    // such as dynamic stylesheet injection with style-loader, especially during the development phase
    // 执行启动阶段补丁, 返回释放还原的接口列表
    const bootstrappingFreers = patchAtBootstrapping()

    return {
        proxy: sandbox.proxy,
        mount() {
            sandbox.active();

            // sideEffectsRebuilders 包含启动阶段的rebuild,和mount阶段的rebuld,将其拆分出来
            // 启动阶段的rebuild执行
            const satb = sideEffectsRebuilders.slice(0, bootstrappingFreers.length);
            satb.forEach(rebuild => rebuild());
            
            // 返回挂载阶段的释放还原的接口
            mountingFreers = patchAtMounting()

            // mount阶段的rebuld
            const satm = sideEffectsRebuilders.slice(bootstrappingFreers.length);
            satm.forEach(rebuild => rebuild());
        },
        unmount() {
            // record the rebuilders of window side effects (event listeners or timers)
            // note that the frees of mounting phase are one-off as it will be re-init at next mounting
            // 启动阶段,挂载阶段的所有释放资源,同时返回rebuild列表并记录下来,下次挂载用
            sideEffectsRebuilders = [...bootstrappingFreers, ...mountingFreers].map(free => free());

            sandbox.inactive();
        }
    }
}

dom操作的封装 :patchDynamicAppend

patchDynamicAppend 是 patchAtBootstrapping 和 patchAtMounting的核心方法

这里有一个场景,微应用加载后,执行微应用里面的脚本,我们知道由于沙盒的设置,里面的全局变量访问以proxy的方式进行,但是通过生成`], proxy, { strictGlobal: !singular, success: element.onload, error: element.onerror, }); // 将注释插入到mountDOM中 const dynamicInlineScriptCommentElement = document.createComment('dynamic inline script replaced by qiankun'); return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicInlineScriptCommentElement, referenceNode); } default: break; } } // refChild为null,则为appendChild, 否则为insertBefore return rawDOMAppendOrInsertBefore.call(this, element, refChild); }; }

你可能感兴趣的:(乾坤框架解析 - 沙箱机制)