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

智能推荐

稀疏编码的数学基础与理论分析-程序员宅基地

文章浏览阅读290次,点赞8次,收藏10次。1.背景介绍稀疏编码是一种用于处理稀疏数据的编码技术,其主要应用于信息传输、存储和处理等领域。稀疏数据是指数据中大部分元素为零或近似于零的数据,例如文本、图像、音频、视频等。稀疏编码的核心思想是将稀疏数据表示为非零元素和它们对应的位置信息,从而减少存储空间和计算复杂度。稀疏编码的研究起源于1990年代,随着大数据时代的到来,稀疏编码技术的应用范围和影响力不断扩大。目前,稀疏编码已经成为计算...

EasyGBS国标流媒体服务器GB28181国标方案安装使用文档-程序员宅基地

文章浏览阅读217次。EasyGBS - GB28181 国标方案安装使用文档下载安装包下载,正式使用需商业授权, 功能一致在线演示在线API架构图EasySIPCMSSIP 中心信令服务, 单节点, 自带一个 Redis Server, 随 EasySIPCMS 自启动, 不需要手动运行EasySIPSMSSIP 流媒体服务, 根..._easygbs-windows-2.6.0-23042316使用文档

【Web】记录巅峰极客2023 BabyURL题目复现——Jackson原生链_原生jackson 反序列化链子-程序员宅基地

文章浏览阅读1.2k次,点赞27次,收藏7次。2023巅峰极客 BabyURL之前AliyunCTF Bypassit I这题考查了这样一条链子:其实就是Jackson的原生反序列化利用今天复现的这题也是大同小异,一起来整一下。_原生jackson 反序列化链子

一文搞懂SpringCloud,详解干货,做好笔记_spring cloud-程序员宅基地

文章浏览阅读734次,点赞9次,收藏7次。微服务架构简单的说就是将单体应用进一步拆分,拆分成更小的服务,每个服务都是一个可以独立运行的项目。这么多小服务,如何管理他们?(服务治理 注册中心[服务注册 发现 剔除])这么多小服务,他们之间如何通讯?这么多小服务,客户端怎么访问他们?(网关)这么多小服务,一旦出现问题了,应该如何自处理?(容错)这么多小服务,一旦出现问题了,应该如何排错?(链路追踪)对于上面的问题,是任何一个微服务设计者都不能绕过去的,因此大部分的微服务产品都针对每一个问题提供了相应的组件来解决它们。_spring cloud

Js实现图片点击切换与轮播-程序员宅基地

文章浏览阅读5.9k次,点赞6次,收藏20次。Js实现图片点击切换与轮播图片点击切换<!DOCTYPE html><html> <head> <meta charset="UTF-8"> <title></title> <script type="text/ja..._点击图片进行轮播图切换

tensorflow-gpu版本安装教程(过程详细)_tensorflow gpu版本安装-程序员宅基地

文章浏览阅读10w+次,点赞245次,收藏1.5k次。在开始安装前,如果你的电脑装过tensorflow,请先把他们卸载干净,包括依赖的包(tensorflow-estimator、tensorboard、tensorflow、keras-applications、keras-preprocessing),不然后续安装了tensorflow-gpu可能会出现找不到cuda的问题。cuda、cudnn。..._tensorflow gpu版本安装

随便推点

物联网时代 权限滥用漏洞的攻击及防御-程序员宅基地

文章浏览阅读243次。0x00 简介权限滥用漏洞一般归类于逻辑问题,是指服务端功能开放过多或权限限制不严格,导致攻击者可以通过直接或间接调用的方式达到攻击效果。随着物联网时代的到来,这种漏洞已经屡见不鲜,各种漏洞组合利用也是千奇百怪、五花八门,这里总结漏洞是为了更好地应对和预防,如有不妥之处还请业内人士多多指教。0x01 背景2014年4月,在比特币飞涨的时代某网站曾经..._使用物联网漏洞的使用者

Visual Odometry and Depth Calculation--Epipolar Geometry--Direct Method--PnP_normalized plane coordinates-程序员宅基地

文章浏览阅读786次。A. Epipolar geometry and triangulationThe epipolar geometry mainly adopts the feature point method, such as SIFT, SURF and ORB, etc. to obtain the feature points corresponding to two frames of images. As shown in Figure 1, let the first image be ​ and th_normalized plane coordinates

开放信息抽取(OIE)系统(三)-- 第二代开放信息抽取系统(人工规则, rule-based, 先抽取关系)_语义角色增强的关系抽取-程序员宅基地

文章浏览阅读708次,点赞2次,收藏3次。开放信息抽取(OIE)系统(三)-- 第二代开放信息抽取系统(人工规则, rule-based, 先关系再实体)一.第二代开放信息抽取系统背景​ 第一代开放信息抽取系统(Open Information Extraction, OIE, learning-based, 自学习, 先抽取实体)通常抽取大量冗余信息,为了消除这些冗余信息,诞生了第二代开放信息抽取系统。二.第二代开放信息抽取系统历史第二代开放信息抽取系统着眼于解决第一代系统的三大问题: 大量非信息性提取(即省略关键信息的提取)、_语义角色增强的关系抽取

10个顶尖响应式HTML5网页_html欢迎页面-程序员宅基地

文章浏览阅读1.1w次,点赞6次,收藏51次。快速完成网页设计,10个顶尖响应式HTML5网页模板助你一臂之力为了寻找一个优质的网页模板,网页设计师和开发者往往可能会花上大半天的时间。不过幸运的是,现在的网页设计师和开发人员已经开始共享HTML5,Bootstrap和CSS3中的免费网页模板资源。鉴于网站模板的灵活性和强大的功能,现在广大设计师和开发者对html5网站的实际需求日益增长。为了造福大众,Mockplus的小伙伴整理了2018年最..._html欢迎页面

计算机二级 考试科目,2018全国计算机等级考试调整,一、二级都增加了考试科目...-程序员宅基地

文章浏览阅读282次。原标题:2018全国计算机等级考试调整,一、二级都增加了考试科目全国计算机等级考试将于9月15-17日举行。在备考的最后冲刺阶段,小编为大家整理了今年新公布的全国计算机等级考试调整方案,希望对备考的小伙伴有所帮助,快随小编往下看吧!从2018年3月开始,全国计算机等级考试实施2018版考试大纲,并按新体系开考各个考试级别。具体调整内容如下:一、考试级别及科目1.一级新增“网络安全素质教育”科目(代..._计算机二级增报科目什么意思

conan简单使用_apt install conan-程序员宅基地

文章浏览阅读240次。conan简单使用。_apt install conan