乐趣区

关于javascript:HTML-Entry-源码分析

简介

从 HTML Entry 的诞生起因 -> 原理简述 -> 理论利用 -> 源码剖析,带你全方位刨析 HTML Entry 框架。

序言

HTML Entry 这个词大家可能比拟生疏,毕竟在 google 上搜 HTML Entry 是什么 ? 都搜寻不到正确的后果。但如果你理解微前端的话,可能就会有一些理解。

致读者

本着不节约大家工夫的准则,特此说明,如果你能读懂 HTML Entry 是什么?? 局部,则可持续往下浏览,如果看不懂倡议浏览完举荐材料再回来浏览

JS Entry 有什么问题

说到 HTML Entry 就不得不提另外一个词 JS Entry,因为 HTML Entry 就是来解决 JS Entry 所面临的问题的。

微前端畛域最驰名的两大框架别离是 single-spaqiankun,后者是基于前者做了二次封装,并解决了前者的一些问题。

single-spa 就做了两件事件:

  • 加载微利用(加载办法还得用户本人来实现)
  • 治理微利用的状态(初始化、挂载、卸载)

JS Entry 的理念就在加载微利用的时候用到了,在应用 single-spa 加载微利用时,咱们加载的不是微利用自身,而是微利用导出的 JS 文件,而在入口文件中会导出一个对象,这个对象上有 bootstrapmountunmount 这三个接入 single-spa 框架必须提供的生命周期办法,其中 mount 办法规定了微利用应该怎么挂载到主利用提供的容器节点上,当然你要接入一个微利用,就须要对微利用进行一系列的革新,然而 JS Entry 的问题就出在这儿,革新时对微利用的侵入行太强,而且和主利用的耦合性太强。

single-spa 采纳 JS Entry 的形式接入微利用。微利用革新个别分为三步:

  • 微利用路由革新,增加一个特定的前缀
  • 微利用入口革新,挂载点变更和生命周期函数导出
  • 打包工具配置更改

侵入型强其实说的就是第三点,更改打包工具的配置,应用 single-spa 接入微利用须要将微利用整个打包成一个 JS 文件,公布到动态资源服务器,而后在主利用中配置该 JS 文件的地址通知 single-spa 去这个地址加载微利用。

不说其它的,就当初这个改变就存在很大的问题,将整个微利用打包成一个 JS 文件,常见的打包优化基本上都没了,比方:按需加载、首屏资源加载优化、css 独立打包等优化措施。

留神:子利用也能够将包打成多个,而后利用 webpack 的 webpack-manifest-plugin 插件打包出 manifest.json 文件,生成一份资源清单,而后主利用的 loadApp 近程读取每个子利用的清单文件,顺次加载文件外面的资源;不过该计划也没方法享受子利用的按需加载能力

我的项目公布当前呈现了 bug,修复之后须要更新上线,为了革除浏览器缓存带来的利用,个别文件名会带上 chunkcontent,微利用公布之后文件名都会发生变化,这时候还须要更新主利用中微利用配置,而后从新编译主利用而后公布,这套操作几乎是不能忍耐的,这也是 微前端框架 之 single-spa 从入门到精通 这篇文章中示例我的项目中微利用公布时的环境配置抉择 development 的起因。

qiankun 框架为了解决 JS Entry 的问题,于是采纳了 HTML Entry 的形式,让用户接入微利用就像应用 iframe 一样简略。

如果以上内容没有看懂,则阐明这篇文章不太适宜你浏览,倡议浏览 微前端框架 之 single-spa 从入门到精通,这篇文章具体讲述了 single-spa 的根底应用和源码原理,浏览完当前再回来读这篇文章会有事倍功半的成果,请读者切勿强行浏览,否则可能呈现头昏脑胀的景象。

HTML Entry

HTML Entry 是由 import-html-entry 库实现的,通过 http 申请加载指定地址的首屏内容即 html 页面,而后解析这个 html 模版失去 template, scripts , entry, styles

{
  template: 通过解决的脚本,link、script 标签都被正文掉了,
  scripts: [脚本的 http 地址 或者 { async: true, src: xx} 或者 代码块],
  styles: [款式的 http 地址],
     entry: 入口脚本的地址,要不是标有 entry 的 script 的 src,要不就是最初一个 script 标签的 src
}

而后近程加载 styles 中的款式内容,将 template 模版中正文掉的 link 标签替换为相应的 style 元素。

而后向外裸露一个 Promise 对象

{
  // template 是 link 替换为 style 后的 template
    template: embedHTML,
    // 动态资源地址
    assetPublicPath,
    // 获取内部脚本,最终失去所有脚本的代码内容
    getExternalScripts: () => getExternalScripts(scripts, fetch),
    // 获取内部款式文件的内容
    getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch),
    // 脚本执行器,让 JS 代码 (scripts) 在指定 上下文 中运行
    execScripts: (proxy, strictGlobal) => {if (!scripts.length) {return Promise.resolve();
        }
        return execScripts(entry, scripts, proxy, { fetch, strictGlobal});
    }
}

这就是 HTML Entry 的原理,更具体的内容可持续浏览上面的源码剖析局部

理论利用

qiankun 框架为了解决 JS Entry 的问题,就采纳了 HTML Entry 的形式,让用户接入微利用就像应用 iframe 一样简略。

通过下面的浏览晓得了 HTML Entry 最终会返回一个 Promise 对象,qiankun 就用了这个对象中的 templateassetPublicPathexecScripts 三项,将 template 通过 DOM 操作增加到主利用中,执行 execScripts 办法失去微利用导出的生命周期办法,并且还顺便解决了 JS 全局净化的问题,因为执行 execScripts 办法的时候能够通过 proxy 参数指定 JS 的执行上下文。

更加具体的内容可浏览 微前端框架 之 qiankun 从入门到源码剖析

HTML Entry 源码剖析

importEntry

/**
 * 加载指定地址的首屏内容
 * @param {*} entry 能够是一个字符串格局的地址,比方 localhost:8080,也能够是一个配置对象,比方 {scripts, styles, html}
 * @param {*} opts
 * return importHTML 的执行后果
 */
export function importEntry(entry, opts = {}) {
    // 从 opt 参数中解析出 fetch 办法 和 getTemplate 办法,没有就用默认的
    const {fetch = defaultFetch, getTemplate = defaultGetTemplate} = opts;
    // 获取动态资源地址的一个办法
    const getPublicPath = opts.getPublicPath || opts.getDomain || defaultGetPublicPath;

    if (!entry) {throw new SyntaxError('entry should not be empty!');
    }

    // html entry,entry 是一个字符串格局的地址
    if (typeof entry === 'string') {return importHTML(entry, { fetch, getPublicPath, getTemplate});
    }

    // config entry,entry 是一个对象 = {scripts, styles, html}
    if (Array.isArray(entry.scripts) || Array.isArray(entry.styles)) {const { scripts = [], styles = [], html = ''} = entry;
        const setStylePlaceholder2HTML = tpl => styles.reduceRight((html, styleSrc) => `${genLinkReplaceSymbol(styleSrc)}${html}`, tpl);
        const setScriptPlaceholder2HTML = tpl => scripts.reduce((html, scriptSrc) => `${html}${genScriptReplaceSymbol(scriptSrc)}`, tpl);

        return getEmbedHTML(getTemplate(setScriptPlaceholder2HTML(setStylePlaceholder2HTML(html))), styles, {fetch}).then(embedHTML => ({
            template: embedHTML,
            assetPublicPath: getPublicPath(entry),
            getExternalScripts: () => getExternalScripts(scripts, fetch),
            getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch),
            execScripts: (proxy, strictGlobal) => {if (!scripts.length) {return Promise.resolve();
                }
                return execScripts(scripts[scripts.length - 1], scripts, proxy, {fetch, strictGlobal});
            },
        }));

    } else {throw new SyntaxError('entry scripts or styles should be array!');
    }
}

importHTML

/**
 * 加载指定地址的首屏内容
 * @param {*} url 
 * @param {*} opts 
 * return Promise<{
      // template 是 link 替换为 style 后的 template
        template: embedHTML,
        // 动态资源地址
        assetPublicPath,
        // 获取内部脚本,最终失去所有脚本的代码内容
        getExternalScripts: () => getExternalScripts(scripts, fetch),
        // 获取内部款式文件的内容
        getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch),
        // 脚本执行器,让 JS 代码 (scripts) 在指定 上下文 中运行
        execScripts: (proxy, strictGlobal) => {if (!scripts.length) {return Promise.resolve();
            }
            return execScripts(entry, scripts, proxy, { fetch, strictGlobal});
        },
   }>
 */
export default function importHTML(url, opts = {}) {
    // 三个默认的办法
    let fetch = defaultFetch;
    let getPublicPath = defaultGetPublicPath;
    let getTemplate = defaultGetTemplate;

    if (typeof opts === 'function') {
        // if 分支,兼容遗留的 importHTML api,ops 能够间接是一个 fetch 办法
        fetch = opts;
    } else {
        // 用用户传递的参数(如果提供了的话)笼罩默认办法
        fetch = opts.fetch || defaultFetch;
        getPublicPath = opts.getPublicPath || opts.getDomain || defaultGetPublicPath;
        getTemplate = opts.getTemplate || defaultGetTemplate;
    }

    // 通过 fetch 办法申请 url,这也就是 qiankun 为什么要求你的微利用要反对跨域的起因
    return embedHTMLCache[url] || (embedHTMLCache[url] = fetch(url)
        // response.text() 是一个 html 模版
        .then(response => response.text())
        .then(html => {

            // 获取动态资源地址
            const assetPublicPath = getPublicPath(url);
            /**
          * 从 html 模版中解析出内部脚本的地址或者内联脚本的代码块 和 link 标签的地址
             * {
              *     template: 通过解决的脚本,link、script 标签都被正文掉了,
       *     scripts: [脚本的 http 地址 或者 { async: true, src: xx} 或者 代码块],
       *  styles: [款式的 http 地址],
          *     entry: 入口脚本的地址,要不是标有 entry 的 script 的 src,要不就是最初一个 script 标签的 src
              * }
             */
            const {template, scripts, entry, styles} = processTpl(getTemplate(html), assetPublicPath);

            // getEmbedHTML 办法通过 fetch 近程加载所有的内部款式,而后将对应的 link 正文标签替换为 style,即内部款式替换为内联款式,而后返回 embedHTML,即解决过后的 HTML 模版
            return getEmbedHTML(template, styles, { fetch}).then(embedHTML => ({
                // template 是 link 替换为 style 后的 template
                template: embedHTML,
                // 动态资源地址
                assetPublicPath,
                // 获取内部脚本,最终失去所有脚本的代码内容
                getExternalScripts: () => getExternalScripts(scripts, fetch),
                // 获取内部款式文件的内容
                getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch),
                // 脚本执行器,让 JS 代码 (scripts) 在指定 上下文 中运行
                execScripts: (proxy, strictGlobal) => {if (!scripts.length) {return Promise.resolve();
                    }
                    return execScripts(entry, scripts, proxy, { fetch, strictGlobal});
                },
            }));
        }));
}

processTpl

/**
 * 从 html 模版中解析出内部脚本的地址或者内联脚本的代码块 和 link 标签的地址
 * @param tpl html 模版
 * @param baseURI
 * @stripStyles whether to strip the css links
 * @returns {{template: void | string | *, scripts: *[], entry: *}}
 * return {
 *     template: 通过解决的脚本,link、script 标签都被正文掉了,
 *     scripts: [脚本的 http 地址 或者 { async: true, src: xx} 或者 代码块],
 *  styles: [款式的 http 地址],
 *     entry: 入口脚本的地址,要不是标有 entry 的 script 的 src,要不就是最初一个 script 标签的 src
 * }
 */
export default function processTpl(tpl, baseURI) {let scripts = [];
    const styles = [];
    let entry = null;
    // 判断浏览器是否反对 es module,<script type = "module" />
    const moduleSupport = isModuleScriptSupported();

    const template = tpl

        // 移除 html 模版中的正文内容 <!-- xx -->
        .replace(HTML_COMMENT_REGEX, '')

        // 匹配 link 标签
        .replace(LINK_TAG_REGEX, match => {
            /**
             * 将模版中的 link 标签变成正文,如果有存在 href 属性且非预加载的 link,则将地址存到 styles 数组,如果是预加载的 link 间接变成正文
             */
            // <link rel = "stylesheet" />
            const styleType = !!match.match(STYLE_TYPE_REGEX);
            if (styleType) {

                // <link rel = "stylesheet" href = "xxx" />
                const styleHref = match.match(STYLE_HREF_REGEX);
                // <link rel = "stylesheet" ignore />
                const styleIgnore = match.match(LINK_IGNORE_REGEX);

                if (styleHref) {

                    // 获取 href 属性值
                    const href = styleHref && styleHref[2];
                    let newHref = href;

                    // 如果 href 没有协定阐明给的是一个绝对地址,拼接 baseURI 失去残缺地址
                    if (href && !hasProtocol(href)) {newHref = getEntirePath(href, baseURI);
                    }
                    // 将 <link rel = "stylesheet" ignore /> 变成 <!-- ignore asset ${url} replaced by import-html-entry -->
                    if (styleIgnore) {return genIgnoreAssetReplaceSymbol(newHref);
                    }

                    // 将 href 属性值存入 styles 数组
                    styles.push(newHref);
                    // <link rel = "stylesheet" href = "xxx" /> 变成 <!-- link ${linkHref} replaced by import-html-entry -->
                    return genLinkReplaceSymbol(newHref);
                }
            }

            // 匹配 <link rel = "preload or prefetch" href = "xxx" />,示意预加载资源
            const preloadOrPrefetchType = match.match(LINK_PRELOAD_OR_PREFETCH_REGEX) && match.match(LINK_HREF_REGEX) && !match.match(LINK_AS_FONT);
            if (preloadOrPrefetchType) {
                // 失去 href 地址
                const [, , linkHref] = match.match(LINK_HREF_REGEX);
                // 将标签变成 <!-- prefetch/preload link ${linkHref} replaced by import-html-entry -->
                return genLinkReplaceSymbol(linkHref, true);
            }

            return match;
        })
        // 匹配 <style></style>
        .replace(STYLE_TAG_REGEX, match => {if (STYLE_IGNORE_REGEX.test(match)) {
                // <style ignore></style> 变成 <!-- ignore asset style file replaced by import-html-entry -->
                return genIgnoreAssetReplaceSymbol('style file');
            }
            return match;
        })
        // 匹配 <script></script>
        .replace(ALL_SCRIPT_REGEX, (match, scriptTag) => {
            // 匹配 <script ignore></script>
            const scriptIgnore = scriptTag.match(SCRIPT_IGNORE_REGEX);
            // 匹配 <script nomodule></script> 或者 <script type = "module"></script>,都属于应该被疏忽的脚本
            const moduleScriptIgnore =
                (moduleSupport && !!scriptTag.match(SCRIPT_NO_MODULE_REGEX)) ||
                (!moduleSupport && !!scriptTag.match(SCRIPT_MODULE_REGEX));
            // in order to keep the exec order of all javascripts

            // <script type = "xx" />
            const matchedScriptTypeMatch = scriptTag.match(SCRIPT_TYPE_REGEX);
            // 获取 type 属性值
            const matchedScriptType = matchedScriptTypeMatch && matchedScriptTypeMatch[2];
            // 验证 type 是否无效,type 为空 或者 'text/javascript', 'module', 'application/javascript', 'text/ecmascript', 'application/ecmascript',都视为无效
            if (!isValidJavaScriptType(matchedScriptType)) {return match;}

            // if it is a external script,匹配非 <script type = "text/ng-template" src = "xxx"></script>
            if (SCRIPT_TAG_REGEX.test(match) && scriptTag.match(SCRIPT_SRC_REGEX)) {
                /*
                collect scripts and replace the ref
                */

                // <script entry />
                const matchedScriptEntry = scriptTag.match(SCRIPT_ENTRY_REGEX);
                // <script src = "xx" />
                const matchedScriptSrcMatch = scriptTag.match(SCRIPT_SRC_REGEX);
                // 脚本地址
                let matchedScriptSrc = matchedScriptSrcMatch && matchedScriptSrcMatch[2];

                if (entry && matchedScriptEntry) {
                    // 阐明呈现了两个入口地址,即两个 <script entry src = "xx" />
                    throw new SyntaxError('You should not set multiply entry script!');
                } else {
                    // 补全脚本地址,地址如果没有协定,阐明是一个相对路径,增加 baseURI
                    if (matchedScriptSrc && !hasProtocol(matchedScriptSrc)) {matchedScriptSrc = getEntirePath(matchedScriptSrc, baseURI);
                    }

                    // 脚本的入口地址
                    entry = entry || matchedScriptEntry && matchedScriptSrc;
                }

                if (scriptIgnore) {// <script ignore></script> 替换为 <!-- ignore asset ${url || 'file'} replaced by import-html-entry -->
                    return genIgnoreAssetReplaceSymbol(matchedScriptSrc || 'js file');
                }

                if (moduleScriptIgnore) {
                    // <script nomodule></script> 或者 <script type = "module"></script> 替换为
                    // <!-- nomodule script ${scriptSrc} ignored by import-html-entry --> 或
                    // <!-- module script ${scriptSrc} ignored by import-html-entry -->
                    return genModuleScriptReplaceSymbol(matchedScriptSrc || 'js file', moduleSupport);
                }

                if (matchedScriptSrc) {
                    // 匹配 <script src = 'xx' async />,阐明是异步加载的脚本
                    const asyncScript = !!scriptTag.match(SCRIPT_ASYNC_REGEX);
                    // 将脚本地址存入 scripts 数组,如果是异步加载,则存入一个对象 {async: true, src: xx}
                    scripts.push(asyncScript ? { async: true, src: matchedScriptSrc} : matchedScriptSrc);
                    // <script src = "xx" async /> 或者 <script src = "xx" /> 替换为 
                    // <!-- async script ${scriptSrc} replaced by import-html-entry --> 或 
                    // <!-- script ${scriptSrc} replaced by import-html-entry -->
                    return genScriptReplaceSymbol(matchedScriptSrc, asyncScript);
                }

                return match;
            } else {
                // 阐明是外部脚本,<script>xx</script>
                if (scriptIgnore) {
                    // <script ignore /> 替换为 <!-- ignore asset js file replaced by import-html-entry -->
                    return genIgnoreAssetReplaceSymbol('js file');
                }

                if (moduleScriptIgnore) {
                    // <script nomodule></script> 或者 <script type = "module"></script> 替换为
                    // <!-- nomodule script ${scriptSrc} ignored by import-html-entry --> 或 
                    // <!-- module script ${scriptSrc} ignored by import-html-entry -->
                    return genModuleScriptReplaceSymbol('js file', moduleSupport);
                }

                // if it is an inline script,<script>xx</script>,失去标签之间的代码 => xx
                const code = getInlineCode(match);

                // remove script blocks when all of these lines are comments. 判断代码块是否全是正文
                const isPureCommentBlock = code.split(/[\r\n]+/).every(line => !line.trim() || line.trim().startsWith('//'));

                if (!isPureCommentBlock) {
                    // 不是正文,则将代码块存入 scripts 数组
                    scripts.push(match);
                }

                // <script>xx</script> 替换为 <!-- inline scripts replaced by import-html-entry -->
                return inlineScriptReplaceSymbol;
            }
        });

    // filter empty script
    scripts = scripts.filter(function (script) {return !!script;});

    return {
        template,
        scripts,
        styles,
        // set the last script as entry if have not set
        entry: entry || scripts[scripts.length - 1],
    };
}

getEmbedHTML

/**
 * convert external css link to inline style for performance optimization,内部款式转换成内联款式
 * @param template,html 模版
 * @param styles link 款式链接
 * @param opts = {fetch}
 * @return embedHTML 解决过后的 html 模版
 */
function getEmbedHTML(template, styles, opts = {}) {const { fetch = defaultFetch} = opts;
    let embedHTML = template;

    return getExternalStyleSheets(styles, fetch)
        .then(styleSheets => {
            // 通过循环,将之前设置的 link 正文标签替换为 style 标签,即 <style>/* href 地址 */ xx </style>
            embedHTML = styles.reduce((html, styleSrc, i) => {html = html.replace(genLinkReplaceSymbol(styleSrc), `<style>/* ${styleSrc} */${styleSheets[i]}</style>`);
                return html;
            }, embedHTML);
            return embedHTML;
        });
}

getExternalScripts

/**
 * 加载脚本,最终返回脚本的内容,Promise<Array>,每个元素都是一段 JS 代码
 * @param {*} scripts = [脚本 http 地址 or 内联脚本的脚本内容 or { async: true, src: xx}]
 * @param {*} fetch 
 * @param {*} errorCallback 
 */
export function getExternalScripts(scripts, fetch = defaultFetch, errorCallback = () => {}) {

    // 定义一个能够加载近程指定 url 脚本的办法,当然外面也做了缓存,如果命中缓存间接从缓存中获取
    const fetchScript = scriptUrl => scriptCache[scriptUrl] ||
        (scriptCache[scriptUrl] = fetch(scriptUrl).then(response => {
            // usually browser treats 4xx and 5xx response of script loading as an error and will fire a script error event
            // https://stackoverflow.com/questions/5625420/what-http-headers-responses-trigger-the-onerror-handler-on-a-script-tag/5625603
            if (response.status >= 400) {errorCallback();
                throw new Error(`${scriptUrl} load failed with status ${response.status}`);
            }

            return response.text();}));

    return Promise.all(scripts.map(script => {if (typeof script === 'string') {
                // 字符串,要不是链接地址,要不是脚本内容(代码)if (isInlineCode(script)) {
                    // if it is inline script
                    return getInlineCode(script);
                } else {
                    // external script,加载脚本
                    return fetchScript(script);
                }
            } else {
                // use idle time to load async script
                // 异步脚本,通过 requestIdleCallback 办法加载
                const {src, async} = script;
                if (async) {
                    return {
                        src,
                        async: true,
                        content: new Promise((resolve, reject) => requestIdleCallback(() => fetchScript(src).then(resolve, reject))),
                    };
                }

                return fetchScript(src);
            }
        },
    ));
}

getExternalStyleSheets

/**
 * 通过 fetch 办法加载指定地址的款式文件
 * @param {*} styles = [href]
 * @param {*} fetch 
 * return Promise<Array>,每个元素都是一堆款式内容
 */
export function getExternalStyleSheets(styles, fetch = defaultFetch) {
    return Promise.all(styles.map(styleLink => {if (isInlineCode(styleLink)) {
                // if it is inline style
                return getInlineCode(styleLink);
            } else {
                // external styles,加载款式并缓存
                return styleCache[styleLink] ||
                    (styleCache[styleLink] = fetch(styleLink).then(response => response.text()));
            }

        },
    ));
}

execScripts

/**
 * FIXME to consistent with browser behavior, we should only provide callback way to invoke success and error event
 * 脚本执行器,让指定的脚本 (scripts) 在规定的上下文环境中执行
 * @param entry 入口地址
 * @param scripts = [脚本 http 地址 or 内联脚本的脚本内容 or { async: true, src: xx}] 
 * @param proxy 脚本执行上下文,全局对象,qiankun JS 沙箱生成 windowProxy 就是传递到了这个参数
 * @param opts
 * @returns {Promise<unknown>}
 */
export function execScripts(entry, scripts, proxy = window, opts = {}) {
    const {fetch = defaultFetch, strictGlobal = false, success, error = () => {}, beforeExec = () => {},} = opts;

    // 获取指定的所有内部脚本的内容,并设置每个脚本的执行上下文,而后通过 eval 函数运行
    return getExternalScripts(scripts, fetch, error)
        .then(scriptsText => {
            // scriptsText 为脚本内容数组 => 每个元素是一段 JS 代码
            const geval = (code) => {beforeExec();
                (0, eval)(code);
            };

            /**
             * 
             * @param {*} scriptSrc 脚本地址
             * @param {*} inlineScript 脚本内容
             * @param {*} resolve 
             */
            function exec(scriptSrc, inlineScript, resolve) {

                // 性能度量
                const markName = `Evaluating script ${scriptSrc}`;
                const measureName = `Evaluating Time Consuming: ${scriptSrc}`;

                if (process.env.NODE_ENV === 'development' && supportsUserTiming) {performance.mark(markName);
                }

                if (scriptSrc === entry) {
                    // 入口
                    noteGlobalProps(strictGlobal ? proxy : window);

                    try {
                        // bind window.proxy to change `this` reference in script
                        geval(getExecutableScript(scriptSrc, inlineScript, proxy, strictGlobal));
                        const exports = proxy[getGlobalProp(strictGlobal ? proxy : window)] || {};
                        resolve(exports);
                    } catch (e) {
                        // entry error must be thrown to make the promise settled
                        console.error(`[import-html-entry]: error occurs while executing entry script ${scriptSrc}`);
                        throw e;
                    }
                } else {if (typeof inlineScript === 'string') {
                        try {
                            // bind window.proxy to change `this` reference in script,就是设置 JS 代码的执行上下文,而后通过 eval 函数运行运行代码
                            geval(getExecutableScript(scriptSrc, inlineScript, proxy, strictGlobal));
                        } catch (e) {
                            // consistent with browser behavior, any independent script evaluation error should not block the others
                            throwNonBlockingError(e, `[import-html-entry]: error occurs while executing normal script ${scriptSrc}`);
                        }
                    } else {
                        // external script marked with async,异步加载的代码,下载完当前运行
                        inlineScript.async && inlineScript?.content
                            .then(downloadedScriptText => geval(getExecutableScript(inlineScript.src, downloadedScriptText, proxy, strictGlobal)))
                            .catch(e => {throwNonBlockingError(e, `[import-html-entry]: error occurs while executing async script ${inlineScript.src}`);
                            });
                    }
                }

                // 性能度量
                if (process.env.NODE_ENV === 'development' && supportsUserTiming) {performance.measure(measureName, markName);
                    performance.clearMarks(markName);
                    performance.clearMeasures(measureName);
                }
            }

            /**
             * 递归
             * @param {*} i 示意第几个脚本
             * @param {*} resolvePromise 胜利回调 
             */
            function schedule(i, resolvePromise) {if (i < scripts.length) {
                    // 第 i 个脚本的地址
                    const scriptSrc = scripts[i];
                    // 第 i 个脚本的内容
                    const inlineScript = scriptsText[i];

                    exec(scriptSrc, inlineScript, resolvePromise);
                    if (!entry && i === scripts.length - 1) {
                        // resolve the promise while the last script executed and entry not provided
                        resolvePromise();} else {
                        // 递归调用下一个脚本
                        schedule(i + 1, resolvePromise);
                    }
                }
            }

            // 从第 0 个脚本开始调度
            return new Promise(resolve => schedule(0, success || resolve));
        });
}

结语

以上就是 HTML Entry 的全部内容,也是深刻了解 微前端single-spaqiankun 不可或缺的一部分,源码在 github

浏览到这里如果你想持续深刻了解 微前端single-spaqiankun 等,举荐浏览如下内容

  • 微前端专栏

    • 微前端框架 之 single-spa 从入门到精通
    • 微前端框架 之 qiankun 从入门到源码剖析
    • qiankun 2.x 运行时沙箱 源码剖析
  • single-spa 官网
  • qiankun 官网

感激各位的:点赞 珍藏 评论,咱们下期见。


当学习成为了习惯,常识也就变成了常识,扫码关注微信公众号,独特学习、提高。文章已收录到 github,欢送 Watch 和 Star。

退出移动版