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

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

系列链接

相关推荐
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅5 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼6 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax