微前端(wujie)源码解析-6.wujie-core沙箱激活

无界沙箱激活

前言

上一篇文章我们分析了无界沙箱的创建过程,在创建过程中主要是创建了iframe以及对iframewindowdocumentlocation等进行了代理,

到目前为止,我们的准备工作已经做完了,有了一个子应用的壳子iframe,但是子应用还没有真正的运行起来,在无界源码中,有一个子应用的激活过程,我们今天看下激活过程。

子应用静态资源获取

在激活子应用之前,我们需要先获取子应用的静态资源,代码如下:

ts 复制代码
const { template, getExternalScripts, getExternalStyleSheets } = await importHTML({
    url,
    html,
    opts: {
        fetch: fetch || window.fetch,
        plugins: newSandbox.plugins,
        loadError: newSandbox.lifecycles.loadError,
        fiber,
    },
});

importHTML函数用来获取子应用的静态资源,template为子应用的html模板,getExternalScripts为子应用的js资源,getExternalStyleSheets为子应用的css资源, 用官方例子,我们可以看到template为:

html 复制代码
<!DOCTYPE html>
<html lang="">
<head>
  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width,initial-scale=1.0">
  <link rel="icon" href="favicon.ico">
  <title>vue2</title>
  <!-- prefetch/preload/modulepreload link js/Page1.js replaced by wujie --><!-- prefetch/preload/modulepreload link js/Page2.js replaced by wujie --><!-- prefetch/preload/modulepreload link js/Page3.js replaced by wujie --><!-- prefetch/preload/modulepreload link js/app.js replaced by wujie --><!-- prefetch/preload/modulepreload link js/chunk-vendors.js replaced by wujie --></head>
<body>
<noscript>
  <strong>We're sorry but vue2 doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>

<!--  script http://localhost:7200/js/chunk-vendors.js replaced by wujie --><!--  script http://localhost:7200/js/app.js replaced by wujie --></body>
</html>

其中有一个细节点,是子应用的js资源被注释了,之所以这样处理,是要让子应用的js资源不被浏览器加载到webComponent中,而是由无界来加载到iframe中,被注释掉的js资源通过getExternalScripts可以获取到,最终通过getExternalScripts返回的js资源结果如下:

ts 复制代码
[
    { src: 'http://localhost:7200/js/chunk-vendors.js' }, 
    { src: 'http://localhost:7200/js/app.js' }
]

这里我们先有个大致的了解,方便后面分析。至于importHTML函数的实现,大家有兴趣可以自己看下,这里不是我们的重点。

小结: 在激活子应用之前,我们需要先获取子应用的静态资源,包括html模板、js资源、css资源等,其中js、css外链资源被注释掉,后续可以通过提供的函数获取到。

激活子应用

子应用的资源准备好后,源码中还有一个处理processCssLoader函数,用来处理css资源,这里我们先跳过,大家可以把processedHtml等价于我们刚刚获取的子应用的template

ts 复制代码
const processedHtml = await processCssLoader(newSandbox, template, getExternalStyleSheets);
await newSandbox.active({ url, sync, prefix, template: processedHtml, el, props, alive, fetch, replace });

那么加下来,我们就来分析active函数,active函数的实现如下:

ts 复制代码
export default class Wujie {
    public async active(options: {
        url: string;
        sync?: boolean;
        prefix?: { [key: string]: string };
        template?: string;
        el?: string | HTMLElement;
        props?: { [key: string]: any };
        alive?: boolean;
        fetch?: (input: RequestInfo, init?: RequestInit) => Promise<Response>;
        replace?: (code: string) => string;
    }): Promise<void> {
        const { sync, url, el, template, props, alive, prefix, fetch, replace } = options;
        this.url = url;
        this.sync = sync;
        this.alive = alive;
        this.hrefFlag = false;
        this.prefix = prefix ?? this.prefix;
        this.replace = replace ?? this.replace;
        this.provide.props = props ?? this.provide.props;
        this.activeFlag = true;
        // wait iframe init
        await this.iframeReady;

        // 处理子应用自定义fetch,暂时不分析

        // 处理子应用路由同步
        // 先将url同步回iframe,然后再同步回浏览器url
        syncUrlToIframe(iframeWindow);
        syncUrlToWindow(iframeWindow);

        // inject template
        this.template = template ?? this.template;

        if (this.shadowRoot) {
          // ...
        } else {
            // 预执行无容器,暂时插入iframe内部触发Web Component的connect
            const iframeBody = rawDocumentQuerySelector.call(iframeWindow.document, "body") as HTMLElement;
            this.el = renderElementToContainer(createWujieWebComponent(this.id), el ?? iframeBody);
        }
        
        await renderTemplateToShadowRoot(this.shadowRoot, iframeWindow, this.template);
        this.patchCssRules();

        // inject shadowRoot to app
        this.provide.shadowRoot = this.shadowRoot;
    }   
}

激活步骤可以分为以下几步:

  • 等待iframe初始化完成,初始化阶段首先将iframe的dom结构做了基本的填充,然后对iframe做了一些patch处理,这里我们不做展开分析,有兴趣的可以去看看initIframeDom这个函数,我们罗列两个重要的操作,比如
    • 重写iframe的history的pushState和replaceState方法,同步路由到主应用
    • 监听hashchange和popstate事件,同步路由到主应用
  • 处理子应用路由同步,首先是将url同步到iframe,然后再同步回浏览器url,这里的逻辑,以后有机会再看
  • 创建自定义组件,并插入到子应用的容器中,这里我们通过代码来看下:
ts 复制代码
this.el = renderElementToContainer(createWujieWebComponent(this.id), el ?? iframeBody);

export function createWujieWebComponent(id: string): HTMLElement {
    const contentElement = window.document.createElement("wujie-app");
    contentElement.setAttribute(WUJIE_APP_ID, id);
    contentElement.classList.add(WUJIE_IFRAME_CLASS);
    return contentElement;
}
export function renderElementToContainer(
    element: Element | ChildNode,
    selectorOrElement: string | HTMLElement
): HTMLElement {
    // 代码有删减,这里只看核心逻辑
    const container = getContainer(selectorOrElement);
    rawElementAppendChild.call(container, element);
    return container;
}

这段代码的目的是要创建一个webComponent,并插入到子应用容器中,创建的过程比较简单,插入的时候要找到容器,然后插入到容器中,那么容器是什么呢,还记得我们在主应用中,承载子应用使用的<WujieVue>吗,这个组件的根节点就是我们的容器,我们在回顾下<WujieVue>的组件定义:

js 复制代码
 render(c) {
    return c("div", {
      style: {
        width: this.width,
        height: this.height,
      },
      ref: "wujie",
    });
  }

如上代码所示,这里的div就是我们的容器,那么我们的webComponent就是插入到这个容器中

  • 将子应用的html模板渲染到webComponent中 将子应用的html模板渲染到webComponent是通过renderTemplateToShadowRoot函数实现的:
ts 复制代码
    await renderTemplateToShadowRoot(this.shadowRoot, iframeWindow, this.template);

函数的第一个入参是shadowRoot,那么shadowRoot是什么呢,什么时候创建的呢? 我们把时间线拨回到上一步创建webComponent前,我们通过判断if (this.shadowRoot)来创建并插入webComponent,当我们执行到renderTemplateToShadowRoot的时候, shadowRoot已经存在了,这是因为在创建并插入webComponent的时候,触发了webComponentconnectedCallback生命周期,我们来看下具体的实现:

ts 复制代码
export function defineWujieWebComponent() {
    const customElements = window.customElements;
    if (customElements && !customElements?.get("wujie-app")) {
        class WujieApp extends HTMLElement {
            connectedCallback(): void {
                if (this.shadowRoot) return;
                const shadowRoot = this.attachShadow({ mode: "open" });
                const sandbox = getWujieById(this.getAttribute(WUJIE_APP_ID));
                patchElementEffect(shadowRoot, sandbox.iframe.contentWindow);
                sandbox.shadowRoot = shadowRoot;
            }

            disconnectedCallback(): void {
                const sandbox = getWujieById(this.getAttribute(WUJIE_APP_ID));
                sandbox?.unmount();
            }
        }
        customElements?.define("wujie-app", WujieApp);
    }
}

关于webComponent的生命周期,如果有不了解的可以自行查阅,这里不做展开,我们看关键步骤

ts 复制代码
sandbox.shadowRoot = shadowRoot;

connectedCallback生命周期中,我们将shadowRoot赋值给了sandbox,这样我们就可以在renderTemplateToShadowRoot中使用shadowRoot了, 这里可能有同学会好奇什么时候定义的自定义组件呢,其实defineWujieWebComponent函数在wujie-core的入口文件index.js中已经被执行过了,换言之我们在主项目中通过import WujieVue from "wujie-vue2";引入无界时,已经被调用了

ts 复制代码
// index.js
// 定义webComponent容器
defineWujieWebComponent()

搞清楚这些以后,我们回过头继续看下renderTemplateToShadowRoot如何将子应用的模板渲染到webComponent中的:

ts 复制代码
export async function renderTemplateToShadowRoot(
    shadowRoot: ShadowRoot,
    iframeWindow: Window,
    template: string
): Promise<void> {
    // 将子应用的html模板渲染为dom元素
    const html = renderTemplateToHtml(iframeWindow, template);
    // other code
    // change ownerDocumen
    // 将子应用的html插入到shadowRoot中
    shadowRoot.appendChild(processedHtml);
    const shade = document.createElement("div");
    shade.setAttribute("style", WUJIE_SHADE_STYLE);
    processedHtml.insertBefore(shade, processedHtml.firstChild);
    shadowRoot.head = shadowRoot.querySelector("head");
    shadowRoot.body = shadowRoot.querySelector("body");

    // 修复 html parentNode
    Object.defineProperty(shadowRoot.firstChild, "parentNode", {
        enumerable: true,
        configurable: true,
        get: () => iframeWindow.document,
    });

    // 修复工作
    patchRenderEffect(shadowRoot, iframeWindow.__WUJIE.id, false);
}

renderTemplateToShadowRoot主要做了以下三件事:

  • 将子应用的模板字符串template渲染为dom元素
  • 将子应用的dom元素插入到shadowRoot中,shadowRoot就是我们上一步创建的webComponentshadowRoot
  • 其他工作,这里我们不做展开

小结:激活子应用的过程,主要是将子应用的模板渲染到webComponent中,这里的webComponent就是我们在主应用中承载子应用的<WujieVue>组件

总结

子应用的激活过程我们分析完成了,这里我们简单总结下:

  • 激活子应用之前,需要先获取子应用的静态资源,包括html模板、js资源、css资源等,其中js、css外链资源被注释掉,后续可以通过提供的函数获取到。
  • 激活子应用的过程,主要是将子应用的模板渲染到shadowRoot中,shadowRoot又插入到容器中,容器就是就是我们在主应用中承载子应用的<WujieVue>组件

经过以上工作,我们的子应用的激活过程就完成了。

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

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

系列链接

相关推荐
一个处女座的程序猿O(∩_∩)O1 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
hackeroink4 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者5 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-6 小时前
验证码机制
前端·后端
燃先生._.7 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭7 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
高山我梦口香糖8 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235248 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240259 小时前
前端如何检测用户登录状态是否过期
前端