简介

从 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 等,推荐阅读如下内容

感谢各位的:点赞收藏评论,我们下期见。


当学习成为了习惯,知识也就变成了常识,扫码关注微信公众号,共同学习、进步。文章已收录到 github,欢迎 Watch 和 Star。

HTML Entry 源码分析的更多相关文章

  1. 微前端框架 之 qiankun 从入门到源码分析

    封面 简介 从 single-spa 的缺陷讲起 -> qiankun 是如何从框架层面解决 single-spa 存在的问题 -> qiankun 源码解读,带你全方位刨析 qianku ...

  2. qiankun 2.x 运行时沙箱 源码分析

    简介 从源码层面详细讲解了 qiankun 框架中的 JS 沙箱 和 样式沙箱的实现原理. 序言 沙箱 这个词想必大家应该不陌生,即使陌生,读完这篇文章也就不那么陌生了 沙箱 (Sandboxie) ...

  3. HashMap与TreeMap源码分析

    1. 引言     在红黑树--算法导论(15)中学习了红黑树的原理.本来打算自己来试着实现一下,然而在看了JDK(1.8.0)TreeMap的源码后恍然发现原来它就是利用红黑树实现的(很惭愧学了Ja ...

  4. u-boot源码分析之C语言段

    题外话: 最近一直在学习u-boot的源代码,从代码量到代码风格,都让我认识到什么才是真正的程序.以往我所学到的C语言知识和u-boot的源代码相比,实在不值一提.说到底,机器都是0和1控制的.感觉这 ...

  5. ThreadLocal 工作原理、部分源码分析

    1.大概去哪里看 ThreadLocal 其根本实现方法,是在Thread里面,有一个ThreadLocal.ThreadLocalMap属性 ThreadLocal.ThreadLocalMap t ...

  6. MyCat源码分析系列之——结果合并

    更多MyCat源码分析,请戳MyCat源码分析系列 结果合并 在SQL下发流程和前后端验证流程中介绍过,通过用户验证的后端连接绑定的NIOHandler是MySQLConnectionHandler实 ...

  7. JDK1.8 HashMap 源码分析

    一.概述 以键值对的形式存储,是基于Map接口的实现,可以接收null的键值,不保证有序(比如插入顺序),存储着Entry(hash, key, value, next)对象. 二.示例 public ...

  8. Java并发包源码分析

    并发是一种能并行运行多个程序或并行运行一个程序中多个部分的能力.如果程序中一个耗时的任务能以异步或并行的方式运行,那么整个程序的吞吐量和可交互性将大大改善.现代的PC都有多个CPU或一个CPU中有多个 ...

  9. MyBatis源码分析(3)—— Cache接口以及实现

    @(MyBatis)[Cache] MyBatis源码分析--Cache接口以及实现 Cache接口 MyBatis中的Cache以SPI实现,给需要集成其它Cache或者自定义Cache提供了接口. ...

随机推荐

  1. 1114 - Easily Readable

    1114 - Easily Readable    PDF (English) Statistics Forum Time Limit: 2 second(s) Memory Limit: 32 MB ...

  2. Docker 与 K8S学习笔记(五)—— 容器的操作(上篇)

    上一篇我们介绍了Dockerfile的基本编写方法,这一节我们来看看Docker容器的常用操作. 一.容器的运行方式 容器有两种运行方式,即daemon形式运行与非daemon形式运行,通俗地讲就是长 ...

  3. Masked Gradient-Based Causal Structure Learning

    目录 概 主要内容 最终的目标 代码 Ng I., Fang Z., Zhu S., Chen Z. and Wang J. Masked Gradient-Based Causal Structur ...

  4. A Tutorial on Energy-Based Learning

    目录 概 主要内容 损失函数 Energy Loss Generalized Perceptron Loss Generalized Margin Loss Hinge Loss Log Loss L ...

  5. CS5216 设计于DP转HDMI转换器|DP转HDMI 1080P中继器 电平转化器开关设计方案与线路图

    CS5216是一款Displayport to hdmi 1080p音视频信号转换芯片,主要用于设计与开发DP转HDMI 转换器.中继器.电平转换器等产品当中.它支持交流和直流耦合TMDS信号高达1. ...

  6. MA8621带SD读卡的USB 2.0高速3端口HUB方案芯片|MA8621中文规格书|USB 2.0方案

    MA8621说明 MA8621是USB 2.0高速3端口集线器的高性能解决方案,带有SD卡控制器,完全符合通用串行总线规范2.0.控制器继承了先进的串行接口技术,当3个DS(下游)端口同时工作时,功耗 ...

  7. 引用element-ui的Drawer抽屉组件报错问题

    前提:vue项目采取按需引入的方式引入element,并且使用其他组件都正常,没有发生异常 问题表现: 在vue项目中引用了Drawer 抽屉组件,结果报错 意思就是组件未注册,当时我的表情: 没办法 ...

  8. Sentry 企业级数据安全解决方案 - Relay 入门

    内容整理自官方开发文档 Sentry Relay 通过提供作为应用程序和 sentry.io 之间中间层的独立服务来提供企业级数据安全性. Relay 专门设计用于: 在将个人身份信息 (PII) 发 ...

  9. SpringBoot集成MyBatis-Plus框架详细方法

    1.说明 本文详细介绍Spring Boot集成MyBatis-Plus框架的方法, 使用MySQL数据库进行测试, 包括完整的开发到测试步骤, 从一开始的Spring Boot工程创建, 到MySQ ...

  10. Eclipse导入Zookeeper源码Version2017.11.3

    将Zookeeper源码导入Eclipse, Zookeeper源码需要使用ant构建后才能导入Eclipse, 和Solr的源码一样也是使用ant构建的, 大部分可以参考Eclipse导入Solr源 ...