HTML Entry 源碼分析

簡介

從 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
			// //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 等,推薦閱讀如下內容

感謝各位的:點贊、收藏和評論,我們下期見。


當學習成為了習慣,知識也就變成了常識,掃碼關注微信公眾號,共同學習、進步。文章已收錄到 github,歡迎 Watch 和 Star。

微信公众号

Tags: