查看原文
其他

微前端框架是怎么导入加载子应用的

Peter 谭金杰 前端大全 2021-01-14

(给前端大全加星标,提升前端技能

作者:前端巅峰 公号 / Peter 谭金杰

微前端似乎是最近一个很火的话题,我们也即将使用在生产环境中,接下来会更新一系列微前端源码分析、手写微前端文章




废话不多说,直接参考目前的微前端框架注册子应用模块代码


下面代码,我指定的entry,就是子应用的访问入口地址




微前端到底是怎么回事呢?  我画了一张图



我们今天不谈其他的实现技术细节,坑点,就谈整体架构,这张图就能完全解释清楚



那么registerMicroApps,到底做了什么呢?



源码解析下,只看重要部分今天:


lifeCycles是我们自己传入的生命周期函数(这里先不解释),跟react这种框架一样,微前端针对每个子应用,也封装了一些生命周期,如果你是小白,那我就用最简单的话告诉你,生命周期钩子,其实在框架源码就是一个函数编写调用顺序而已(有的分异步和同步)


apps就是我们传入的数组,子应用集合


代码里做了一些防重复注册、数据处理等


看源码,不要全部都看,那样很费时间,而且你也得不到利益最大化,只看最精髓、重要部分


无论上面做了上面子应用去重、数据处理,我只要盯着每个子应用,即app这个对象即可


看到了loadApp这个方法,我们可以大概猜测到,是通过这个方法加载


下面__rest是对数据进行处理




loadApp这个函数有大概300行,挑最重点地方看


registerApplication是single-spa的方法,我们这里通过loadApp这个方法,对数据进行处理



上面这个函数,应该是整个微前端框架最复杂的地方,它最终会返回一个函数,当成函数传递给single-spa这个库的registerApplication方法使用


它的内部是switch case逻辑,然后返回一个数组


这是一个逻辑判断


case 0: entry = app.entry, appName = app.name; _b = configuration.singular, singular = _b === void 0 ? false : _b, _c = configuration.sandbox, sandbox = _c === void 0 ? true : _c, importEntryOpts = __rest(configuration, ["singular", "sandbox"]);return [4/*yield*/          , importEntry(entry, importEntryOpts)];


重点来了


会通过importEntry 去加载entry(子应用地址)




上面代码里最重要的,如果我们entry传入字符串,那么就会使用这个函数去加载HTML内容(其实微前端的所有子应用加载,都是把dom节点加载渲染到基座的index.html文件中的一个div标签内


importHTML这个函数,就是我们今晚最重要的一个点


传入url地址,发起fetch请求(此时由于域名或者端口不一样,会出现跨域,所有子应用的热更新开发模式下,webpack配置要做以下处理,部署也要考虑这个问题)



整个importHTML函数好像很长很长,但是我们就看最重要的地方,一个框架(库),流程线很长+版本迭代原因,需要兼容老的版本,所以很多源码对于我们其实是无用的



整个函数,最后返回了一个对象,这里很明显,通过fetch请求,获取了对应子应用entry入口的资源文件后,转换成了字符串


这里processTpl其实就是对这个子应用的dom模版(字符串格式)进行一个数据拼装,其实也不是很复杂,由于时间关系,可以自己看看过程,重点看结果


这里的思想,是redux的中间件源码思想,将数据进行了一层包装,高可用使用


function processTpl(tpl, baseURI) {var scripts = [];var styles = [];var entry = null;var template = tpl/* remove html comment first */ .replace(HTML_COMMENT_REGEX, '').replace(LINK_TAG_REGEX, function (match) {/* change the css link */var styleType = !!match.match(STYLE_TYPE_REGEX);
if (styleType) {var styleHref = match.match(STYLE_HREF_REGEX);var styleIgnore = match.match(LINK_IGNORE_REGEX);
if (styleHref) {var href = styleHref && styleHref[2];var newHref = href;
if (href && !hasProtocol(href)) { newHref = getEntirePath(href, baseURI); }
if (styleIgnore) {return genIgnoreAssetReplaceSymbol(newHref); }
styles.push(newHref);return genLinkReplaceSymbol(newHref); } }
var preloadOrPrefetchType = match.match(LINK_PRELOAD_OR_PREFETCH_REGEX) && match.match(LINK_HREF_REGEX);
if (preloadOrPrefetchType) {var _match$match = match.match(LINK_HREF_REGEX), _match$match2 = (0, _slicedToArray2["default"])(_match$match, 3), linkHref = _match$match2[2];
return genLinkReplaceSymbol(linkHref, true); }
return match; }).replace(STYLE_TAG_REGEX, function (match) {if (STYLE_IGNORE_REGEX.test(match)) {return genIgnoreAssetReplaceSymbol('style file'); }
return match; }).replace(ALL_SCRIPT_REGEX, function (match) {var scriptIgnore = match.match(SCRIPT_IGNORE_REGEX); // in order to keep the exec order of all javascripts// if it is a external script
if (SCRIPT_TAG_REGEX.test(match) && match.match(SCRIPT_SRC_REGEX)) {/* collect scripts and replace the ref */var matchedScriptEntry = match.match(SCRIPT_ENTRY_REGEX);var matchedScriptSrcMatch = match.match(SCRIPT_SRC_REGEX);var matchedScriptSrc = matchedScriptSrcMatch && matchedScriptSrcMatch[2];
if (entry && matchedScriptEntry) {throw new SyntaxError('You should not set multiply entry script!'); } else {// append the domain while the script not have an protocol prefixif (matchedScriptSrc && !hasProtocol(matchedScriptSrc)) { matchedScriptSrc = getEntirePath(matchedScriptSrc, baseURI); }
entry = entry || matchedScriptEntry && matchedScriptSrc; }
if (scriptIgnore) {return genIgnoreAssetReplaceSymbol(matchedScriptSrc || 'js file'); }
if (matchedScriptSrc) {var asyncScript = !!match.match(SCRIPT_ASYNC_REGEX); scripts.push(asyncScript ? { async: true, src: matchedScriptSrc } : matchedScriptSrc);return genScriptReplaceSymbol(matchedScriptSrc, asyncScript); }
return match; } else {if (scriptIgnore) {return genIgnoreAssetReplaceSymbol('js file'); } // if it is an inline script

var code = (0, _utils.getInlineCode)(match); // remove script blocks when all of these lines are comments.
var isPureCommentBlock = code.split(/[\r\n]+/).every(function (line) {return !line.trim() || line.trim().startsWith('//'); });
if (!isPureCommentBlock) { scripts.push(match); }
return inlineScriptReplaceSymbol; } }); scripts = scripts.filter(function (script) {// filter empty scriptreturn !!script; });return { template: template, scripts: scripts, styles: styles,// set the last script as entry if have not set entry: entry || scripts[scripts.length - 1] };}


最终返回了一个对象,此时已经不是一个纯html的字符串了,而是一个对象,而且脚本样式都分离了



这个是框架帮我们处理的,必须要设置一个入口js文件


// set the last script as entry if have not set

下面是真正的single-spa源码,注册子应用,用apps这个数组去收集所有的子应用(数组每一项已经拥有了脚本、html、css样式的内容)



此时我们只要根据我们之前编写的activeRule和监听前端路由变化去控制展示子应用即可,原理如下:(今天不做过多讲解这块)


window.addEventListener('hashchange', reroute);window.addEventListener('popstate', reroute);
// 拦截所有注册的事件,以便确保这里的事件总是第一个执行const originalAddEventListener = window.addEventListener;const originalRemoveEventListener = window.removeEventListener;window.addEventListener = function (eventName, handler, args) { if (eventName && HIJACK_EVENTS_NAME.test(eventName) && typeof handler === 'function') { EVENTS_POOL[eventName].indexOf(handler) === -1 && EVENTS_POOL[eventName].push(handler); } return originalAddEventListener.apply(this, arguments);};
window.removeEventListener = function (eventName, handler) { if (eventName && HIJACK_EVENTS_NAME.test(eventName) && typeof handler === 'function') { let eventList = EVENTS_POOL[eventName]; eventList.indexOf(handler) > -1 && (EVENTS_POOL[eventName] = eventList.filter(fn => fn !== handler)); } return originalRemoveEventListener.apply(this, arguments);};



也是redux的中间件思想,劫持了事件,然后进行派发,优先调用微前端框架的路由事件,然后进行过滤展示子应用:

export function getAppsToLoad() { return APPS.filter(notSkipped).filter(withoutLoadError).filter(isntLoaded).filter(shouldBeActive);}


整个微前端的触发流程






推荐阅读  点击标题可跳转

万字解析微前端、微前端框架qiankun以及源码

了解什么是微前端

前端如何在繁忙的业务中提升自己


觉得本文对你有帮助?请分享给更多人

关注「前端大全」加星标,提升前端技能

好文章,我在看❤️

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存