微前端框架是怎麼導入加載子應用的 【3000字精讀】

寫在開頭:

微前端彷佛是最近一個很火的話題,咱們也即將使用在生產環境中,接下來會更新一系列微前端源碼分析、手寫微前端文章javascript


廢話很少說,直接參考目前的微前端框架註冊子應用模塊代碼

  • 下面代碼,我指定的entry,就是子應用的訪問入口地址

  • 微前端究竟是怎麼回事呢? 我畫了一張圖

咱們今天不談其餘的實現技術細節,坑點,就談總體架構,這張圖就能徹底解釋清楚

那麼registerMicroApps,到底作了什麼呢?css

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

lifeCycles是咱們本身傳入的生命週期函數(這裏先不解釋),跟react這種框架同樣,微前端針對每一個子應用,也封裝了一些生命週期,若是你是小白,那我就用最簡單的話告訴你,生命週期鉤子,其實在框架源碼就是一個函數編寫調用順序而已(有的分異步和同步)html

  • 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 prefix
if (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 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]
  };
}
  • 最終返回了一個對象,此時已經不是一個純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);
}

整個微前端的觸發流程圖

相信經過此文,你能真正瞭解微前端的使用原理,後期我會出一個手寫微前端框架的文章前端

最後

歡迎加我微信(CALASFxiaotan),拉你進技術羣,長期交流學習...java

歡迎關注公衆號「前端巔峯」,認真學前端,作個有專業的技術人...react

點個贊支持我吧,轉發就更好了webpack

相關文章
相關標籤/搜索