微前端(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的沙箱激活机制,敬请期待!

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

系列链接

相关推荐
To_OC7 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
To_OC8 小时前
LC 17 电话号码的字母组合:我的回溯算法,就是从这道题开窍的
javascript·算法·leetcode
vipbic8 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy8 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志8 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble10 小时前
c端系统,其实更像一个信息展示平台
前端
你怎么知道我是队长10 小时前
JavaScript的变量和数据类型介绍
开发语言·javascript·ecmascript
李姆斯10 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜11 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司11 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能