微前端(wujie)源码解析-5.wujie-core沙箱

wujie沙箱解析

前言

上一篇文章中我们分析了wujie-core的实现,核心是创建沙箱、激活沙箱、运行沙箱三个步骤, 本文将对创建沙箱环节做深入的剖析。

沙箱实现原理

  • js沙箱: 先抛结论,wujie-core的js沙箱实现原理基于iframe来实现的

将子应用的js注入主应用同域的iframe中运行,iframe是一个原生的window沙箱, 内部有完整的history和location接口,子应用实例instance运行在iframe中, 路由也彻底和主应用解耦,可以直接在业务组件里面启动应用。

  • css沙箱: wujie-core的css沙箱实现原理基于webcomponent来实现的

无界采用webcomponent来实现页面的样式隔离,无界会创建一个wujie自定义元素, 然后将子应用的完整结构渲染在内部

  • iframe与webcomponent连接机制

无界通过代理 iframedocumentwebcomponent,从而实现两者的互联

下面我们通过代码来分析wujie-core沙箱的实现

沙箱创建

当我们通过new Wujie()创建一个沙箱时,会执行Wujie的构造函数

js 复制代码
export default class Wujie {
    constructor(options: {
       name: string;
       url: string;
       attrs: { [key: string]: any };
       degradeAttrs: { [key: string]: any };
       fiber: boolean;
       degrade;
       plugins: Array<plugin>;
       lifecycles: lifecycles;
    }) {
       // 传递inject给嵌套子应用
       if (window.__POWERED_BY_WUJIE__) this.inject = window.__WUJIE.inject;
       else {
          this.inject = {
             idToSandboxMap: idToSandboxCacheMap,
             appEventObjMap,
             mainHostPath: window.location.protocol + "//" + window.location.host,
          };
       }
       const { name, url, attrs, fiber, degradeAttrs, degrade, lifecycles, plugins } = options;
       this.id = name;
       this.fiber = fiber;
       this.degrade = degrade || !wujieSupport;
       this.bus = new EventBus(this.id);
       this.url = url;
       this.degradeAttrs = degradeAttrs;
       this.provide = { bus: this.bus };
       this.styleSheetElements = [];
       this.execQueue = [];
       this.lifecycles = lifecycles;
       this.plugins = getPlugins(plugins);

       // 创建目标地址的解析
       const { urlElement, appHostPath, appRoutePath } = appRouteParse(url);
       const { mainHostPath } = this.inject;
       // 创建iframe
       this.iframe = iframeGenerator(this, attrs, mainHostPath, appHostPath, appRoutePath);

       if (this.degrade) {
         // ...somthing code
       } else {
          const { proxyWindow, proxyDocument, proxyLocation } = proxyGenerator(
             this.iframe,
             urlElement,
             mainHostPath,
             appHostPath
          );
          this.proxy = proxyWindow;
          this.proxyDocument = proxyDocument;
          this.proxyLocation = proxyLocation;
       }
       this.provide.location = this.proxyLocation;

       addSandboxCacheWithWujie(this.id, this);
    }
}

在构造函数中,我们可以看到Wujie的构造函数主要做了以下4件事情:

  • 初始化赋值,例如:idfiberdegradebusurldegradeAttrsprovidestyleSheetElementsexecQueuelifecyclesplugins
  • 创建iframe,并且将iframe挂到无界实例上
  • 创建代理,并且将代理挂到无界实例上
  • 将沙箱实例添加到缓存中

我们放一个流程图如下:

在以上四步中,我们重点看下创建iframe和创建代理两个步骤

创建iframe

iframe是通过iframeGenerator函数来创建的,函数的实现如下;

js 复制代码
export function iframeGenerator(
    sandbox: WuJie,
    attrs: { [key: string]: any },
    mainHostPath: string,
    appHostPath: string,
    appRoutePath: string
): HTMLIFrameElement {
    // 创建iframe元素
    const iframe = window.document.createElement("iframe");
    const attrsMerge = { src: mainHostPath, style: "display: none", ...attrs, name: sandbox.id, [WUJIE_DATA_FLAG]: "" };
    // 将属性设置到iframe上
    setAttrsToElement(iframe, attrsMerge);
    // 将iframe添加到body中,iframe为隐藏状态
    window.document.body.appendChild(iframe);

    const iframeWindow = iframe.contentWindow;
    // 变量需要提前注入,在入口函数通过变量防止死循环
    // 将变量注入到iframe中
    patchIframeVariable(iframeWindow, sandbox, appHostPath);
    sandbox.iframeReady = stopIframeLoading(iframeWindow).then(() => {
       if (!iframeWindow.__WUJIE) {
          patchIframeVariable(iframeWindow, sandbox, appHostPath);
       }
       initIframeDom(iframeWindow, sandbox, mainHostPath, appHostPath);
       /**
        * 如果有同步优先同步,非同步从url读取
        */
       if (!isMatchSyncQueryById(iframeWindow.__WUJIE.id)) {
          iframeWindow.history.replaceState(null, "", mainHostPath + appRoutePath);
       }
    });
    return iframe;
}

iframeGenerator函数中,主要做了以下几件事情:

  • 创建iframe元素
  • 将属性设置到iframe上,iframe和主应用同域,且为隐藏状态(display:none)
  • 将iframe插入到body中
  • 将变量注入到iframe中,这里重点关注下,注入了沙箱实例__WUJIE、子应用域名__WUJIE_PUBLIC_PATH__,源码如下:
ts 复制代码
function patchIframeVariable(iframeWindow: Window, wujie: WuJie, appHostPath: string): void {
    iframeWindow.__WUJIE = wujie;
    iframeWindow.__WUJIE_PUBLIC_PATH__ = appHostPath + "/";
    iframeWindow.$wujie = wujie.provide;
    iframeWindow.__WUJIE_RAW_WINDOW__ = iframeWindow;
}
  • 创建一个promise,在iframe加载完成后,初始化iframe的dom结构,这块我们后期在沙箱激活的时候再分析
  • 最后返回iframe元素

沙箱代理

在创建iframe后,需要将iframewindowdocumentlocation等对象代理,下面看下整体的代码:

ts 复制代码
export function proxyGenerator(
    iframe: HTMLIFrameElement,
    urlElement: HTMLAnchorElement,
    mainHostPath: string,
    appHostPath: string
): {
    proxyWindow: Window;
    proxyDocument: Object;
    proxyLocation: Object;
} {
    const proxyWindow = new Proxy(iframe.contentWindow, {
        get: (target: Window, p: PropertyKey): any => {
            
            return getTargetValue(target, p);
        },

        set: (target: Window, p: PropertyKey, value: any) => {
            target[p] = value;
            return true;
        }
    });

    // proxy document
    const proxyDocument = new Proxy(
        {},
        {
            get: function (_fakeDocument, propKey) {
                const document = window.document;
                const { shadowRoot, proxyLocation } = iframe.contentWindow.__WUJIE;
                const rawCreateElement = iframe.contentWindow.__WUJIE_RAW_DOCUMENT_CREATE_ELEMENT__;
                const rawCreateTextNode = iframe.contentWindow.__WUJIE_RAW_DOCUMENT_CREATE_TEXT_NODE__;
                // 劫持对document的操作,使之作用于shadowRoot上
                // other proxy code
                if (propKey === "getElementById") {
                    return new Proxy(shadowRoot.querySelector, {
                        apply(target, ctx, args) {
                            // other code
                            target.call(shadowRoot, `[id="${args[0]}"]`)
                            // other code
                        },
                    });
                }
                // other proxy code
                if (propKey === "forms") return shadowRoot.querySelectorAll("form");
                if (propKey === "images") return shadowRoot.querySelectorAll("img");
                if (propKey === "links") return shadowRoot.querySelectorAll("a");
                // other code
            },
        }
    );

    // proxy location
    const proxyLocation = new Proxy(
        {},
        {
            get: function (_fakeLocation, propKey) {
                const location = iframe.contentWindow.location;
                // proxy location的 href host 等属性
                
                // 返回location的属性
                return getTargetValue(location, propKey);
            },
            set: function (_fakeLocation, propKey, value) {
                iframe.contentWindow.location[propKey] = value;
                return true;
            }
        }
    );
    return { proxyWindow, proxyDocument, proxyLocation };
}

从中可以看到,proxyGenerator函数主要做了以下几件事情:

  • 代理iframewindow对象,通过proxyWindow变量来引用
  • 代理iframedocument对象,通过proxyDocument变量来引用
  • 代理iframelocation对象,通过proxyLocation变量来引用

下面我们展开来看下三个代理的实现原理

proxyWindow

proxyWindow是用来代理iframewindow对象,源码如下:

ts 复制代码
const proxyWindow = new Proxy(iframe.contentWindow, {
        get: (target: Window, p: PropertyKey): any => {
            // 劫持location
            // 对自身的处理 window self
            // 修正this指针指向
            return getTargetValue(target, p);
        },

        set: (target: Window, p: PropertyKey, value: any) => {
            target[p] = value;
            return true;
        }
    });

上面的代码中已经精简,通过new Proxy来创建一个代理对象来代理iframewindow,并且添加了getset方法,get方法主要是用来获取子应用window的属性,set方法主要是用来设置window的属性,源码中还做了一些边界处理,我们暂时不做分析, 只需知道对于子应用window的操作大部分最终还是作用于iframewindow上即可。

proxyDocument

proxyDocument是用来代理iframedocument对象,使的对于document的操作大部分都基于子应用(部分方法会从主应用获取),源码如下:

ts 复制代码
 const proxyDocument = new Proxy(
        {},
        {
            get: function (_fakeDocument, propKey) {
                const document = window.document;
                const { shadowRoot, proxyLocation } = iframe.contentWindow.__WUJIE;
                const rawCreateElement = iframe.contentWindow.__WUJIE_RAW_DOCUMENT_CREATE_ELEMENT__;
                const rawCreateTextNode = iframe.contentWindow.__WUJIE_RAW_DOCUMENT_CREATE_TEXT_NODE__;
                // 劫持对document的操作,使之作用于shadowRoot上
                // other proxy code
                if (propKey === "getElementById") {
                    return new Proxy(shadowRoot.querySelector, {
                        apply(target, ctx, args) {
                            // other code
                            target.call(shadowRoot, `[id="${args[0]}"]`)
                            // other code
                        },
                    });
                }
                // other proxy code
                if (propKey === "forms") return shadowRoot.querySelectorAll("form");
                if (propKey === "images") return shadowRoot.querySelectorAll("img");
                if (propKey === "links") return shadowRoot.querySelectorAll("a");
                // other code
            },
        }
    );

从上面的代码中可以看出,proxyDocument主要做了以下几件事情:

  • 劫持document上的部分方法,例如:getElementByIdgetElementsByTagNamegetElementsByClassNamegetElementsByNamequerySelectorquerySelectorAll等,使之作用于shadowRoot根节点进行检索,
  • 劫持document上的部分属性,例如:formsimageslinks等,使之基于shadowRoot根节点进行检索
  • 以上是从子应用获取的方法和属性,还有一些是从主应用获取的方法和属性,这里不过多赘婿

proxyLocation

proxyLocation是用来代理iframelocation对象,使得我们对于location的操作基本上是基于子应用的,源码如下:

ts 复制代码
// proxy location
    const proxyLocation = new Proxy(
        {},
        {
            get: function (_fakeLocation, propKey) {
                const location = iframe.contentWindow.location;
                // proxy location的 href host 等属性
                
                // 修正this 返回location的属性
                return getTargetValue(location, propKey);
            },
            set: function (_fakeLocation, propKey, value) {
                iframe.contentWindow.location[propKey] = value;
                return true;
            }
        }
    );

同理,proxyLocation主要做了以下几件事情:

  • 劫持location上的部分方法,例如:hosthostnameportprotocolorigin等,这些属性的取值,都是基于urlElement的,urlElement(baseUrl为父应用:http://localhost:8000)是子应用的url(http://localhost:7200)解析后的对象%25E6%2598%25AF%25E5%25AD%2590%25E5%25BA%2594%25E7%2594%25A8%25E7%259A%2584url(http%3A%2F%2Flocalhost%3A7200)%25E8%25A7%25A3%25E6%259E%2590%25E5%2590%258E%25E7%259A%2584%25E5%25AF%25B9%25E8%25B1%25A1 "http://localhost:8000)%E6%98%AF%E5%AD%90%E5%BA%94%E7%94%A8%E7%9A%84url(http://localhost:7200)%E8%A7%A3%E6%9E%90%E5%90%8E%E7%9A%84%E5%AF%B9%E8%B1%A1")
  • 其余的方法和属性,都是基于iframelocation对象的,这里不过多赘述
  • 对于location的修改,也都是基于iframelocation对象进行修改

总结

在创建无界实例的时候,第一步是创建沙箱,而创建沙箱有两个关键步骤:

  • 创建iframe,并挂到实例上
  • 对iframe的windowdocumentlocation等对象进行代理,挂到实例上,除少部分属性和方法需要从主应用获取外,大部分都是从子应用获取的

完成创建iframe和代理后,无界实例就创建完成了。顺带提一下,我们在代码中访问windowdocumentlocation等对象时,是如何访问到代理对象上的呢?这里抛出一段无界中的代码,大家可以思考下:

ts 复制代码
    if (!iframeWindow.__WUJIE.degrade && !module) {
    code = `(function(window, self, global, location) {
      ${code}
}).bind(window.__WUJIE.proxy)(
  window.__WUJIE.proxy,
  window.__WUJIE.proxy,
  window.__WUJIE.proxy,
  window.__WUJIE.proxyLocation,
);`;
}

下一期我们将分析wujie-core的沙箱激活机制,敬请期待!

如果觉得本文有帮助 记得点赞三连哦 十分感谢!

系列链接

相关推荐
hackeroink1 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者3 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-3 小时前
验证码机制
前端·后端
燃先生._.4 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖5 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235245 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240256 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar6 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人7 小时前
前端知识补充—CSS
前端·css