技术标签: WEB前端 JavaScript读源码系列 javascript
最近网络上对于微前端讨论的愈加激烈,qiankun
就是一款由蚂蚁金服推出的比较成熟的微前端框架,基于 single-spa
进行二次开发,用于将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。尤其适合遗留项目技术栈难以维护,又需要新的技术来迭代功能。
qiankun
一大特点就是将html
做为入口文件,规避了JavaScript
为了支持缓存而根据文件内容动态生成文件名,造成入口文件无法锁定的问题。将html
做为入口文件,其实就是将静态的html
做为一个资源列表来使用了,这样也避免了一些潜在的问题。本文的主角就是支持qiankun
将html
做为入口所依赖的 import-html-entry
库,版本是1.7.3
。
import-html-entry
的默认导出接口,返回值为一个promise
对象。接口声明如下。
importHTML(url, opts = {
})
参数说明:
html
模板路径fetch
使用html
模板的,如果没有传入,模块内置了默认属性。属性 | 参数 | 返回值 | 功能 | 默认 |
---|---|---|---|---|
fetch | url:string |
promise | 用于获取远端的脚本和样式文件内容 | 浏览器fetch ,如果浏览器不支持,会报错 |
getPublicPath | 模板url:string |
publicPath:string |
用于获取静态资源publicPath ,将模板中外部资源为相对路径的,转换为绝对路径。 |
以当前location.href 为publicPath |
getDomain | ?? | string | 如果没有提供getPublicPath 参数,则使用getDomain ,两者都没有提供的时候,使用默认getPublicPath |
无 |
getTemplate | html模板字符串:string |
html模板字符串:string |
用于支持使用者在模板解析前,做一次处理 | 无处理 |
接口返回promise<pending
,resolve
参数为一个对象,拥有以下属性。
属性 | 类型 | 说明 | 参数 |
---|---|---|---|
template | string | 被处理后的html 模板字符串,外联的样式文件被替换为内联样式 |
- |
assetPublicPath | string | 静态资源的baseURL |
- |
getExternalScripts | function:promise | 将模板中所有script 标签按照出现的先后顺序,提取出内容,组成一个数组 |
- |
getExternalStyleSheets | function:promise | 将模板中所有link 和style 标签按照出现的先后顺序,提取出内容,组成一个数组 |
- |
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
,如果模板中没有被标记为entry
的script
标签,则会返回最后一个script
标签的src
值。22
行,调用getEmbedHTML
函数将所有通过外部引入的样式,转换为内联样式。embedHTML
函数的代码比较简单,可以直接去看。25~31
行,这里使用了getExternalScripts
、getExternalStyleSheets
、execScripts
三个函数,一一来看下。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()));
}
},
));
}
函数的第一个参数是 模板中所有link
和style
标签组成的数组,第二个参数是用于请求的fetch
,函数比较简单,主要是通过对link
和style
的区分,分别来获取样式的具体内容组成数组,并返回。
后面发现,在解析模板的时候
style
标签的内容并没有被放入styles
中,不知道是不是一个失误,issue
准备中_
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},...]
。这段代码太长,下面的代码中,将和性能测试相关的部分删除掉了,只留下了功能代码。
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
这个函数的主要作用,是通过修改脚本字符串,改变脚本执行时候的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);
通过这一波操作,给脚本字符串构件了一个简单的执行环境,该环境屏蔽了全局了this
、window
和self
。但是这里默认传入的依然是window
,只是在调用的时候可以通过参数传入。
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
主要是提供了一些工具函数,其中getGlobalProp
和noteGlobalProps
比较有意思,用于根据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
用于标记执行entry
前window
的属性状态,执行entry
模块后,会导出结果并挂载到window
上。getGlobalProp
用于检测entry
模块执行后window
的变化,根据变化找出entry
的指向结果并返回。文章浏览阅读290次,点赞8次,收藏10次。1.背景介绍稀疏编码是一种用于处理稀疏数据的编码技术,其主要应用于信息传输、存储和处理等领域。稀疏数据是指数据中大部分元素为零或近似于零的数据,例如文本、图像、音频、视频等。稀疏编码的核心思想是将稀疏数据表示为非零元素和它们对应的位置信息,从而减少存储空间和计算复杂度。稀疏编码的研究起源于1990年代,随着大数据时代的到来,稀疏编码技术的应用范围和影响力不断扩大。目前,稀疏编码已经成为计算...
文章浏览阅读217次。EasyGBS - GB28181 国标方案安装使用文档下载安装包下载,正式使用需商业授权, 功能一致在线演示在线API架构图EasySIPCMSSIP 中心信令服务, 单节点, 自带一个 Redis Server, 随 EasySIPCMS 自启动, 不需要手动运行EasySIPSMSSIP 流媒体服务, 根..._easygbs-windows-2.6.0-23042316使用文档
文章浏览阅读1.2k次,点赞27次,收藏7次。2023巅峰极客 BabyURL之前AliyunCTF Bypassit I这题考查了这样一条链子:其实就是Jackson的原生反序列化利用今天复现的这题也是大同小异,一起来整一下。_原生jackson 反序列化链子
文章浏览阅读734次,点赞9次,收藏7次。微服务架构简单的说就是将单体应用进一步拆分,拆分成更小的服务,每个服务都是一个可以独立运行的项目。这么多小服务,如何管理他们?(服务治理 注册中心[服务注册 发现 剔除])这么多小服务,他们之间如何通讯?这么多小服务,客户端怎么访问他们?(网关)这么多小服务,一旦出现问题了,应该如何自处理?(容错)这么多小服务,一旦出现问题了,应该如何排错?(链路追踪)对于上面的问题,是任何一个微服务设计者都不能绕过去的,因此大部分的微服务产品都针对每一个问题提供了相应的组件来解决它们。_spring cloud
文章浏览阅读5.9k次,点赞6次,收藏20次。Js实现图片点击切换与轮播图片点击切换<!DOCTYPE html><html> <head> <meta charset="UTF-8"> <title></title> <script type="text/ja..._点击图片进行轮播图切换
文章浏览阅读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月,在比特币飞涨的时代某网站曾经..._使用物联网漏洞的使用者
文章浏览阅读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
文章浏览阅读708次,点赞2次,收藏3次。开放信息抽取(OIE)系统(三)-- 第二代开放信息抽取系统(人工规则, rule-based, 先关系再实体)一.第二代开放信息抽取系统背景 第一代开放信息抽取系统(Open Information Extraction, OIE, learning-based, 自学习, 先抽取实体)通常抽取大量冗余信息,为了消除这些冗余信息,诞生了第二代开放信息抽取系统。二.第二代开放信息抽取系统历史第二代开放信息抽取系统着眼于解决第一代系统的三大问题: 大量非信息性提取(即省略关键信息的提取)、_语义角色增强的关系抽取
文章浏览阅读1.1w次,点赞6次,收藏51次。快速完成网页设计,10个顶尖响应式HTML5网页模板助你一臂之力为了寻找一个优质的网页模板,网页设计师和开发者往往可能会花上大半天的时间。不过幸运的是,现在的网页设计师和开发人员已经开始共享HTML5,Bootstrap和CSS3中的免费网页模板资源。鉴于网站模板的灵活性和强大的功能,现在广大设计师和开发者对html5网站的实际需求日益增长。为了造福大众,Mockplus的小伙伴整理了2018年最..._html欢迎页面
文章浏览阅读282次。原标题:2018全国计算机等级考试调整,一、二级都增加了考试科目全国计算机等级考试将于9月15-17日举行。在备考的最后冲刺阶段,小编为大家整理了今年新公布的全国计算机等级考试调整方案,希望对备考的小伙伴有所帮助,快随小编往下看吧!从2018年3月开始,全国计算机等级考试实施2018版考试大纲,并按新体系开考各个考试级别。具体调整内容如下:一、考试级别及科目1.一级新增“网络安全素质教育”科目(代..._计算机二级增报科目什么意思
文章浏览阅读240次。conan简单使用。_apt install conan