You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
/** * 从 html 模版中解析出外部脚本的地址或者内联脚本的代码块 和 link 标签的地址 * @param tpl html 模版 * @param baseURI * @stripStyles whether to strip the css links * @returns {{template: void | string | *, scripts: *[], entry: *}} * return { * template: 经过处理的脚本,link、script 标签都被注释掉了, * scripts: [脚本的http地址 或者 { async: true, src: xx } 或者 代码块], * styles: [样式的http地址], * entry: 入口脚本的地址,要不是标有 entry 的 script 的 src,要不就是最后一个 script 标签的 src * } */exportdefaultfunctionprocessTpl(tpl,baseURI){letscripts=[];conststyles=[];letentry=null;// 判断浏览器是否支持 es module,<script type = "module" />constmoduleSupport=isModuleScriptSupported();consttemplate=tpl// 移除 html 模版中的注释内容 <!-- xx -->.replace(HTML_COMMENT_REGEX,'')// 匹配 link 标签.replace(LINK_TAG_REGEX,match=>{/** * 将模版中的 link 标签变成注释,如果有存在 href 属性且非预加载的 link,则将地址存到 styles 数组,如果是预加载的 link 直接变成注释 */// <link rel = "stylesheet" />conststyleType=!!match.match(STYLE_TYPE_REGEX);if(styleType){// <link rel = "stylesheet" href = "xxx" />conststyleHref=match.match(STYLE_HREF_REGEX);// <link rel = "stylesheet" ignore />conststyleIgnore=match.match(LINK_IGNORE_REGEX);if(styleHref){// 获取 href 属性值consthref=styleHref&&styleHref[2];letnewHref=href;// 如果 href 没有协议说明给的是一个相对地址,拼接 baseURI 得到完整地址if(href&&!hasProtocol(href)){newHref=getEntirePath(href,baseURI);}// 将 <link rel = "stylesheet" ignore /> 变成 <!-- ignore asset ${url} replaced by import-html-entry -->if(styleIgnore){returngenIgnoreAssetReplaceSymbol(newHref);}// 将 href 属性值存入 styles 数组styles.push(newHref);// <link rel = "stylesheet" href = "xxx" /> 变成 <!-- link ${linkHref} replaced by import-html-entry -->returngenLinkReplaceSymbol(newHref);}}// 匹配 <link rel = "preload or prefetch" href = "xxx" />,表示预加载资源constpreloadOrPrefetchType=match.match(LINK_PRELOAD_OR_PREFETCH_REGEX)&&match.match(LINK_HREF_REGEX)&&!match.match(LINK_AS_FONT);if(preloadOrPrefetchType){// 得到 href 地址const[,,linkHref]=match.match(LINK_HREF_REGEX);// 将标签变成 <!-- prefetch/preload link ${linkHref} replaced by import-html-entry -->returngenLinkReplaceSymbol(linkHref,true);}returnmatch;})// 匹配 <style></style>.replace(STYLE_TAG_REGEX,match=>{if(STYLE_IGNORE_REGEX.test(match)){// <style ignore></style> 变成 <!-- ignore asset style file replaced by import-html-entry -->returngenIgnoreAssetReplaceSymbol('style file');}returnmatch;})// 匹配 <script></script>.replace(ALL_SCRIPT_REGEX,(match,scriptTag)=>{// 匹配 <script ignore></script>constscriptIgnore=scriptTag.match(SCRIPT_IGNORE_REGEX);// 匹配 <script nomodule></script> 或者 <script type = "module"></script>,都属于应该被忽略的脚本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" />constmatchedScriptTypeMatch=scriptTag.match(SCRIPT_TYPE_REGEX);// 获取 type 属性值constmatchedScriptType=matchedScriptTypeMatch&&matchedScriptTypeMatch[2];// 验证 type 是否有效,type 为空 或者 'text/javascript', 'module', 'application/javascript', 'text/ecmascript', 'application/ecmascript',都视为有效if(!isValidJavaScriptType(matchedScriptType)){returnmatch;}// if it is a external script,匹配非 <script type = "text/ng-template" src = "xxx"></script>if(SCRIPT_TAG_REGEX.test(match)&&scriptTag.match(SCRIPT_SRC_REGEX)){/* collect scripts and replace the ref */// <script entry />constmatchedScriptEntry=scriptTag.match(SCRIPT_ENTRY_REGEX);// <script src = "xx" />constmatchedScriptSrcMatch=scriptTag.match(SCRIPT_SRC_REGEX);// 脚本地址letmatchedScriptSrc=matchedScriptSrcMatch&&matchedScriptSrcMatch[2];if(entry&&matchedScriptEntry){// 说明出现了两个入口地址,即两个 <script entry src = "xx" />thrownewSyntaxError('You should not set multiply entry script!');}else{// 补全脚本地址,地址如果没有协议,说明是一个相对路径,添加 baseURIif(matchedScriptSrc&&!hasProtocol(matchedScriptSrc)){matchedScriptSrc=getEntirePath(matchedScriptSrc,baseURI);}// 脚本的入口地址entry=entry||matchedScriptEntry&&matchedScriptSrc;}if(scriptIgnore){// <script ignore></script> 替换为 <!-- ignore asset ${url || 'file'} replaced by import-html-entry -->returngenIgnoreAssetReplaceSymbol(matchedScriptSrc||'js file');}if(moduleScriptIgnore){// <script nomodule></script> 或者 <script type = "module"></script> 替换为// <!-- nomodule script ${scriptSrc} ignored by import-html-entry --> 或// <!-- module script ${scriptSrc} ignored by import-html-entry -->returngenModuleScriptReplaceSymbol(matchedScriptSrc||'js file',moduleSupport);}if(matchedScriptSrc){// 匹配 <script src = 'xx' async />,说明是异步加载的脚本constasyncScript=!!scriptTag.match(SCRIPT_ASYNC_REGEX);// 将脚本地址存入 scripts 数组,如果是异步加载,则存入一个对象 { async: true, src: xx }scripts.push(asyncScript ? {async: true,src: matchedScriptSrc} : matchedScriptSrc);// <script src = "xx" async /> 或者 <script src = "xx" /> 替换为 // <!-- async script ${scriptSrc} replaced by import-html-entry --> 或 // <!-- script ${scriptSrc} replaced by import-html-entry -->returngenScriptReplaceSymbol(matchedScriptSrc,asyncScript);}returnmatch;}else{// 说明是内部脚本,<script>xx</script>if(scriptIgnore){// <script ignore /> 替换为 <!-- ignore asset js file replaced by import-html-entry -->returngenIgnoreAssetReplaceSymbol('js file');}if(moduleScriptIgnore){// <script nomodule></script> 或者 <script type = "module"></script> 替换为// <!-- nomodule script ${scriptSrc} ignored by import-html-entry --> 或 // <!-- module script ${scriptSrc} ignored by import-html-entry -->returngenModuleScriptReplaceSymbol('js file',moduleSupport);}// if it is an inline script,<script>xx</script>,得到标签之间的代码 => xxconstcode=getInlineCode(match);// remove script blocks when all of these lines are comments. 判断代码块是否全是注释constisPureCommentBlock=code.split(/[\r\n]+/).every(line=>!line.trim()||line.trim().startsWith('//'));if(!isPureCommentBlock){// 不是注释,则将代码块存入 scripts 数组scripts.push(match);}// <script>xx</script> 替换为 <!-- inline scripts replaced by import-html-entry -->returninlineScriptReplaceSymbol;}});// filter empty scriptscripts=scripts.filter(function(script){return!!script;});return{
template,
scripts,
styles,// set the last script as entry if have not setentry: entry||scripts[scripts.length-1],};}
getEmbedHTML
/** * convert external css link to inline style for performance optimization,外部样式转换成内联样式 * @param template,html 模版 * @param styles link 样式链接 * @param opts = { fetch } * @return embedHTML 处理过后的 html 模版 */functiongetEmbedHTML(template,styles,opts={}){const{ fetch =defaultFetch}=opts;letembedHTML=template;returngetExternalStyleSheets(styles,fetch).then(styleSheets=>{// 通过循环,将之前设置的 link 注释标签替换为 style 标签,即 <style>/* href地址 */ xx </style>embedHTML=styles.reduce((html,styleSrc,i)=>{html=html.replace(genLinkReplaceSymbol(styleSrc),`<style>/* ${styleSrc} */${styleSheets[i]}</style>`);returnhtml;},embedHTML);returnembedHTML;});}
getExternalScripts
/** * 加载脚本,最终返回脚本的内容,Promise<Array>,每个元素都是一段 JS 代码 * @param {*} scripts = [脚本http地址 or 内联脚本的脚本内容 or { async: true, src: xx }] * @param {*} fetch * @param {*} errorCallback */exportfunctiongetExternalScripts(scripts,fetch=defaultFetch,errorCallback=()=>{}){// 定义一个可以加载远程指定 url 脚本的方法,当然里面也做了缓存,如果命中缓存直接从缓存中获取constfetchScript=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/5625603if(response.status>=400){errorCallback();thrownewError(`${scriptUrl} load failed with status ${response.status}`);}returnresponse.text();}));returnPromise.all(scripts.map(script=>{if(typeofscript==='string'){// 字符串,要不是链接地址,要不是脚本内容(代码)if(isInlineCode(script)){// if it is inline scriptreturngetInlineCode(script);}else{// external script,加载脚本returnfetchScript(script);}}else{// use idle time to load async script// 异步脚本,通过 requestIdleCallback 方法加载const{ src, async }=script;if(async){return{
src,async: true,content: newPromise((resolve,reject)=>requestIdleCallback(()=>fetchScript(src).then(resolve,reject))),};}returnfetchScript(src);}},));}
getExternalStyleSheets
/** * 通过 fetch 方法加载指定地址的样式文件 * @param {*} styles = [ href ] * @param {*} fetch * return Promise<Array>,每个元素都是一堆样式内容 */exportfunctiongetExternalStyleSheets(styles,fetch=defaultFetch){returnPromise.all(styles.map(styleLink=>{if(isInlineCode(styleLink)){// if it is inline stylereturngetInlineCode(styleLink);}else{// external styles,加载样式并缓存returnstyleCache[styleLink]||(styleCache[styleLink]=fetch(styleLink).then(response=>response.text()));}},));}
execScripts
/** * FIXME to consistent with browser behavior, we should only provide callback way to invoke success and error event * 脚本执行器,让指定的脚本(scripts)在规定的上下文环境中执行 * @param entry 入口地址 * @param scripts = [脚本http地址 or 内联脚本的脚本内容 or { async: true, src: xx }] * @param proxy 脚本执行上下文,全局对象,qiankun JS 沙箱生成 windowProxy 就是传递到了这个参数 * @param opts * @returns {Promise<unknown>} */exportfunctionexecScripts(entry,scripts,proxy=window,opts={}){const{
fetch =defaultFetch, strictGlobal =false, success, error =()=>{}, beforeExec =()=>{},}=opts;// 获取指定的所有外部脚本的内容,并设置每个脚本的执行上下文,然后通过 eval 函数运行returngetExternalScripts(scripts,fetch,error).then(scriptsText=>{// scriptsText 为脚本内容数组 => 每个元素是一段 JS 代码constgeval=(code)=>{beforeExec();(0,eval)(code);};/** * * @param {*} scriptSrc 脚本地址 * @param {*} inlineScript 脚本内容 * @param {*} resolve */functionexec(scriptSrc,inlineScript,resolve){// 性能度量constmarkName=`Evaluating script ${scriptSrc}`;constmeasureName=`Evaluating Time Consuming: ${scriptSrc}`;if(process.env.NODE_ENV==='development'&&supportsUserTiming){performance.mark(markName);}if(scriptSrc===entry){// 入口noteGlobalProps(strictGlobal ? proxy : window);try{// bind window.proxy to change `this` reference in scriptgeval(getExecutableScript(scriptSrc,inlineScript,proxy,strictGlobal));constexports=proxy[getGlobalProp(strictGlobal ? proxy : window)]||{};resolve(exports);}catch(e){// entry error must be thrown to make the promise settledconsole.error(`[import-html-entry]: error occurs while executing entry script ${scriptSrc}`);throwe;}}else{if(typeofinlineScript==='string'){try{// bind window.proxy to change `this` reference in script,就是设置 JS 代码的执行上下文,然后通过 eval 函数运行运行代码geval(getExecutableScript(scriptSrc,inlineScript,proxy,strictGlobal));}catch(e){// consistent with browser behavior, any independent script evaluation error should not block the othersthrowNonBlockingError(e,`[import-html-entry]: error occurs while executing normal script ${scriptSrc}`);}}else{// external script marked with async,异步加载的代码,下载完以后运行inlineScript.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}`);});}}// 性能度量if(process.env.NODE_ENV==='development'&&supportsUserTiming){performance.measure(measureName,markName);performance.clearMarks(markName);performance.clearMeasures(measureName);}}/** * 递归 * @param {*} i 表示第几个脚本 * @param {*} resolvePromise 成功回调 */functionschedule(i,resolvePromise){if(i<scripts.length){// 第 i 个脚本的地址constscriptSrc=scripts[i];// 第 i 个脚本的内容constinlineScript=scriptsText[i];exec(scriptSrc,inlineScript,resolvePromise);if(!entry&&i===scripts.length-1){// resolve the promise while the last script executed and entry not providedresolvePromise();}else{// 递归调用下一个脚本schedule(i+1,resolvePromise);}}}// 从第 0 个脚本开始调度returnnewPromise(resolve=>schedule(0,success||resolve));});}
结语
以上就是 HTML Entry 的全部内容,也是深入理解 微前端、single-spa、qiankun 不可或缺的一部分,源码在 github
HTML Entry 源码分析
当学习成为了习惯,知识也就变成了常识。感谢各位的 点赞、收藏和评论。
新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn
文章已收录到 github,欢迎 Watch 和 Star。
封面
简介
从 HTML Entry 的诞生原因 -> 原理简述 -> 实际应用 -> 源码分析,带你全方位刨析 HTML Entry 框架。
序言
HTML Entry
这个词大家可能比较陌生,毕竟在google
上搜HTML Entry 是什么 ?
都搜索不到正确的结果。但如果你了解微前端的话,可能就会有一些了解。致读者
本着不浪费大家时间的原则,特此说明,如果你能读懂 HTML Entry 是什么?? 部分,则可继续往下阅读,如果看不懂建议阅读完推荐资料再回来阅读
JS Entry 有什么问题
说到
HTML Entry
就不得不提另外一个词JS Entry
,因为HTML Entry
就是来解决JS Entry
所面临的问题的。微前端领域最著名的两大框架分别是
single-spa
和qiankun
,后者是基于前者做了二次封装,并解决了前者的一些问题。single-spa
就做了两件事情:而
JS Entry
的理念就在加载微应用的时候用到了,在使用single-spa
加载微应用时,我们加载的不是微应用本身,而是微应用导出的JS
文件,而在入口文件中会导出一个对象,这个对象上有bootstrap
、mount
、unmount
这三个接入single-spa
框架必须提供的生命周期方法,其中mount
方法规定了微应用应该怎么挂载到主应用提供的容器节点上,当然你要接入一个微应用,就需要对微应用进行一系列的改造,然而JS Entry
的问题就出在这儿,改造时对微应用的侵入行太强,而且和主应用的耦合性太强。single-spa
采用JS Entry
的方式接入微应用。微应用改造一般分为三步:侵入型强其实说的就是第三点,更改打包工具的配置,使用
single-spa
接入微应用需要将微应用整个打包成一个JS
文件,发布到静态资源服务器,然后在主应用中配置该JS
文件的地址告诉single-spa
去这个地址加载微应用。不说其它的,就现在这个改动就存在很大的问题,将整个微应用打包成一个
JS
文件,常见的打包优化基本上都没了,比如:按需加载、首屏资源加载优化、css 独立打包等优化措施。项目发布以后出现了
bug
,修复之后需要更新上线,为了清除浏览器缓存带来的应用,一般文件名会带上chunkcontent
,微应用发布之后文件名都会发生变化,这时候还需要更新主应用中微应用配置,然后重新编译主应用然后发布,这套操作简直是不能忍受的,这也是 微前端框架 之 single-spa 从入门到精通 这篇文章中示例项目中微应用发布时的环境配置选择development
的原因。qiankun
框架为了解决JS Entry
的问题,于是采用了HTML Entry
的方式,让用户接入微应用就像使用iframe
一样简单。如果以上内容没有看懂,则说明这篇文章不太适合你阅读,建议阅读 微前端框架 之 single-spa 从入门到精通,这篇文章详细讲述了
single-spa
的基础使用和源码原理,阅读完以后再回来读这篇文章会有事半功倍的效果,请读者切勿强行阅读,否则可能出现头昏脑胀的现象。HTML Entry
HTML Entry
是由import-html-entry
库实现的,通过http
请求加载指定地址的首屏内容即html
页面,然后解析这个html
模版得到template
,scripts
,entry
,styles
然后远程加载
styles
中的样式内容,将template
模版中注释掉的link
标签替换为相应的style
元素。然后向外暴露一个
Promise
对象这就是
HTML Entry
的原理,更详细的内容可继续阅读下面的源码分析部分实际应用
qiankun
框架为了解决JS Entry
的问题,就采用了HTML Entry
的方式,让用户接入微应用就像使用iframe
一样简单。通过上面的阅读知道了
HTML Entry
最终会返回一个Promise
对象,qiankun
就用了这个对象中的template
、assetPublicPath
和execScripts
三项,将template
通过DOM
操作添加到主应用中,执行execScripts
方法得到微应用导出的生命周期方法,并且还顺便解决了JS
全局污染的问题,因为执行execScripts
方法的时候可以通过proxy
参数指定JS
的执行上下文。更加具体的内容可阅读 微前端框架 之 qiankun 从入门到源码分析
HTML Entry 源码分析
importEntry
importHTML
processTpl
getEmbedHTML
getExternalScripts
getExternalStyleSheets
execScripts
结语
以上就是
HTML Entry
的全部内容,也是深入理解微前端
、single-spa
、qiankun
不可或缺的一部分,源码在 github阅读到这里如果你想继续深入理解
微前端
、single-spa
、qiankun
等,推荐阅读如下内容微前端专栏
微前端框架 之 single-spa 从入门到精通
微前端框架 之 qiankun 从入门到源码分析
qiankun 2.x 运行时沙箱 源码分析
single-spa 官网
qiankun 官网
感谢各位的:点赞、收藏和评论,我们下期见。
当学习成为了习惯,知识也就变成了常识。感谢各位的 点赞、收藏和评论。
新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn
文章已收录到 github,欢迎 Watch 和 Star。
The text was updated successfully, but these errors were encountered: