HTML Entry 原始碼分析

李永寧發表於2022-02-05

簡介

從 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。

微信公眾號

相關文章