前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >微前端03 : 乾坤的沙箱容器分析(Js沙箱机制建立后的具体应用)

微前端03 : 乾坤的沙箱容器分析(Js沙箱机制建立后的具体应用)

作者头像
杨艺韬
发布2022-09-27 14:16:11
7621
发布2022-09-27 14:16:11
举报

“在微前端01 : 乾坤的Js隔离机制(快照沙箱、两种代理沙箱)中,我们知道了乾坤的沙箱的核心原理和具体实现。但知道这些还不够,因为沙箱本身就像是一个工具,有了工具还得应用到实践中,这个工具才能创造价值发挥作用。我们也在微前端02 : 乾坤的微应用加载流程分析(从微应用的注册到loadApp方法内部实现)中提到了在加载微应用过程中跟沙箱相关的部分逻辑,但受限于篇幅并未展开。本文将会详细讲解乾坤对沙箱的具体应用。 ”

沙箱容器的主逻辑

对沙箱机制的具体应用,本质上就是对沙箱容器的控制,至于什么是沙箱容器,我们直接看代码:

代码语言:javascript
复制
// 代码片段一,所属文件:src/sandbox/index.ts
/**
 * @param appName
 * @param elementGetter
 * @param scopedCSS
 * @param useLooseSandbox
 * @param excludeAssetFilter
 * @param globalContext
 */
export function createSandboxContainer(
  appName: string,
  elementGetter: () => HTMLElement | ShadowRoot,
  scopedCSS: boolean,
  useLooseSandbox?: boolean,
  excludeAssetFilter?: (url: string) => boolean,
  globalContext?: typeof window,
) {
  let sandbox: SandBox;
  if (window.Proxy) {
    sandbox = useLooseSandbox ? new LegacySandbox(appName, globalContext) : new ProxySandbox(appName, globalContext);
  } else {
    sandbox = new SnapshotSandbox(appName);
  }
  // 此处省略许多代码...   占位1
  return {
    instance: sandbox,
    async mount() {
      // 此处省略许多代码... 占位2
      sandbox.active();
      // 此处省略许多代码... 占位3
    },
    async unmount() {
      // 此处省略许多代码... 占位4
      sandbox.inactive();
      // 此处省略许多代码... 占位5
    }
  };
}

由代码片段一可知,所谓沙箱容器,就是一个对象。该对象包括三个属性instance、mount、unmount,其中instace代表沙箱实例,mount、unmount是两个方法,供沙箱容器持有者在合适的时机进行调用。关于沙箱实例,我们先看创建沙箱实例的时候传入了globalContext,还记得我们在微前端01 : 乾坤的Js隔离机制(快照沙箱、两种代理沙箱)中各沙箱的极简版吧,当时我直接用的window,那为什么在真实源码中要通过传入globalContext而不是直接使用window呢。答案其实很简单,参数存在的意义就是参数值可变,否则都直接写死了,换句话说更灵活了。举个例子,如果我们的微应用的载体是另一个微应用呢?如果没有这种灵活性,就不能很好的支持复杂多变的场景,乾坤作为业界知名框架,在众多开发者的打磨下,对于细节的处理确实很值得学习。聊完了沙箱实例的创建,我们再来看看mount、unmount这两个方法。如果忽略省略的代码片段注释处省略的代码,那mount、unmount仅仅是调用sandbox.activesandbox.inactive两个方法让沙箱激活或者失活。如果是这样的话,这个沙箱容器的存在的意义就不大了,但我在介绍mount、unmount两个方法中的其他逻辑之前,我们来先看看代码片段一中占位1处的三行代码:

代码语言:javascript
复制
// 代码片段二,所属文件:src/sandbox/index.ts
const bootstrappingFreers = patchAtBootstrapping(appName, elementGetter, sandbox, scopedCSS, excludeAssetFilter);
let mountingFreers: Freer[] = []; 
let sideEffectsRebuilders: Rebuilder[] = []; 

函数patchAtBootstrapping

我们先暂时只关注第一行代码,这里调用了函数patchAtBootstrapping:

代码语言:javascript
复制
// 代码片段三,所属文件:src/sandbox/patchers/index.ts
export function patchAtBootstrapping(
  appName: string,
  elementGetter: () => HTMLElement | ShadowRoot,
  sandbox: SandBox,
  scopedCSS: boolean,
  excludeAssetFilter?: CallableFunction,
): Freer[] {
  const patchersInSandbox = {
    [SandBoxType.LegacyProxy]: [
      () => patchLooseSandbox(appName, elementGetter, sandbox.proxy, false, scopedCSS, excludeAssetFilter),
    ],
    [SandBoxType.Proxy]: [
      () => patchStrictSandbox(appName, elementGetter, sandbox.proxy, false, scopedCSS, excludeAssetFilter),
    ],
    [SandBoxType.Snapshot]: [
      () => patchLooseSandbox(appName, elementGetter, sandbox.proxy, false, scopedCSS, excludeAssetFilter),
    ],
  };

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

函数patchAtBootstrapping只做了一件事情,就是根据不同的沙箱类型,执行后并以数组的形式返回执行结果。为什么是数组类型呢?就这个方法本身而言,直接返回函数值没有任何问题,因为从代码中可以看出,不管何种沙箱类型,在patchAtBootstrapping中,都只执行了一个函数。之所以包装成数组,是因为其他和patchAtBootstrapping发挥作用类似的函数,比如patchAtMounting,里面就会有多个函数需要执行。这样做的好处是,保证了数据格式的统一,利于后续相关逻辑进行统一处理,同时也有很好的可扩展性,将来如果函数patchAtBootstrapping需要执行多个函数,不需要改动代码整体结构。这是我们值得学习的。

函数patchStrictSandbox

至于patchLooseSandbox、patchStrictSandbox、patchLooseSandbox这三个函数。接下来我只深入到patchStrictSandbox中去,因为patchStrictSandbox最重要,其他两个函数的内部主体逻辑和patchStrictSandbox类似,感兴趣的朋友们可以自行阅读,如果有不清楚的地方可以留言交流。接下来我们就看看函数patchStrictSandbox的代码吧:

代码语言:javascript
复制
// 代码片段四,所属文件:src/sandbox/patchers/dynamicAppend/forStrictSandbox.ts
export function patchStrictSandbox(
  appName: string,
  appWrapperGetter: () => HTMLElement | ShadowRoot,
  proxy: Window,
  mounting = true,
  scopedCSS = false,
  excludeAssetFilter?: CallableFunction,
): Freer {
  // 此处省略许多代码... 占位1
  return function free() {
    // 此处省略许多代码... 占位2
    return function rebuild() {
       // 此处省略许多代码... 占位3
    };
  };
}

在省略了许多代码后,我们可以直观的看到该函数的主体结构,这个过程我们可以用伪代码来描述这个调用过程:

代码语言:javascript
复制
// 代码片段五
let freeFunc = patchStrictSandbox(许多参数...); // 第一步:在这个函数里面执行了代码,影响了程序状态
let rebuidFun = freeFunc(); // 第二步:将第一步中对程序状态的影响撤销掉
rebuidFun();// 第三步:恢复到第一步执行完成时程序的状态

理解了patchStrictSandbox的主逻辑,我们来看看代码片段四中占位1处所省略的代码:

代码语言:javascript
复制
// 代码片段六,所属文件:src/sandbox/patchers/dynamicAppend/forStrictSandbox.ts
export function patchStrictSandbox(
  appName: string,
  appWrapperGetter: () => HTMLElement | ShadowRoot,
  proxy: Window,
  mounting = true,
  scopedCSS = false,
  excludeAssetFilter?: CallableFunction,
): Freer {
    //*********************第一部分*********************/
    let containerConfig = proxyAttachContainerConfigMap.get(proxy);
    if (!containerConfig) {
        containerConfig = {
          appName,
          proxy,
          appWrapperGetter,
          dynamicStyleSheetElements: [],
          strictGlobal: true,
          excludeAssetFilter,
          scopedCSS,
        };
        proxyAttachContainerConfigMap.set(proxy, containerConfig);
    }
    const { dynamicStyleSheetElements } = containerConfig;

    /***********************第二部分*********************/
    const unpatchDocumentCreate = patchDocumentCreateElement();
    const unpatchDynamicAppendPrototypeFunctions = patchHTMLDynamicAppendPrototypeFunctions(
        (element) => elementAttachContainerConfigMap.has(element),
        (element) => elementAttachContainerConfigMap.get(element)!,
    );
    // 此处省略许多代码... 
}

函数patchStrictSandbox的第一部分逻辑

我们先来分析代码片段六中的第一部分,可以看到该部分有几个重要的变量,proxyAttachContainerConfigMap、dynamicStyleSheetElements、proxy、containerConfig,这部分代码做了三件事,一是从缓存变量proxyAttachContainerConfigMap中根据proxy获取配置对象,如果有就赋值给变量containerConfig。二是如果缓存中没有proxy对应的配置对象,那么则定一个初始化配置对象,并以proxykey,以这个配置对象为value,存储到缓存变量proxyAttachContainerConfigMap中。三是从containerConfig中获取dynamicStyleSheetElements。这里有几个点值得推敲。首先,proxy是什么,为什么要以proxykey将配置对象存储在proxyAttachContainerConfigMap中?proxy实际上就是在上文代码片段一中创建的沙箱实例,对应代码片段一中的sandbox变量。

其次,在代码片段六中,proxyAttachContainerConfigMap只赋值了初始值,既然有是从缓存变量proxyAttachContainerConfigMap中根据proxy获取配置对象的这个操作,说明proxyAttachContainerConfigMap肯定在其他地方有更新containerConfig的操作,否则没必要只缓存一个初始化值。具体应该在哪里更新这个containerConfig,更新containerConfig中的哪个属性对应的值,我们在后文会提到。

最后,dynamicStyleSheetElements是什么?实际上其类型是HTMLStyleElement[]HTMLStyleElement表示<style>元素。我们这里不追究HTMLStyleElement到底有多少属性和方法,但需要关注的是,HTMLStyleElement实例中有一个sheet属性,这个属性是一个CSSStyleSheet对象。至于CSSStyleSheet的概念和各种属性我就不在这里一一详述了,可以参阅相关文档了解。此时我们需要知道的是,CSSStyleSheet的实例有个重要的属性cssRules,该属性类型为CSSRuleList,是一个CSSStyleRule对象数组。关于CSSStyleRule的详细内容就不继续介绍了,只需要知道CSSStyleRule相当于代表了一条具体的css样式,如下所示:

代码语言:javascript
复制
// 注意虽然样式呈现的效果等价,但实际上通过CssStyleRule控制样式和普通的以文本的形式挂载到dom上的样式有着一些不同,这些不同会在后面提到
div{
   color:red;
}

这里了解这些就足够了,后续在分析乾坤对css资源进行处理的时候还会涉及CSSStyleRule,到时再继续探讨。

“注:请阅读英文版MDN文档,对于HTMLStyleElement的解释,中文版的 翻译还比较落后,与英文版的介绍有出入 ”

函数patchStrictSandbox的第二部分逻辑

这时我们将视野回到代码片段六中的第二部分,为了方便阅读将相关代码放到这里:

代码语言:javascript
复制
const unpatchDocumentCreate = patchDocumentCreateElement();
const unpatchDynamicAppendPrototypeFunctions = patchHTMLDynamicAppendPrototypeFunctions(
    (element) => elementAttachContainerConfigMap.has(element),
    (element) => elementAttachContainerConfigMap.get(element)!,
);

patchDocumentCreateElement

我们先看看patchDocumentCreateElement中的代码:

代码语言:javascript
复制
// 代码片段七,所属文件:src/sandbox/patchers/dynamicAppend/forStrictSandbox.ts
function patchDocumentCreateElement() {
    // 省略许多代码...
    const rawDocumentCreateElement = document.createElement;
    Document.prototype.createElement = function createElement(
        // 省略许多代码...
    ): HTMLElement {
      const element = rawDocumentCreateElement.call(this, tagName, options);
      // 关键点1
      if (isHijackingTag(tagName)) {
        // 省略许多代码
      }
      return element;
    };
    // 关键点2 
    if (document.hasOwnProperty('createElement')) {
      document.createElement = Document.prototype.createElement;
    }
    // 关键点3 
    docCreatePatchedMap.set(Document.prototype.createElement, rawDocumentCreateElement);
  }
    
  return function unpatch() {
    // 关键点4
    //此次省略一些代码...
    Document.prototype.createElement = docCreateElementFnBeforeOverwrite;
    document.createElement = docCreateElementFnBeforeOverwrite;
  };
}

在省略一些代码后,patchDocumentCreateElement函数实现的功能,逐渐清晰可见。该函数主要做了三件事情。一是重写Document.prototype.createElement,重写的目的在代码片段七中的关键点1体现,具体关键点1内部做了什么由于逻辑较简单暂不在这里介绍。二是保存重写后的createElement和重写前的createElement这二者的对应关系,对应关键点3。至于上面代码片段提到的关键点2,是对document的一个变化,这个点应该和其他地方的逻辑有关系,否则没有必要对document进行判断处理,暂时没发现用到这个处理的地方,后续找到了相关逻辑再补上这个细节,但意义不太大,再看情况决定。三是返回一个函数,该函数会还原重写Document.prototype.createElement时候对Document.prototype.createElement的影响。

由于篇幅较长,请将我们的视野再次移动到代码片段六中的第二部分:

代码语言:javascript
复制
const unpatchDocumentCreate = patchDocumentCreateElement();
const unpatchDynamicAppendPrototypeFunctions = patchHTMLDynamicAppendPrototypeFunctions(
    (element) => elementAttachContainerConfigMap.has(element),
    (element) => elementAttachContainerConfigMap.get(element)!,
);

刚才我们分析了函数patchDocumentCreateElement,现在可以知道代码片段中的unpatchDocumentCreate是一个函数,执行后会清除对Document.prototype.createElement的影响。这里我不再进入函数patchHTMLDynamicAppendPrototypeFunctions中进行分析,原理和函数patchDocumentCreateElement类似,只不过其影响和恢复的的是HTMLHeadElement.prototype.appendChild、HTMLHeadElement.prototype.removeChild、HTMLBodyElement.prototype.removeChild、HTMLHeadElement.prototype.insertBefore等原型方法。

函数patchStrictSandbox的free函数

此时,请将视线移动到代码片段四中的占位2处,代码如下:

代码语言:javascript
复制
// 此处省略许多代码...
if (allMicroAppUnmounted) {
  unpatchDynamicAppendPrototypeFunctions();
  unpatchDocumentCreate();
}
recordStyledComponentsCSSRules(dynamicStyleSheetElements);

从上文的分析我们知道,执行unpatchDynamicAppendPrototypeFunctions、unpatchDocumentCreate两个函数后,会清除重写相应原型函数的影响。我们重点看看recordStyledComponentsCSSRules(dynamicStyleSheetElements);,代码如下:

代码语言:javascript
复制
export function recordStyledComponentsCSSRules(styleElements: HTMLStyleElement[]): void {
  styleElements.forEach((styleElement) => {
    if (styleElement instanceof HTMLStyleElement && isStyledComponentsLike(styleElement)) {
      if (styleElement.sheet) {
        styledComponentCSSRulesMap.set(styleElement, (styleElement.sheet as CSSStyleSheet).cssRules);
      }
    }
  });
}

核心其实只有一行代码:styledComponentCSSRulesMap.set(styleElement, (styleElement.sheet as CSSStyleSheet).cssRules);。上文我们知道了cssRules代表着一条条具体的css样式,就这行代码而言,这些样式是从远程加载而来,相当于从网络上获取了一个css文件,然后对其中的内容进行解析,生成一个style标签,style标签具体承载的样式并非以字符串的形式,这里的具体代码比较冗长暂时不贴出来。实际上就是保存一个style标签对象和其中的内容之间的关系。这里保存的cssRules在下文的分析中会用到。

函数patchStrictSandbox中free函数的rebuild函数

此时,请将视线移动到代码片段四中的占位3处,代码如下:

代码语言:javascript
复制
return function rebuild() {
  rebuildCSSRules(dynamicStyleSheetElements, (stylesheetElement) => {
    const appWrapper = appWrapperGetter();
    if (!appWrapper.contains(stylesheetElement)) {
      rawHeadAppendChild.call(appWrapper, stylesheetElement);
      return true;
    }

    return false;
  });
};

对应的rebuildCSSRules函数如下:

代码语言:javascript
复制
export function rebuildCSSRules(
  styleSheetElements: HTMLStyleElement[],
  reAppendElement: (stylesheetElement: HTMLStyleElement) => boolean,
) {
  styleSheetElements.forEach((stylesheetElement) => {
    const appendSuccess = reAppendElement(stylesheetElement);
    if (appendSuccess) {
      if (stylesheetElement instanceof HTMLStyleElement && isStyledComponentsLike(stylesheetElement)) {
        const cssRules = getStyledElementCSSRules(stylesheetElement);
        if (cssRules) {
          for (let i = 0; i < cssRules.length; i++) {
            const cssRule = cssRules[i];
            const cssStyleSheetElement = stylesheetElement.sheet as CSSStyleSheet;
            cssStyleSheetElement.insertRule(cssRule.cssText, cssStyleSheetElement.cssRules.length);
          }
        }
      }
    }
  });
}

从代码逻辑看可以直观的看出两件事情,一是将前面生成的style标签添加到微应用上;二是将之前保存的cssRule插入到对应的style标签上。为什么一定要执行insertRule呢?通过cssRule动态控制样式和普通style标签控制样式有所不同。一旦cssRule所关联的style标签脱离document,这些cssRule都会失效。这也是为什么需要保存和重新设置的原因。

到此,本文代码片段一中的占位1处的代码就算执行完成了。对占位1的代码理解清楚后,本文也就基本完成了。因为mount、unmount其实就是在利用占位1提供的bootstrappingFreers函数改变以及恢复状态。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-03-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 杨艺韬 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 沙箱容器的主逻辑
  • 函数patchAtBootstrapping
  • 函数patchStrictSandbox
    • 函数patchStrictSandbox的第一部分逻辑
      • 函数patchStrictSandbox的第二部分逻辑
      • patchDocumentCreateElement
      • 函数patchStrictSandbox的free函数
      • 函数patchStrictSandbox中free函数的rebuild函数
      相关产品与服务
      容器服务
      腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档