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

微信公眾號

Tags: