The preface

The term HTML Entry is probably unfamiliar to you, after all, what is an HTML Entry on Google? I can’t find the right results. But if you know the micro front, you might know something about it.

To the reader

In the interest of not wasting your time, if you can read an HTML Entry? If you don’t understand the recommendation, read the recommendation and come back

What’s wrong with JS Entry

When talking about HTML Entry, we have to mention another word JS Entry, because HTML Entry is to solve the problems faced by JS Entry.

The two most famous micro front end frameworks are single-SPA and Qiankun. The latter is based on the former to do a second package and solve some problems of the former.

Single-spa does two things:

  • Loading the microapplication (the loading method must be implemented by the user)
  • Manage the status of microapplications (initialization, mount, unmount)

The concept of JS Entry is used when loading micro-applications. When using single-SPA to load micro-applications, what we load is not the micro-application itself, but the exported JS file of micro-applications, and an object will be exported in the Entry file. This object has three lifecycle methods: bootstrap, mount, and unmount. The mount method specifies how the microapplication should be mounted to the container node provided by the master application, if you want to access a microapplication. It requires a series of modifications to the micro application. However, the problem with JS Entry is that the transformation is too intrusive to the micro application and too coupled to the main application.

Single-spa uses JS Entry to access micro-applications. Microapplication transformation is generally divided into three steps:

  • Microapply routing retooling to add a specific prefix
  • Microapplication entry modification, mount point change and life cycle function export
  • Packaging tool configuration changed

In fact, the third point is to change the configuration of the packaging tool, using single-SPA to connect to the micro application needs to package the micro application into a JS file, published to the static resource server, and then configure the ADDRESS of the JS file in the main application to tell single-SPA to load the micro application at this address.

Without saying anything else, there is a big problem with this change, the entire microapplication is packaged into a JS file, the common packaging optimization is basically gone, such as: on demand loading, first screen resource loading optimization, CSS independent packaging and other optimization measures.

In order to clear the application brought by the browser cache, generally the file name will carry chunkcontent. After the micro application is released, the file name will change. At this time, the micro application configuration in the main application needs to be updated, and then the main application needs to be recompiled and released. This is simply intolerable, which is why development was chosen for the microfront-end framework’s single-SPA from getting started to mastering the environment configuration for the microapplication in the sample project in this article.

In order to solve the JS Entry problem, The Qiankun framework adopts THE HTML Entry method, so that users can access micro-applications as simple as using iframe.

If the above content does not understand, it indicates that this article is not suitable for you to read, it is recommended to read the micro front-end framework of single-SPA from entry to proficient, this article describes in detail the basic use of single-SPA and source principle, after reading and then come back to read this article will be twice the result with half the effort, Please do not force readers to read, otherwise may appear dazed phenomenon.

HTML Entry

HTML Entry is implemented by the import-html-Entry library. The first screen content (HTML page) of the specified address is loaded through HTTP request, and then the HTML template is parsed to get Template, scripts, Entry, styles

Styles: {template: processed script, link and script tags are commented out, scripts: [HTTP address of script or {async: true, SRC: xx} or code block], styles: [HTTP address of the style], entry: The address of the entry script, either the SRC of the script marked with the entry or the SRC of the last script tag}Copy the code

Then load the style content in Styles remotely, replacing the commented link tag in the template template with the corresponding style element.

Then expose a Promise object

{
  // Template is the template with link replaced by style
	template: embedHTML,
	// Static resource address
	assetPublicPath,
	// Get the external script, and finally get the code content of all the scripts
	getExternalScripts: () = > getExternalScripts(scripts, fetch),
	// Get the content of the external style file
	getExternalStyleSheets: () = > getExternalStyleSheets(styles, fetch),
	// The script executor makes JS code run in the specified context
	execScripts: (proxy, strictGlobal) = > {
		if(! scripts.length) {return Promise.resolve();
		}
		returnexecScripts(entry, scripts, proxy, { fetch, strictGlobal }); }}Copy the code

This is how HTML Entry works. For more details, see the source code analysis section below

The practical application

In order to solve the JS Entry problem, The Qiankun framework adopts THE HTML Entry method, making it as simple as using iframe to access micro-applications.

Template, assetPublicPath, and execScripts are used in an HTML Entry that returns a Promise object. The template is added to the main application through DOM operation, and the lifecycle method exported by the micro application is obtained by executing the execScripts method. In addition, the problem of JS global pollution is solved by the way. The proxy argument is used to specify the JS execution context when executing the execScripts method.

More specific content can read the micro front-end framework qiankun from introduction to source analysis

HTML Entry source analysis

importEntry

/** * load the first screen content at the specified address *@param {*} Entry can be a string address, such as localhost:8080, or a configuration object, such as {scripts, styles, HTML} *@param {*} opts* return importHTML */
export function importEntry(entry, opts = {}) {
	// Resolve the fetch and getTemplate methods from the opt arguments
	const { fetch = defaultFetch, getTemplate = defaultGetTemplate } = opts;
	// A method to get the static resource address
	const getPublicPath = opts.getPublicPath || opts.getDomain || defaultGetPublicPath;

	if(! entry) {throw new SyntaxError('entry should not be empty! ');
	}

	// HTML entry. An entry is an address in string format
	if (typeof entry === 'string') {
		return importHTML(entry, { fetch, getPublicPath, getTemplate });
	}

	// config entry, entry is an object = {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! '); }}Copy the code

importHTML

/** * load the first screen content at the specified address *@param {*} url 
 * @param {*} Opts * return Promise<{// template = link; EmbedHTML, // static resource address assetPublicPath, // get the external script, and finally get the code content of all the scripts getExternalScripts: () => getExternalScripts(scripts, fetch), // Fetch the contents of the external style file getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch), // script executer, lets JS code (scripts) run execScripts in the specified context: (proxy, strictGlobal) => { if (! scripts.length) { return Promise.resolve(); } return execScripts(entry, scripts, proxy, { fetch, strictGlobal }); }} > * /
export default function importHTML(url, opts = {}) {
	// Three default methods
	let fetch = defaultFetch;
	let getPublicPath = defaultGetPublicPath;
	let getTemplate = defaultGetTemplate;

	if (typeof opts === 'function') {
		// If branch, compatible with the legacy importHTML API, ops can be directly a FETCH method
		fetch = opts;
	} else {
		// Override the default method with the arguments passed by the user (if provided)
		fetch = opts.fetch || defaultFetch;
		getPublicPath = opts.getPublicPath || opts.getDomain || defaultGetPublicPath;
		getTemplate = opts.getTemplate || defaultGetTemplate;
	}

	// Use the fetch method to request the URL. This is why Qiankun asked your app to support cross-domains
	return embedHTMLCache[url] || (embedHTMLCache[url] = fetch(url)
		// Response.text () is an HTML template
		.then(response= > response.text())
		.then(html= > {

			// Get the static resource address
			const assetPublicPath = getPublicPath(url);
			/** * parse the address of the external script from the HTML template or the address of the inline script block and the link tag * {* template: processed script, link and script tags are commented out, * scripts: [script HTTP address or {async: true, SRC: xx} or code block], * styles: [style HTTP address], * entry: The address of the entry script is either the SRC of the script marked with entry or the SRC *} */ of the last script tag
			const { template, scripts, entry, styles } = processTpl(getTemplate(html), assetPublicPath);

			// The getEmbedHTML method loads all the external styles remotely via fetch, then replaces the corresponding link annotation tag with style, i.e. the external style is replaced with an inline style, and returns the processed HTML template as embedHTML
			return getEmbedHTML(template, styles, { fetch }).then(embedHTML= > ({
				// Template is the template with link replaced by style
				template: embedHTML,
				// Static resource address
				assetPublicPath,
				// Get the external script, and finally get the code content of all the scripts
				getExternalScripts: () = > getExternalScripts(scripts, fetch),
				// Get the content of the external style file
				getExternalStyleSheets: () = > getExternalStyleSheets(styles, fetch),
				// The script executor makes JS code run in the specified context
				execScripts: (proxy, strictGlobal) = > {
					if(! scripts.length) {return Promise.resolve();
					}
					returnexecScripts(entry, scripts, proxy, { fetch, strictGlobal }); }})); })); }Copy the code

processTpl

/** * parse the address of the external script from the HTML template or the address of the inline script block and link tag *@param TPL HTML template *@param baseURI
 * @stripStyles whether to strip the css links
 * @returns {{template: void | string | *, scripts: *[], entry: *}} * return {* template: the link and script tags are commented out, * scripts: [HTTP address of the script or {async: true, SRC: Xx} or code block], * styles: [HTTP address of the style], * entry: address of the entry script, either SRC of the script marked with the entry or SRC *} */ of the last script tag
export default function processTpl(tpl, baseURI) {

	let scripts = [];
	const styles = [];
	let entry = null;
	// If the browser supports es module, 
	const moduleSupport = isModuleScriptSupported();

	const template = tpl

		// Remove comments from the HTML template <! -- xx -->
		.replace(HTML_COMMENT_REGEX, ' ')

		// Match the link tag
		.replace(LINK_TAG_REGEX, match= > {
			/** * make the link tag in the template as a comment. If there is a non-preloaded link with href attribute, store the address in the styles array. If there is a preloaded link, make it as a comment */
			// <link rel = "stylesheet" />
			conststyleType = !! 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) {

					// Get the href value
					const href = styleHref && styleHref[2];
					let newHref = href;

					// If href has no protocol specification and is given a relative address, concatenate baseURI to get the full address
					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);
					}

					// Store the href value in the styles array
					styles.push(newHref);
					// < span style = "box-sizing: border-box; color: RGB (74, 74, 74); font-size: 14px! Important; word-break: inherit! Important; -- link ${linkHref} replaced by import-html-entry -->
					returngenLinkReplaceSymbol(newHref); }}Href = "XXX" /> 
			constpreloadOrPrefetchType = match.match(LINK_PRELOAD_OR_PREFETCH_REGEX) && match.match(LINK_HREF_REGEX) && ! match.match(LINK_AS_FONT);if (preloadOrPrefetchType) {
				// Get the href address
				const [, , linkHref] = match.match(LINK_HREF_REGEX);
				// Change the tag to <! -- prefetch/preload link ${linkHref} replaced by import-html-entry -->
				return genLinkReplaceSymbol(linkHref, true);
			}

			return match;
		})
		/ / match the < style > < / style >
		.replace(STYLE_TAG_REGEX, match= > {
			if (STYLE_IGNORE_REGEX.test(match)) {
				// < span style = "box-sizing: border-box! Important; word-wrap: break-word! Important; -- ignore asset style file replaced by import-html-entry -->
				return genIgnoreAssetReplaceSymbol('style file');
			}
			return match;
		})
		/ / match < script > < / script >
		.replace(ALL_SCRIPT_REGEX, (match, scriptTag) = > {
			// Match 
			const scriptIgnore = scriptTag.match(SCRIPT_IGNORE_REGEX);
			// Matches  or , which are scripts that should be ignored
			constmoduleScriptIgnore = (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);
			// Get the value of the type attribute
			const matchedScriptType = matchedScriptTypeMatch && matchedScriptTypeMatch[2];
			// Verify that type is valid, If type is null or 'text/javascript', 'module', 'Application /javascript', 'text/ecmascript',' Application/ECMAScript ', it is considered valid
			if(! isValidJavaScriptType(matchedScriptType)) {return match;
			}

			
			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);
				// The script address
				let matchedScriptSrc = matchedScriptSrcMatch && matchedScriptSrcMatch[2];

				if (entry && matchedScriptEntry) {
					
					throw new SyntaxError('You should not set multiply entry script! ');
				} else {
					// Complete the script address, if there is no protocol, it is a relative path, add baseURI
					if(matchedScriptSrc && ! hasProtocol(matchedScriptSrc)) { matchedScriptSrc = getEntirePath(matchedScriptSrc, baseURI); }// The entry address of the script
					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) {
					//  or 
					/ / <! -- nomodule script ${scriptSrc} ignored by import-html-entry --> or
					/ / <! -- module script ${scriptSrc} ignored by import-html-entry -->
					return genModuleScriptReplaceSymbol(matchedScriptSrc || 'js file', moduleSupport);
				}

				if (matchedScriptSrc) {
					// Match , indicating that the script is loaded asynchronously
					constasyncScript = !! scriptTag.match(SCRIPT_ASYNC_REGEX);// Store the script address in an array of scripts, or in an object {async: true, SRC: xx}
					scripts.push(asyncScript ? { async: true.src: matchedScriptSrc } : matchedScriptSrc);
					//  or  replace with
					/ / <! -- async script ${scriptSrc} replace by import-html-entry --> or
					/ / <! -- script ${scriptSrc} replaced by import-html-entry -->
					return genScriptReplaceSymbol(matchedScriptSrc, asyncScript);
				}

				return match;
			} else {
				// 
				if (scriptIgnore) {
					// <script ignore /> -- ignore asset js file replaced by import-html-entry -->
					return genIgnoreAssetReplaceSymbol('js file');
				}

				if (moduleScriptIgnore) {
					//  or 
					/ / <! -- nomodule script ${scriptSrc} ignored by import-html-entry --> or
					/ / <! -- module script ${scriptSrc} ignored by import-html-entry -->
					return genModuleScriptReplaceSymbol('js file', moduleSupport);
				}

				// If it is an inline script, , get the code between the tags => xx
				const code = getInlineCode(match);

				// remove script blocks when all of these lines are comments. Determine if the block of code is full of comments
				const isPureCommentBlock = code.split(/[\r\n]+/).every(line= >! line.trim() || line.trim().startsWith('/ /'));

				if(! isPureCommentBlock) {// If it is not a comment, the code block is stored in the scripts array
					scripts.push(match);
				}

				// <script>xx</script> -- inline scripts replaced by import-html-entry -->
				returninlineScriptReplaceSymbol; }});// 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]}; }Copy the code

getEmbedHTML

/** * Convert external CSS link to inline style for performance optimization@param Template, HTML template *@param Styles link@param opts = { fetch }
 * @return The HTML template */ treated by embedHTML
function getEmbedHTML(template, styles, opts = {}) {
	const { fetch = defaultFetch } = opts;
	let embedHTML = template;

	return getExternalStyleSheets(styles, fetch)
		.then(styleSheets= > {
			// < span style = "box-sizing: border-box; color: RGB (74, 74, 74); line-height: 22px; white-space: inherit;"
			embedHTML = styles.reduce((html, styleSrc, i) = > {
				html = html.replace(genLinkReplaceSymbol(styleSrc), `<style>/* ${styleSrc}* /${styleSheets[i]}</style>`);
				return html;
			}, embedHTML);
			return embedHTML;
		});
}

Copy the code

getExternalScripts

/** * load the script and finally return the contents of the script, Promise<Array>, each element is a piece of JS code *@param {*} Scripts = [HTTP address of script or content of inline script or {async: true, SRC: xx}] *@param {*} fetch 
 * @param {*} errorCallback 
 */
export function getExternalScripts(scripts, fetch = defaultFetch, errorCallback = () => {
}) {

	// Define a method to load a remote url script, and cache it. If the script hits the cache, retrieve it directly from the cache
	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/5625 603
			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') {
				// String, either link address or script content (code)
				if (isInlineCode(script)) {
					// if it is inline script
					return getInlineCode(script);
				} else {
					// External script to load the script
					returnfetchScript(script); }}else {
				// use idle time to load async script
				// The asynchronous script is loaded through the requestIdleCallback method
				const { src, async } = script;
				if (async) {
					return {
						src,
						async: true.content: new Promise((resolve, reject) = > requestIdleCallback(() = > fetchScript(src).then(resolve, reject))),
					};
				}

				returnfetchScript(src); }})); }Copy the code

getExternalStyleSheets

/** * load the style file at the specified address with the fetch method@param {*} styles = [ href ]
 * @param {*} Fetch * return Promise<Array>, each element is a pile of style content */
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, load styles and cache them
				return styleCache[styleLink] ||
					(styleCache[styleLink] = fetch(styleLink).then(response= >response.text())); }})); }Copy the code

execScripts

/** * FIXME to consistent with browser behavior, We should only provide callback way to invoke success and error events, allowing the specified scripts to execute in the specified context@param Entry Entry address *@param Scripts = [HTTP address of script or content of inline script or {async: true, SRC: xx}] *@param Proxy script execution context, global object, qiankun JS sandbox generated windowProxy is passed this parameter *@param opts
 * @returns {Promise<unknown>}* /
export function execScripts(entry, scripts, proxy = window, opts = {}) {
	const {
		fetch = defaultFetch, strictGlobal = false, success, error = () = > {
		}, beforeExec = () = > {
		},
	} = opts;

	// Get the contents of all the specified external scripts, set the execution context for each script, and run it through the eval function
	return getExternalScripts(scripts, fetch, error)
		.then(scriptsText= > {
			// scriptsText is an array of script contents => Each element is a piece of JS code
			const geval = (code) = > {
				beforeExec();
				(0.eval)(code);
			};

			/ * * * *@param {*} ScriptSrc Indicates the script address@param {*} InlineScript Script content *@param {*} resolve 
			 */
			function exec(scriptSrc, inlineScript, resolve) {

				// Performance metrics
				const markName = `Evaluating script ${scriptSrc}`;
				const measureName = `Evaluating Time Consuming: ${scriptSrc}`;

				if (process.env.NODE_ENV === 'development' && supportsUserTiming) {
					performance.mark(markName);
				}

				if (scriptSrc === entry) {
					/ / the 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}`);
						throwe; }}else {
					if (typeof inlineScript === 'string') {
						try {
							// bind window.proxy to change 'this' reference in script, which is to set the execution context of JS code and then run the code through the eval function
							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 asyncinlineScript.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}`); }); }}// Performance metrics
				if (process.env.NODE_ENV === 'development'&& supportsUserTiming) { performance.measure(measureName, markName); performance.clearMarks(markName); performance.clearMeasures(measureName); }}/** * recursion *@param {*} I indicates the number of scripts *@param {*} ResolvePromise successful callback */
			function schedule(i, resolvePromise) {

				if (i < scripts.length) {
					// The address of the ith script
					const scriptSrc = scripts[i];
					// The contents of the i-th script
					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 {
						// Recursively call the next script
						schedule(i + 1, resolvePromise); }}}// Start scheduling from script 0
			return new Promise(resolve= > schedule(0, success || resolve));
		});
}

Copy the code

conclusion

That’s all for HTML Entry, which is also an essential part of understanding micro Front end, Single-Spa, and Qiankun. The source code is on Github

If you want to learn more about micro Front end, Single-Spa, Qiankun, etc., the following are recommended

  • Single – SPA micro front end framework from beginner to master
  • Micro front-end framework qiankun from introduction to source analysis
  • Qiankun 2.x runtime sandbox source analysis
  • Single – spa website
  • Qiankun website