简介
从 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 独立打包等优化措施。
留神:子利用也能够将包打成多个,而后利用 webpack 的 webpack-manifest-plugin 插件打包出 manifest.json 文件,生成一份资源清单,而后主利用的 loadApp 近程读取每个子利用的清单文件,顺次加载文件外面的资源;不过该计划也没方法享受子利用的按需加载能力
我的项目公布当前呈现了 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
{ template: 通过解决的脚本,link、script 标签都被正文掉了, scripts: [脚本的http地址 或者 { async: true, src: xx } 或者 代码块], styles: [款式的http地址], entry: 入口脚本的地址,要不是标有 entry 的 script 的 src,要不就是最初一个 script 标签的 src}
而后近程加载 styles
中的款式内容,将 template
模版中正文掉的 link
标签替换为相应的 style
元素。
而后向外裸露一个 Promise
对象
{ // template 是 link 替换为 style 后的 template template: embedHTML, // 动态资源地址 assetPublicPath, // 获取内部脚本,最终失去所有脚本的代码内容 getExternalScripts: () => getExternalScripts(scripts, fetch), // 获取内部款式文件的内容 getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch), // 脚本执行器,让 JS 代码(scripts)在指定 上下文 中运行 execScripts: (proxy, strictGlobal) => { if (!scripts.length) { return Promise.resolve(); } return execScripts(entry, scripts, proxy, { fetch, strictGlobal }); }}
这就是 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
/** * 加载指定地址的首屏内容 * @param {*} entry 能够是一个字符串格局的地址,比方 localhost:8080,也能够是一个配置对象,比方 { scripts, styles, html } * @param {*} opts * return importHTML 的执行后果 */export function importEntry(entry, opts = {}) { // 从 opt 参数中解析出 fetch 办法 和 getTemplate 办法,没有就用默认的 const { fetch = defaultFetch, getTemplate = defaultGetTemplate } = opts; // 获取动态资源地址的一个办法 const getPublicPath = opts.getPublicPath || opts.getDomain || defaultGetPublicPath; if (!entry) { throw new SyntaxError('entry should not be empty!'); } // html entry,entry 是一个字符串格局的地址 if (typeof entry === 'string') { return importHTML(entry, { fetch, getPublicPath, getTemplate }); } // config entry,entry 是一个对象 = { scripts, styles, html } 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 => ({ template: embedHTML, assetPublicPath: getPublicPath(entry), getExternalScripts: () => getExternalScripts(scripts, fetch), getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch), execScripts: (proxy, strictGlobal) => { if (!scripts.length) { return Promise.resolve(); } return execScripts(scripts[scripts.length - 1], scripts, proxy, { fetch, strictGlobal }); }, })); } else { throw new SyntaxError('entry scripts or styles should be array!'); }}
importHTML
/** * 加载指定地址的首屏内容 * @param {*} url * @param {*} opts * return Promise<{ // template 是 link 替换为 style 后的 template template: embedHTML, // 动态资源地址 assetPublicPath, // 获取内部脚本,最终失去所有脚本的代码内容 getExternalScripts: () => getExternalScripts(scripts, fetch), // 获取内部款式文件的内容 getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch), // 脚本执行器,让 JS 代码(scripts)在指定 上下文 中运行 execScripts: (proxy, strictGlobal) => { if (!scripts.length) { return Promise.resolve(); } return execScripts(entry, scripts, proxy, { fetch, strictGlobal }); }, }> */export default function importHTML(url, opts = {}) { // 三个默认的办法 let fetch = defaultFetch; let getPublicPath = defaultGetPublicPath; let getTemplate = defaultGetTemplate; if (typeof opts === 'function') { // if 分支,兼容遗留的 importHTML api,ops 能够间接是一个 fetch 办法 fetch = opts; } else { // 用用户传递的参数(如果提供了的话)笼罩默认办法 fetch = opts.fetch || defaultFetch; getPublicPath = opts.getPublicPath || opts.getDomain || defaultGetPublicPath; getTemplate = opts.getTemplate || defaultGetTemplate; } // 通过 fetch 办法申请 url,这也就是 qiankun 为什么要求你的微利用要反对跨域的起因 return embedHTMLCache[url] || (embedHTMLCache[url] = fetch(url) // response.text() 是一个 html 模版 .then(response => response.text()) .then(html => { // 获取动态资源地址 const assetPublicPath = getPublicPath(url); /** * 从 html 模版中解析出内部脚本的地址或者内联脚本的代码块 和 link 标签的地址 * { * template: 通过解决的脚本,link、script 标签都被正文掉了, * scripts: [脚本的http地址 或者 { async: true, src: xx } 或者 代码块], * styles: [款式的http地址], * entry: 入口脚本的地址,要不是标有 entry 的 script 的 src,要不就是最初一个 script 标签的 src * } */ const { template, scripts, entry, styles } = processTpl(getTemplate(html), assetPublicPath); // getEmbedHTML 办法通过 fetch 近程加载所有的内部款式,而后将对应的 link 正文标签替换为 style,即内部款式替换为内联款式,而后返回 embedHTML,即解决过后的 HTML 模版 return getEmbedHTML(template, styles, { fetch }).then(embedHTML => ({ // template 是 link 替换为 style 后的 template template: embedHTML, // 动态资源地址 assetPublicPath, // 获取内部脚本,最终失去所有脚本的代码内容 getExternalScripts: () => getExternalScripts(scripts, fetch), // 获取内部款式文件的内容 getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch), // 脚本执行器,让 JS 代码(scripts)在指定 上下文 中运行 execScripts: (proxy, strictGlobal) => { if (!scripts.length) { return Promise.resolve(); } return execScripts(entry, scripts, proxy, { fetch, strictGlobal }); }, })); }));}
processTpl
/** * 从 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 * } */export default function processTpl(tpl, baseURI) { let scripts = []; const styles = []; let entry = null; // 判断浏览器是否反对 es module,<script type = "module" /> const moduleSupport = isModuleScriptSupported(); const template = tpl // 移除 html 模版中的正文内容 <!-- xx --> .replace(HTML_COMMENT_REGEX, '') // 匹配 link 标签 .replace(LINK_TAG_REGEX, match => { /** * 将模版中的 link 标签变成正文,如果有存在 href 属性且非预加载的 link,则将地址存到 styles 数组,如果是预加载的 link 间接变成正文 */ // <link rel = "stylesheet" /> const styleType = !!match.match(STYLE_TYPE_REGEX); if (styleType) { // <link rel = "stylesheet" href = "xxx" /> const styleHref = match.match(STYLE_HREF_REGEX); // <link rel = "stylesheet" ignore /> const styleIgnore = match.match(LINK_IGNORE_REGEX); if (styleHref) { // 获取 href 属性值 const href = styleHref && styleHref[2]; let newHref = 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) { return genIgnoreAssetReplaceSymbol(newHref); } // 将 href 属性值存入 styles 数组 styles.push(newHref); // <link rel = "stylesheet" href = "xxx" /> 变成 <!-- link ${linkHref} replaced by import-html-entry --> return genLinkReplaceSymbol(newHref); } } // 匹配 <link rel = "preload or prefetch" href = "xxx" />,示意预加载资源 const preloadOrPrefetchType = 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 --> return genLinkReplaceSymbol(linkHref, true); } return match; }) // 匹配 <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 --> return genIgnoreAssetReplaceSymbol('style file'); } return match; }) // 匹配 <script></script> .replace(ALL_SCRIPT_REGEX, (match, scriptTag) => { // 匹配 <script ignore></script> const scriptIgnore = scriptTag.match(SCRIPT_IGNORE_REGEX); // 匹配 <script nomodule></script> 或者 <script type = "module"></script>,都属于应该被疏忽的脚本 const moduleScriptIgnore = (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" /> const matchedScriptTypeMatch = scriptTag.match(SCRIPT_TYPE_REGEX); // 获取 type 属性值 const matchedScriptType = matchedScriptTypeMatch && matchedScriptTypeMatch[2]; // 验证 type 是否无效,type 为空 或者 'text/javascript', 'module', 'application/javascript', 'text/ecmascript', 'application/ecmascript',都视为无效 if (!isValidJavaScriptType(matchedScriptType)) { return match; } // 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 /> const matchedScriptEntry = scriptTag.match(SCRIPT_ENTRY_REGEX); // <script src = "xx" /> const matchedScriptSrcMatch = scriptTag.match(SCRIPT_SRC_REGEX); // 脚本地址 let matchedScriptSrc = matchedScriptSrcMatch && matchedScriptSrcMatch[2]; if (entry && matchedScriptEntry) { // 阐明呈现了两个入口地址,即两个 <script entry src = "xx" /> throw new SyntaxError('You should not set multiply entry script!'); } else { // 补全脚本地址,地址如果没有协定,阐明是一个相对路径,增加 baseURI if (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 --> return genIgnoreAssetReplaceSymbol(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 --> return genModuleScriptReplaceSymbol(matchedScriptSrc || 'js file', moduleSupport); } if (matchedScriptSrc) { // 匹配 <script src = 'xx' async />,阐明是异步加载的脚本 const asyncScript = !!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 --> return genScriptReplaceSymbol(matchedScriptSrc, asyncScript); } return match; } else { // 阐明是外部脚本,<script>xx</script> if (scriptIgnore) { // <script ignore /> 替换为 <!-- ignore asset js file replaced by import-html-entry --> return genIgnoreAssetReplaceSymbol('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 --> return genModuleScriptReplaceSymbol('js file', moduleSupport); } // if it is an inline script,<script>xx</script>,失去标签之间的代码 => xx const code = getInlineCode(match); // remove script blocks when all of these lines are comments. 判断代码块是否全是正文 const isPureCommentBlock = 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 --> return inlineScriptReplaceSymbol; } }); // filter empty script scripts = scripts.filter(function (script) { return !!script; }); return { template, scripts, styles, // set the last script as entry if have not set entry: 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 模版 */function getEmbedHTML(template, styles, opts = {}) { const { fetch = defaultFetch } = opts; let embedHTML = template; return getExternalStyleSheets(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>`); return html; }, embedHTML); return embedHTML; });}
getExternalScripts
/** * 加载脚本,最终返回脚本的内容,Promise<Array>,每个元素都是一段 JS 代码 * @param {*} scripts = [脚本http地址 or 内联脚本的脚本内容 or { async: true, src: xx }] * @param {*} fetch * @param {*} errorCallback */export function getExternalScripts(scripts, fetch = defaultFetch, errorCallback = () => {}) { // 定义一个能够加载近程指定 url 脚本的办法,当然外面也做了缓存,如果命中缓存间接从缓存中获取 const fetchScript = 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/5625603 if (response.status >= 400) { errorCallback(); throw new Error(`${scriptUrl} load failed with status ${response.status}`); } return 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 // 异步脚本,通过 requestIdleCallback 办法加载 const { src, async } = script; if (async) { return { src, async: true, content: new Promise((resolve, reject) => requestIdleCallback(() => fetchScript(src).then(resolve, reject))), }; } return fetchScript(src); } }, ));}
getExternalStyleSheets
/** * 通过 fetch 办法加载指定地址的款式文件 * @param {*} styles = [ href ] * @param {*} fetch * return Promise<Array>,每个元素都是一堆款式内容 */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())); } }, ));}
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>} */export function execScripts(entry, scripts, proxy = window, opts = {}) { const { fetch = defaultFetch, strictGlobal = false, success, error = () => { }, beforeExec = () => { }, } = opts; // 获取指定的所有内部脚本的内容,并设置每个脚本的执行上下文,而后通过 eval 函数运行 return getExternalScripts(scripts, fetch, error) .then(scriptsText => { // scriptsText 为脚本内容数组 => 每个元素是一段 JS 代码 const geval = (code) => { beforeExec(); (0, eval)(code); }; /** * * @param {*} scriptSrc 脚本地址 * @param {*} inlineScript 脚本内容 * @param {*} resolve */ function exec(scriptSrc, inlineScript, resolve) { // 性能度量 const markName = `Evaluating script ${scriptSrc}`; const measureName = `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 script geval(getExecutableScript(scriptSrc, inlineScript, proxy, strictGlobal)); const exports = proxy[getGlobalProp(strictGlobal ? proxy : window)] || {}; resolve(exports); } catch (e) { // entry error must be thrown to make the promise settled console.error(`[import-html-entry]: error occurs while executing entry script ${scriptSrc}`); throw e; } } else { if (typeof inlineScript === '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 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(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 胜利回调 */ function schedule(i, resolvePromise) { if (i < scripts.length) { // 第 i 个脚本的地址 const scriptSrc = scripts[i]; // 第 i 个脚本的内容 const inlineScript = scriptsText[i]; exec(scriptSrc, inlineScript, resolvePromise); if (!entry && i === scripts.length - 1) { // resolve the promise while the last script executed and entry not provided resolvePromise(); } else { // 递归调用下一个脚本 schedule(i + 1, resolvePromise); } } } // 从第 0 个脚本开始调度 return new Promise(resolve => schedule(0, success || resolve)); });}
结语
以上就是 HTML Entry
的全部内容,也是深刻了解 微前端
、single-spa
、qiankun
不可或缺的一部分,源码在 github
浏览到这里如果你想持续深刻了解 微前端
、single-spa
、qiankun
等,举荐浏览如下内容
微前端专栏
- 微前端框架 之 single-spa 从入门到精通
- 微前端框架 之 qiankun 从入门到源码剖析
- qiankun 2.x 运行时沙箱 源码剖析
- single-spa 官网
- qiankun 官网
感激各位的:点赞、珍藏和评论,咱们下期见。
当学习成为了习惯,常识也就变成了常识,扫码关注微信公众号,独特学习、提高。文章已收录到 github,欢送 Watch 和 Star。