JavaScript读源码系列--微前端之import-html-entry-程序员宅基地

技术标签: WEB前端  JavaScript读源码系列  javascript  

最近网络上对于微前端讨论的愈加激烈,qiankun 就是一款由蚂蚁金服推出的比较成熟的微前端框架,基于 single-spa 进行二次开发,用于将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。尤其适合遗留项目技术栈难以维护,又需要新的技术来迭代功能。

qiankun一大特点就是将html做为入口文件,规避了JavaScript为了支持缓存而根据文件内容动态生成文件名,造成入口文件无法锁定的问题。将html做为入口文件,其实就是将静态的html做为一个资源列表来使用了,这样也避免了一些潜在的问题。本文的主角就是支持qiankunhtml做为入口所依赖的 import-html-entry 库,版本是1.7.3

importHTML

import-html-entry的默认导出接口,返回值为一个promise对象。接口声明如下。

importHTML(url, opts = {
    })

参数说明:

  • url :需要解析的html模板路径
  • opts:默认值为一个空对象
    • 传入为函数类型的时候,直接做为fetch 使用
    • 传入为对象类型的时候,对象属性用于解析html模板的,如果没有传入,模块内置了默认属性。
属性 参数 返回值 功能 默认
fetch url:string promise 用于获取远端的脚本和样式文件内容 浏览器fetch,如果浏览器不支持,会报错
getPublicPath 模板url:string publicPath:string 用于获取静态资源publicPath,将模板中外部资源为相对路径的,转换为绝对路径。 以当前location.hrefpublicPath
getDomain ?? string 如果没有提供getPublicPath参数,则使用getDomain,两者都没有提供的时候,使用默认getPublicPath
getTemplate html模板字符串:string html模板字符串:string 用于支持使用者在模板解析前,做一次处理 无处理

接口返回promise<pendingresolve参数为一个对象,拥有以下属性。

属性 类型 说明 参数
template string 被处理后的html模板字符串,外联的样式文件被替换为内联样式 -
assetPublicPath string 静态资源的baseURL -
getExternalScripts function:promise 将模板中所有script标签按照出现的先后顺序,提取出内容,组成一个数组 -
getExternalStyleSheets function:promise 将模板中所有linkstyle标签按照出现的先后顺序,提取出内容,组成一个数组 -
execScripts function:promise 执行所有的script中的代码,并返回为html模板入口脚本链接entry指向的模块导出对象。 参见下文
export default function importHTML(url, opts = {
    }) {
    
	let fetch = defaultFetch;
	let getPublicPath = defaultGetPublicPath;
	let getTemplate = defaultGetTemplate;
	
	// compatible with the legacy importHTML api
	if (typeof opts === 'function') {
    
		fetch = opts;
	} else {
    
		fetch = opts.fetch || defaultFetch;
		getPublicPath = opts.getPublicPath || opts.getDomain || defaultGetPublicPath;
		getTemplate = opts.getTemplate || defaultGetTemplate;
	}
	
	return embedHTMLCache[url] || (embedHTMLCache[url] = fetch(url)
		.then(response => response.text())
		.then(html => {
    
			
			const assetPublicPath = getPublicPath(url);
			const {
     template, scripts, entry, styles } = processTpl(getTemplate(html), assetPublicPath);
			
			return getEmbedHTML(template, styles, {
     fetch }).then(embedHTML => ({
    
				template: embedHTML,
				assetPublicPath,
				getExternalScripts: () => getExternalScripts(scripts, fetch),
				getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch),
				execScripts: (proxy, strictGlobal) => {
    
					if (!scripts.length) {
    
						return Promise.resolve();
					}
					return execScripts(entry, scripts, proxy, {
     fetch, strictGlobal });
				},
			}));
		}));
}
  • 1~13 行,主要是用来处理传入参数类型及默认值的。
  • 15 行,对解析操作做了缓存处理,如果相同的url已经被处理过,则直接返回处理结果,否则通过fetch去获取模板字符串,并进行后续处理。
  • 20行,processTpl 方法是解析模板的核心函数,后面会具体说,这里主要返回了经过初步处理过的模板字符串template、外部脚本和样式的链接前缀assetPublicPath ,所有外部脚本的src值组成的数组scripts,所有外部样式的href值组成的数组styles,还有上面提到的html模板的入口脚本链接entry ,如果模板中没有被标记为entryscript标签,则会返回最后一个script标签的src值。
  • 22 行,调用getEmbedHTML函数将所有通过外部引入的样式,转换为内联样式。embedHTML 函数的代码比较简单,可以直接去看。
  • 25~31行,这里使用了getExternalScriptsgetExternalStyleSheetsexecScripts 三个函数,一一来看下。

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

函数的第一个参数是 模板中所有linkstyle标签组成的数组,第二个参数是用于请求的fetch,函数比较简单,主要是通过对linkstyle的区分,分别来获取样式的具体内容组成数组,并返回。

后面发现,在解析模板的时候style标签的内容并没有被放入styles中,不知道是不是一个失误,issue准备中_

getExternalScripts

export function getExternalScripts(scripts, fetch = defaultFetch) {
    
	
	const fetchScript = scriptUrl => scriptCache[scriptUrl] ||
		(scriptCache[scriptUrl] = fetch(scriptUrl).then(response => 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
				const {
     src, async } = script;
				if (async) {
    
					return {
    
						src,
						async: true,
						content: new Promise((resolve, reject) => requestIdleCallback(() => fetchScript(src).then(resolve, reject))),
					};
				}
				
				return fetchScript(src);
			}
		},
	));
}

函数的第一个参数是模板中所有script标签组成的数组,第二个参数是用于请求的fetch

  • 3行,主要是包装了一下fetch,提供了缓存的能力
  • 8行,这个判断主要是为了区别处理在importEntry中调用函数的时候,提供的可能是通过对象方式配置的资源,例如scripts可能会是这个样子[{src:"http://xxx.com/static/xx.js",async:true},...]

execScripts

这段代码太长,下面的代码中,将和性能测试相关的部分删除掉了,只留下了功能代码。

export function execScripts(entry, scripts, proxy = window, opts = {
    }) {
    
	const {
     fetch = defaultFetch, strictGlobal = false } = opts;
	
	return getExternalScripts(scripts, fetch)
		.then(scriptsText => {
    
			
			const geval = eval;
			
			function exec(scriptSrc, inlineScript, resolve) {
    
				
				if (scriptSrc === entry) {
    
					noteGlobalProps(strictGlobal ? proxy : window);
					
					// bind window.proxy to change `this` reference in script
					geval(getExecutableScript(scriptSrc, inlineScript, proxy, strictGlobal));
					
					const exports = proxy[getGlobalProp(strictGlobal ? proxy : window)] || {
    };
					resolve(exports);
					
				} else {
    
					
					if (typeof inlineScript === 'string') {
    
						// bind window.proxy to change `this` reference in script
						geval(getExecutableScript(scriptSrc, inlineScript, proxy, strictGlobal));
					} else {
    
						// external script marked with async
						inlineScript.async && inlineScript?.content
							.then(downloadedScriptText => geval(getExecutableScript(inlineScript.src, downloadedScriptText, proxy, strictGlobal)))
							.catch(e => {
    
								console.error(`error occurs while executing async script ${
       inlineScript.src }`);
								throw e;
							});
					}
				}
			}
			
			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, resolve));
		});
}
  • 4行,调用getExternalScripts来获取所有script标签内容组成的数组。
  • 53行,我们先从这里的函数调用开始,这里通过schedule 函数开始从脚本内容数组的第一个开始执行。
  • 37~51行,这段定义了schedule函数,通过代码可以看出,这是一个递归函数,结束条件是数组循环完毕,注意看45行,和模板解析函数一样的逻辑,如果entry不存在,则指定数组的最后一个为脚本入口模块,将执行结果通过放在 Promise 中返回。
  • exec函数比较简单,主要是对entry和非entry的脚本做了区分,对entry模块的执行结果进行返回,见代码18行。

整个代码逻辑比较简单,主要关注entry的处理即可。

另外,代码中通过间接的方式使用了eval 执行了getExecutableScript 函数处理过的脚本字符串,间接的方式确保了eval中代码执行在全局上下文中,而不会影响局部,如果这块不是很清楚,参见神奇的eval()与new Function()【译】以 eval() 和 new Function() 执行JavaScript代码永远不要使用eval

getExecutableScript

这个函数的主要作用,是通过修改脚本字符串,改变脚本执行时候的window/self/this 的指向。

function getExecutableScript(scriptSrc, scriptText, proxy, strictGlobal) {
    
	const sourceUrl = isInlineCode(scriptSrc) ? '' : `//# sourceURL=${
       scriptSrc }\n`;

	window.proxy = proxy;
	// TODO 通过 strictGlobal 方式切换切换 with 闭包,待 with 方式坑趟平后再合并
	return strictGlobal
		? `;(function(window, self){with(window){;${
       scriptText }\n${
       sourceUrl }}}).bind(window.proxy)(window.proxy, window.proxy);`
		: `;(function(window, self){;${
       scriptText }\n${
       sourceUrl }}).bind(window.proxy)(window.proxy, window.proxy);`;
}

核心代码主要是这里;(function(window, self){;${ scriptText }\n${ sourceUrl }}).bind(window.proxy)(window.proxy, window.proxy);

拆开来看。

// 声明一个函数
let scriptText = "xxx";
let sourceUrl = "xx";
let fn = function(window, self){
    
    // 具体脚本内容
};
// 改变函数中 this 的指向
let fnBind = fn.bind(window.proxy);
// 指向函数,并指定参数中 window 和 self
fnBind(window.proxy, window.proxy);

通过这一波操作,给脚本字符串构件了一个简单的执行环境,该环境屏蔽了全局了thiswindowself。但是这里默认传入的依然是window,只是在调用的时候可以通过参数传入。

importEntry

export function importEntry(entry, opts = {
    }) {
    
	// ...

	// html entry
	if (typeof entry === 'string') {
    
		return importHTML(entry, {
     fetch, getPublicPath, getTemplate });
	}

	// config entry
	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 => ({
    
			// 这里处理同 importHTML , 省略
			},
		}));

	} else {
    
		throw new SyntaxError('entry scripts or styles should be array!');
	}
}

第一个参数entry 可以是字符串和对象,类型为字符串的时候与importHTML功能相同。为对象的时候,传入的是脚本和样式的资源列表。如下所示

{
    
    html:"http://xxx.com/static/tpl.html",
   scripts:[
        {
    
            src:"http://xxx.com/static/xx.js",
            async:true
        },
       ...
   ],
    styles:[
        {
     
       		href:"http://xxx.com/static/style.css"
        },
        ...
    ]
} 

其他

src/process-tpl.js 模块主要做了一件事,就是对资源进行分类收集并返回,没有什么难懂的地方。
src/utils 主要是提供了一些工具函数,其中getGlobalPropnoteGlobalProps比较有意思,用于根据entry执行前后window上属性的变化,来获取entry的导出结果。这两个函数主要依据的原理是对象属性的顺序是可预测的,传送门解惑

export function getGlobalProp(global) {
    
	let cnt = 0;
	let lastProp;
	let hasIframe = false;

	for (let p in global) {
    
		if (shouldSkipProperty(global, p))
			continue;

		// 遍历 iframe,检查 window 上的属性值是否是 iframe,是则跳过后面的 first 和 second 判断
		for (let i = 0; i < window.frames.length && !hasIframe; i++) {
    
			const frame = window.frames[i];
			if (frame === global[p]) {
    
				hasIframe = true;
				break;
			}
		}

		if (!hasIframe && (cnt === 0 && p !== firstGlobalProp || cnt === 1 && p !== secondGlobalProp))
			return p;
		cnt++;
		lastProp = p;
	}

	if (lastProp !== lastGlobalProp)
		return lastProp;
}

export function noteGlobalProps(global) {
    
	// alternatively Object.keys(global).pop()
	// but this may be faster (pending benchmarks)
	firstGlobalProp = secondGlobalProp = undefined;

	for (let p in global) {
    
		if (shouldSkipProperty(global, p))
			continue;
		if (!firstGlobalProp)
			firstGlobalProp = p;
		else if (!secondGlobalProp)
			secondGlobalProp = p;
		lastGlobalProp = p;
	}

	return lastGlobalProp;
}
  • noteGlobalProps用于标记执行entrywindow的属性状态,执行entry模块后,会导出结果并挂载到window上。
  • getGlobalProp 用于检测entry模块执行后window的变化,根据变化找出entry的指向结果并返回。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/daihaoxin/article/details/106250617

智能推荐

Docker 快速上手学习入门教程_docker菜鸟教程-程序员宅基地

文章浏览阅读2.5w次,点赞6次,收藏50次。官方解释是,docker 容器是机器上的沙盒进程,它与主机上的所有其他进程隔离。所以容器只是操作系统中被隔离开来的一个进程,所谓的容器化,其实也只是对操作系统进行欺骗的一种语法糖。_docker菜鸟教程

电脑技巧:Windows系统原版纯净软件必备的两个网站_msdn我告诉你-程序员宅基地

文章浏览阅读5.7k次,点赞3次,收藏14次。该如何避免的,今天小编给大家推荐两个下载Windows系统官方软件的资源网站,可以杜绝软件捆绑等行为。该站提供了丰富的Windows官方技术资源,比较重要的有MSDN技术资源文档库、官方工具和资源、应用程序、开发人员工具(Visual Studio 、SQLServer等等)、系统镜像、设计人员工具等。总的来说,这两个都是非常优秀的Windows系统镜像资源站,提供了丰富的Windows系统镜像资源,并且保证了资源的纯净和安全性,有需要的朋友可以去了解一下。这个非常实用的资源网站的创建者是国内的一个网友。_msdn我告诉你

vue2封装对话框el-dialog组件_<el-dialog 封装成组件 vue2-程序员宅基地

文章浏览阅读1.2k次。vue2封装对话框el-dialog组件_

MFC 文本框换行_c++ mfc同一框内输入二行怎么换行-程序员宅基地

文章浏览阅读4.7k次,点赞5次,收藏6次。MFC 文本框换行 标签: it mfc 文本框1.将Multiline属性设置为True2.换行是使用"\r\n" (宽字符串为L"\r\n")3.如果需要编辑并且按Enter键换行,还要将 Want Return 设置为 True4.如果需要垂直滚动条的话将Vertical Scroll属性设置为True,需要水平滚动条的话将Horizontal Scroll属性设_c++ mfc同一框内输入二行怎么换行

redis-desktop-manager无法连接redis-server的解决方法_redis-server doesn't support auth command or ismis-程序员宅基地

文章浏览阅读832次。检查Linux是否是否开启所需端口,默认为6379,若未打开,将其开启:以root用户执行iptables -I INPUT -p tcp --dport 6379 -j ACCEPT如果还是未能解决,修改redis.conf,修改主机地址:bind 192.168.85.**;然后使用该配置文件,重新启动Redis服务./redis-server redis.conf..._redis-server doesn't support auth command or ismisconfigured. try

实验四 数据选择器及其应用-程序员宅基地

文章浏览阅读4.9k次。济大数电实验报告_数据选择器及其应用

随便推点

灰色预测模型matlab_MATLAB实战|基于灰色预测河南省社会消费品零售总额预测-程序员宅基地

文章浏览阅读236次。1研究内容消费在生产中占据十分重要的地位,是生产的最终目的和动力,是保持省内经济稳定快速发展的核心要素。预测河南省社会消费品零售总额,是进行宏观经济调控和消费体制改变创新的基础,是河南省内人民对美好的全面和谐社会的追求的要求,保持河南省经济稳定和可持续发展具有重要意义。本文建立灰色预测模型,利用MATLAB软件,预测出2019年~2023年河南省社会消费品零售总额预测值分别为21881...._灰色预测模型用什么软件

log4qt-程序员宅基地

文章浏览阅读1.2k次。12.4-在Qt中使用Log4Qt输出Log文件,看这一篇就足够了一、为啥要使用第三方Log库,而不用平台自带的Log库二、Log4j系列库的功能介绍与基本概念三、Log4Qt库的基本介绍四、将Log4qt组装成为一个单独模块五、使用配置文件的方式配置Log4Qt六、使用代码的方式配置Log4Qt七、在Qt工程中引入Log4Qt库模块的方法八、获取示例中的源代码一、为啥要使用第三方Log库,而不用平台自带的Log库首先要说明的是,在平时开发和调试中开发平台自带的“打印输出”已经足够了。但_log4qt

100种思维模型之全局观思维模型-67_计算机中对于全局观的-程序员宅基地

文章浏览阅读786次。全局观思维模型,一个教我们由点到线,由线到面,再由面到体,不断的放大格局去思考问题的思维模型。_计算机中对于全局观的

线程间控制之CountDownLatch和CyclicBarrier使用介绍_countdownluach于cyclicbarrier的用法-程序员宅基地

文章浏览阅读330次。一、CountDownLatch介绍CountDownLatch采用减法计算;是一个同步辅助工具类和CyclicBarrier类功能类似,允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。二、CountDownLatch俩种应用场景: 场景一:所有线程在等待开始信号(startSignal.await()),主流程发出开始信号通知,既执行startSignal.countDown()方法后;所有线程才开始执行;每个线程执行完发出做完信号,既执行do..._countdownluach于cyclicbarrier的用法

自动化监控系统Prometheus&Grafana_-自动化监控系统prometheus&grafana实战-程序员宅基地

文章浏览阅读508次。Prometheus 算是一个全能型选手,原生支持容器监控,当然监控传统应用也不是吃干饭的,所以就是容器和非容器他都支持,所有的监控系统都具备这个流程,_-自动化监控系统prometheus&grafana实战

React 组件封装之 Search 搜索_react search-程序员宅基地

文章浏览阅读4.7k次。输入关键字,可以通过键盘的搜索按钮完成搜索功能。_react search