import-html-entry流程分析

871 阅读2分钟

import-html-entry流程分析

Table of contents

importEntry

entry为string时进入importHTML函数

export function importEntry(entry, opts = {}) {
	const { fetch = defaultFetch, getTemplate = defaultGetTemplate, postProcessTemplate } = opts;
	const getPublicPath = opts.getPublicPath || opts.getDomain || defaultGetPublicPath;
    
	// html entry
	if (typeof entry === 'string') {
		return importHTML(entry, {
			fetch,
			getPublicPath,
			getTemplate,
			postProcessTemplate,
		});
	}
	// ...
}

importHTML

下载html,处理模板

export default function importHTML(url, opts = {}) {
	// ...
	return embedHTMLCache[url] || (embedHTMLCache[url] = fetch(url)
		.then(response => readResAsString(response, autoDecodeResponse))
		.then(html => {
			const assetPublicPath = getPublicPath(url);
			const { template, scripts, entry, styles } = processTpl(getTemplate(html), assetPublicPath, postProcessTemplate);

			return getEmbedHTML(...);
}

processTpl

分析html模板,将styles和scripts的内容提取成数据,被提取的styles和scripts会被替换成注释

  • styles处理方式
    • link或style标签上有ignore属性会忽略内容
    • link有href属性,将会将href的值存放在styles数组
    • style标签会将style元素存放在styles数据
  • scripts处理方式
    • script标签上有ignore属性会忽略内容
    • script有href属性,判断是否有async属性
      • 有async属性,scripts数组存放{ async: true, src: '...' }
      • 没有async属性,scripts数组存放href属性
    • 内联script会将script元素存放在scripts数组中
  • 设置entry的值
    • 如果script标签上有entry属性,则会将改script设置为entry
    • 如果script标签上没有entry属性,则会将scripts最后一个元素作为entry

getEmbedHTML

主要将template中被注释替换的样式表,替换回模板。

  • 外链link内容下载下来,替换模板中的注释;
  • 内联样式直接将样式内容替换

getEmbedHTML函数

/**
 * convert external css link to inline style for performance optimization
 */
function getEmbedHTML(template, styles, opts = {}) {
	const { fetch = defaultFetch } = opts;
	let embedHTML = template;

	return getExternalStyleSheets(styles, fetch)
		.then(styleSheets => {
			embedHTML = styles.reduce((html, styleSrc, i) => {
				html = html.replace(genLinkReplaceSymbol(styleSrc), isInlineCode(styleSrc) ? `${styleSrc}` : `<style>/* ${styleSrc} */${styleSheets[i]}</style>`);
				return html;
			}, embedHTML);
			return embedHTML;
		});
}

getExternalStyleSheets

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()));
			}
		},
	));
}

getEmbedHTML返回值

向外暴露得到的信息。

  • 注入内联样式后的template模板
  • assetPublicPath
  • getExternalScripts,返回一个获取全部脚本的Promise
  • getExternalStyleSheets,返回一个获取全部样式的Promise
  • 返回一个execScripts函数,用来执行脚本
export default function importHTML(url, opts = {}) {
	// ...

	return embedHTMLCache[url] || (embedHTMLCache[url] = fetch(url)
		.then(response => readResAsString(response, autoDecodeResponse))
		.then(html => {
			// ...
			return getEmbedHTML(template, styles, { fetch }).then(embedHTML => ({
				template: embedHTML,
				assetPublicPath,
				getExternalScripts: () => getExternalScripts(scripts, fetch),
				getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch),
				execScripts: (proxy, strictGlobal, opts = {}) => {
					if (!scripts.length) {
						return Promise.resolve();
					}
					return execScripts(entry, scripts, proxy, {
						fetch,
						strictGlobal,
						...opts,
					});
				},
			}));
		}));
}

execScripts

执行template中的脚本,最后将entry的执行结果返回。

evalCode

执行string代码

export function evalCode(scriptSrc, code) {
	const key = scriptSrc;
	if (!evalCache[key]) {
		const functionWrappedCode = `(function(){${code}})`;
        // indirect invoke eval, eval will invoke in global scope.
		evalCache[key] = (0, eval)(functionWrappedCode);
	}
	const evalFunc = evalCache[key];
	evalFunc.call(window);
}

getExcutableScript

将执行的代码的this引用绑定在proxy对象上。

function getExecutableScript(scriptSrc, scriptText, opts = {}) {
	const { proxy, strictGlobal, scopedGlobalVariables = [] } = opts;

	const sourceUrl = isInlineCode(scriptSrc) ? '' : `//# sourceURL=${scriptSrc}\n`;

	// 将 scopedGlobalVariables 拼接成函数声明,用于缓存全局变量,避免每次使用时都走一遍代理
	const scopedGlobalVariableFnParameters = scopedGlobalVariables.length ? scopedGlobalVariables.join(',') : '';

	// 通过这种方式获取全局 window,因为 script 也是在全局作用域下运行的,所以我们通过 window.proxy 绑定时也必须确保绑定到全局 window 上
	// 否则在嵌套场景下, window.proxy 设置的是内层应用的 window,而代码其实是在全局作用域运行的,会导致闭包里的 window.proxy 取的是最外层的微应用的 proxy
	const globalWindow = (0, eval)('window');
	globalWindow.proxy = proxy;
	// TODO 通过 strictGlobal 方式切换 with 闭包,待 with 方式坑趟平后再合并
	return strictGlobal
		? (
			scopedGlobalVariableFnParameters
				? `;with(window.proxy){(function(${scopedGlobalVariableFnParameters}){;${scriptText}\n${sourceUrl}}).bind(window)(${scopedGlobalVariableFnParameters})};`
				: `;(function(window, self, globalThis){with(window){;${scriptText}\n${sourceUrl}}}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);`
		)
		: `;(function(window, self, globalThis){;${scriptText}\n${sourceUrl}}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);`;
}

execScripts函数

export function execScripts(entry, scripts, proxy = window, opts = {}) {
	// ...

	return getExternalScripts(scripts, fetch, error)
		.then(scriptsText => {

			const geval = (scriptSrc, inlineScript) => {
				    const rawCode = beforeExec(inlineScript, scriptSrc) || inlineScript;
                    const code = getExecutableScript(scriptSrc, rawCode, { proxy, strictGlobal, scopedGlobalVariables });

                    evalCode(scriptSrc, code);
                    afterExec(inlineScript, scriptSrc);
			};

			function exec(scriptSrc, inlineScript, resolve) {
				// ...
			}

			function schedule(i, resolvePromise) {
                if (i < scripts.length) {
                    const scriptSrc = scripts[i];
                    const inlineScript = scriptsText[i];

                    exec(scriptSrc, inlineScript, resolvePromise);
                    // resolve the promise while the last script executed and entry not provided
                    if (!entry && i === scripts.length - 1) {
                        resolvePromise();
                    } else {
                        schedule(i + 1, resolvePromise);
                    }
                }
			}

			return new Promise(resolve => schedule(0, success || resolve));
		});
}

exec

执行代码,执行到entry代码时,返回exports

function exec(scriptSrc, inlineScript, resolve) {
    if (scriptSrc === entry) {
        noteGlobalProps(strictGlobal ? proxy : window);

        try {
            // bind window.proxy to change `this` reference in script
            geval(scriptSrc, inlineScript);
            const exports = proxy[getGlobalProp(strictGlobal ? proxy : window)] || {};
            resolve(exports);
        } catch (e) {
            // ...
        }
    } else {
        if (typeof inlineScript === 'string') {
            try {
                // bind window.proxy to change `this` reference in script
                geval(scriptSrc, inlineScript);
            } 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(inlineScript.src, downloadedScriptText))
                .catch(e => {
                throwNonBlockingError(e, `[import-html-entry]: error occurs while executing async script ${inlineScript.src}`);
            });
        }
    }
}