HTML Entry 源碼分析


當學習成為了習慣,知識也就變成了常識。感謝各位的 點贊收藏評論

新視頻和文章會第一時間在微信公眾號發送,歡迎關注:李永寧lyn

文章已收錄到 github,歡迎 Watch 和 Star。

簡介

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

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


當學習成為了習慣,知識也就變成了常識。感謝各位的 點贊收藏評論

新視頻和文章會第一時間在微信公眾號發送,歡迎關注:李永寧lyn

文章已收錄到 github,歡迎 Watch 和 Star。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM