簡介
從 HTML Entry 的誕生原因 -> 原理簡述 -> 實際應用 -> 原始碼分析,帶你全方位刨析 HTML Entry 框架。
序言
HTML Entry
這個詞大家可能比較陌生,畢竟在 google
上搜 HTML Entry 是什麼 ?
都搜尋不到正確的結果。但如果你瞭解微前端的話,可能就會有一些瞭解。
致讀者
本著不浪費大家時間的原則,特此說明,如果你能讀懂 HTML Entry 是什麼?? 部分,則可繼續往下閱讀,如果看不懂建議閱讀完推薦資料再回來閱讀
JS Entry 有什麼問題
說到 HTML Entry
就不得不提另外一個詞 JS Entry
,因為 HTML Entry
就是來解決 JS Entry
所面臨的問題的。
微前端領域最著名的兩大框架分別是 single-spa
和 qiankun
,後者是基於前者做了二次封裝,並解決了前者的一些問題。
single-spa
就做了兩件事情:
- 載入微應用(載入方法還得使用者自己來實現)
- 管理微應用的狀態(初始化、掛載、解除安裝)
而 JS Entry
的理念就在載入微應用的時候用到了,在使用 single-spa
載入微應用時,我們載入的不是微應用本身,而是微應用匯出的 JS
檔案,而在入口檔案中會匯出一個物件,這個物件上有 bootstrap
、mount
、unmount
這三個接入 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
就用了這個物件中的 template
、assetPublicPath
和 execScripts
三項,將 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-spa
、qiankun
不可或缺的一部分,原始碼在 github
閱讀到這裡如果你想繼續深入理解 微前端
、single-spa
、qiankun
等,推薦閱讀如下內容
-
-
微前端框架 之 single-spa 從入門到精通
-
微前端框架 之 qiankun 從入門到原始碼分析
-
qiankun 2.x 執行時沙箱 原始碼分析
-
感謝各位的:點贊、收藏和評論,我們下期見。
當學習成為了習慣,知識也就變成了常識,掃碼關注微信公眾號,共同學習、進步。文章已收錄到 github,歡迎 Watch 和 Star。