微前端框架是怎么導入加載子應用的
微前端似乎是最近一個很火的話題,我們也即將使用在生產(chǎn)環(huán)境中,接下來會更新一系列微前端源碼分析、手寫微前端文章
廢話不多說,直接參考目前的微前端框架注冊子應用模塊代碼
下面代碼,我指定的entry,就是子應用的訪問入口地址
微前端到底是怎么回事呢? 我畫了一張圖
我們今天不談其他的實現(xiàn)技術(shù)細節(jié),坑點,就談整體架構(gòu),這張圖就能完全解釋清楚
那么registerMicroApps,到底做了什么呢?
源碼解析下,只看重要部分今天:
lifeCycles是我們自己傳入的生命周期函數(shù)(這里先不解釋),跟react這種框架一樣,微前端針對每個子應用,也封裝了一些生命周期,如果你是小白,那我就用最簡單的話告訴你,生命周期鉤子,其實在框架源碼就是一個函數(shù)編寫調(diào)用順序而已(有的分異步和同步)
apps就是我們傳入的數(shù)組,子應用集合
代碼里做了一些防重復注冊、數(shù)據(jù)處理等
看源碼,不要全部都看,那樣很費時間,而且你也得不到利益最大化,只看最精髓、重要部分
無論上面做了上面子應用去重、數(shù)據(jù)處理,我只要盯著每個子應用,即app這個對象即可
看到了loadApp這個方法,我們可以大概猜測到,是通過這個方法加載
下面__rest是對數(shù)據(jù)進行處理
loadApp這個函數(shù)有大概300行,挑最重點地方看
registerApplication是single-spa的方法,我們這里通過loadApp這個方法,對數(shù)據(jù)進行處理
上面這個函數(shù),應該是整個微前端框架最復雜的地方,它最終會返回一個函數(shù),當成函數(shù)傳遞給single-spa這個庫的registerApplication方法使用
它的內(nèi)部是switch case邏輯,然后返回一個數(shù)組
這是一個邏輯判斷
- case 0:
- entry = app.entry, appappName = 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傳入字符串,那么就會使用這個函數(shù)去加載HTML內(nèi)容(其實微前端的所有子應用加載,都是把dom節(jié)點加載渲染到基座的index.html文件中的一個div標簽內(nèi))
importHTML這個函數(shù),就是我們今晚最重要的一個點
傳入url地址,發(fā)起fetch請求(此時由于域名或者端口不一樣,會出現(xiàn)跨域,所有子應用的熱更新開發(fā)模式下,webpack配置要做以下處理,部署也要考慮這個問題)
整個importHTML函數(shù)好像很長很長,但是我們就看最重要的地方,一個框架(庫),流程線很長+版本迭代原因,需要兼容老的版本,所以很多源碼對于我們其實是無用的
整個函數(shù),最后返回了一個對象,這里很明顯,通過fetch請求,獲取了對應子應用entry入口的資源文件后,轉(zhuǎn)換成了字符串
這里processTpl其實就是對這個子應用的dom模版(字符串格式)進行一個數(shù)據(jù)拼裝,其實也不是很復雜,由于時間關系,可以自己看看過程,重點看結(jié)果
這里的思想,是redux的中間件源碼思想,將數(shù)據(jù)進行了一層包裝,高可用使用
- 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$matchmatch = 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 matchmatchedScriptEntry = match.match(SCRIPT_ENTRY_REGEX);
- var matchmatchedScriptSrcMatch = 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 prefix
- if (matchedScriptSrc && !hasProtocol(matchedScriptSrc)) {
- matchedScriptSrc = getEntirePath(matchedScriptSrc, baseURI);
- }
- entryentry = 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;
- }
- });
- scriptsscripts = scripts.filter(function (script) {
- // filter empty script
- return !!script;
- });
- return {
- template: template,
- scripts: scripts,
- styles: styles,
- // set the last script as entry if have not set
- entry: entry || scripts[scripts.length - 1]
- };
- }
最終返回了一個對象,此時已經(jīng)不是一個純html的字符串了,而是一個對象,而且腳本樣式都分離了
這個是框架幫我們處理的,必須要設置一個入口js文件
- // set the last script as entry if have not set
下面是真正的single-spa源碼,注冊子應用,用apps這個數(shù)組去收集所有的子應用(數(shù)組每一項已經(jīng)擁有了腳本、html、css樣式的內(nèi)容)
此時我們只要根據(jù)我們之前編寫的activeRule和監(jiān)聽前端路由變化去控制展示子應用即可,原理如下:(今天不做過多講解這塊)
- window.addEventListener('hashchange', reroute);
- window.addEventListener('popstate', reroute);
- // 攔截所有注冊的事件,以便確保這里的事件總是第一個執(zhí)行
- 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的中間件思想,劫持了事件,然后進行派發(fā),優(yōu)先調(diào)用微前端框架的路由事件,然后進行過濾展示子應用:
- export function getAppsToLoad() {
- return APPS.filter(notSkipped).filter(withoutLoadError).filter(isntLoaded).filter(shouldBeActive);
- }
整個微前端的觸發(fā)流程