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

Peter譚金傑發表於2020-04-24

寫在開頭:

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


廢話不多說,直接參考目前的微前端框架註冊子應用模組程式碼

  • 下面程式碼,我指定的entry,就是子應用的訪問入口地址
registerMicroApps(
  [
    {
      name: 'rental-web',
      entry: isDev ? '//rental-dev.mysoft.com.cn:8809' : `//${url}:8809`,
      container: '#rental-web',
      render,
      activeRule: '/static',
      props: {
        value,
        setValue
      }
    },
    {
      name: 'fed-rental-web',
      entry: isDev ? '//rental-dev.mysoft.com.cn:8006' : `//${url}:8006`,
      container: '#fed-rental-web',
      render,
      activeRule: '/fed',
      props: {
        value,
        setValue
      }
    },]
  • 微前端到底是怎麼回事呢? 我畫了一張圖

我們今天不談其他的實現技術細節,坑點,就談整體架構,這張圖就能完全解釋清楚

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

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

export function registerMicroApps(apps, lifeCycles) {
  var _this = this; // Each app only needs to be registered once


  var unregisteredApps = apps.filter(function (app) {
    return !microApps.some(function (registeredApp) {
      return registeredApp.name === app.name;
    });
  });
  microApps = __spread(microApps, unregisteredApps);
  unregisteredApps.forEach(function (app) {
    var name = app.name,
        activeRule = app.activeRule,
        props = app.props,
        appConfig = __rest(app, ["name", "activeRule", "props"]);

    registerApplication({
      name: name,
      app: function app() {
        return __awaiter(_this, void 0, void 0, function () {
          return __generator(this, function (_a) {
            switch (_a.label) {
              case 0:
                return [4
                /*yield*/
                , frameworkStartedDefer.promise];

              case 1:
                _a.sent();

                return [2
                /*return*/
                , loadApp(__assign({
                  name: name,
                  props: props
                }, appConfig), frameworkConfiguration, lifeCycles)];
            }
          });
        });
      },
      activeWhen: activeRule,
      customProps: props
    });
  });
}

lifeCycles是我們自己傳入的生命週期函式(這裡先不解釋),跟react這種框架一樣,微前端針對每個子應用,也封裝了一些生命週期,如果你是小白,那我就用最簡單的話告訴你,生命週期鉤子,其實在框架原始碼就是一個函式編寫呼叫順序而已(有的分非同步和同步)

  • apps就是我們傳入的陣列,子應用集合
  • 程式碼裡做了一些防重複註冊、資料處理等
  • 看原始碼,不要全部都看,那樣很費時間,而且你也得不到利益最大化,只看最精髓、重要部分
  • 無論上面做了上面子應用去重、資料處理,我只要盯著每個子應用,即app這個物件即可
  • 看到了loadApp這個方法,我們可以大概猜測到,是通過這個方法載入

下面__rest是對資料進行處理

export function __rest(s, e) {
    var t = {};
    for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
        t[p] = s[p];
    if (s != null && typeof Object.getOwnPropertySymbols === "function")
        for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
            if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
                t[p[i]] = s[p[i]];
        }
    return t;
}
  • loadApp這個函式有大概300行,挑最重點地方看
export function loadApp(app, configuration, lifeCycles) {
  if (configuration === void 0) {
    configuration = {};
  }

  var _a;

  return __awaiter(this, void 0, void 0, function () {
    var entry, appName, _b, singular, _c, sandbox, importEntryOpts, _d, template, execScripts, assetPublicPath, appInstanceId, strictStyleIsolation, appContent, element, container, legacyRender, render, containerGetter, global, mountSandbox, unmountSandbox, sandboxInstance, _e, _f, beforeUnmount, _g, afterUnmount, _h, afterMount, _j, beforeMount, _k, beforeLoad, scriptExports, bootstrap, mount, unmount, globalVariableExports, _l, onGlobalStateChange, setGlobalState, offGlobalStateChange;

    var _this = this;

    return __generator(this, function (_m) {
      switch (_m.label) {
        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)];

        case 1:
          _d = _m.sent(), template = _d.template, execScripts = _d.execScripts, assetPublicPath = _d.assetPublicPath;
          return [4
          /*yield*/
          , validateSingularMode(singular, app)];

        case 2:
          if (!_m.sent()) return [3
          /*break*/
          , 4];
          return [4
          /*yield*/
          , prevAppUnmountedDeferred && prevAppUnmountedDeferred.promise];

        case 3:
          _m.sent();

          _m.label = 4;

        case 4:
          appInstanceId = appName + "_" + (appInstanceCounts.hasOwnProperty(appName) ? ((_a = appInstanceCounts[appName]) !== null && _a !== void 0 ? _a : 0) + 1 : 0);
          strictStyleIsolation = _typeof(sandbox) === 'object' && !!sandbox.strictStyleIsolation;
          appContent = getDefaultTplWrapper(appInstanceId)(template);
          element = createElement(appContent, strictStyleIsolation);
          container = 'container' in app ? app.container : undefined;
          legacyRender = 'render' in app ? app.render : undefined;
          render = getRender(appContent, container, legacyRender); // 第一次載入設定應用可見區域 dom 結構
          // 確保每次應用載入前容器 dom 結構已經設定完畢

          render({
            element: element,
            loading: true
          });
          containerGetter = getAppWrapperGetter(appInstanceId, !!legacyRender, strictStyleIsolation, function () {
            return element;
          });
          global = window;

          mountSandbox = function mountSandbox() {
            return Promise.resolve();
          };

          unmountSandbox = function unmountSandbox() {
            return Promise.resolve();
          };

          if (sandbox) {
            sandboxInstance = createSandbox(appName, containerGetter, Boolean(singular)); // 用沙箱的代理物件作為接下來使用的全域性物件

            global = sandboxInstance.proxy;
            mountSandbox = sandboxInstance.mount;
            unmountSandbox = sandboxInstance.unmount;
          }

          _e = _mergeWith({}, getAddOns(global, assetPublicPath), lifeCycles, function (v1, v2) {
            return _concat(v1 !== null && v1 !== void 0 ? v1 : [], v2 !== null && v2 !== void 0 ? v2 : []);
          }), _f = _e.beforeUnmount, beforeUnmount = _f === void 0 ? [] : _f, _g = _e.afterUnmount, afterUnmount = _g === void 0 ? [] : _g, _h = _e.afterMount, afterMount = _h === void 0 ? [] : _h, _j = _e.beforeMount, beforeMount = _j === void 0 ? [] : _j, _k = _e.beforeLoad, beforeLoad = _k === void 0 ? [] : _k;
          return [4
          /*yield*/
          , execHooksChain(toArray(beforeLoad), app)];

        case 5:
          _m.sent(); // cache the execScripts returned promise


          if (!appExportPromiseCaches[appName]) {
            appExportPromiseCaches[appName] = execScripts(global, !singular);
          }

          return [4
          /*yield*/
          , appExportPromiseCaches[appName]];

        case 6:
          scriptExports = _m.sent();

          if (validateExportLifecycle(scriptExports)) {
            // eslint-disable-next-line prefer-destructuring
            bootstrap = scriptExports.bootstrap; // eslint-disable-next-line prefer-destructuring

            mount = scriptExports.mount; // eslint-disable-next-line prefer-destructuring

            unmount = scriptExports.unmount;
          } else {
            if (process.env.NODE_ENV === 'development') {
              console.warn("[qiankun] lifecycle not found from " + appName + " entry exports, fallback to get from window['" + appName + "']");
            }

            globalVariableExports = global[appName];

            if (validateExportLifecycle(globalVariableExports)) {
              // eslint-disable-next-line prefer-destructuring
              bootstrap = globalVariableExports.bootstrap; // eslint-disable-next-line prefer-destructuring

              mount = globalVariableExports.mount; // eslint-disable-next-line prefer-destructuring

              unmount = globalVariableExports.unmount;
            } else {
              delete appExportPromiseCaches[appName];
              throw new Error("[qiankun] You need to export lifecycle functions in " + appName + " entry");
            }
          }

          _l = getMicroAppStateActions(appInstanceId), onGlobalStateChange = _l.onGlobalStateChange, setGlobalState = _l.setGlobalState, offGlobalStateChange = _l.offGlobalStateChange;
          return [2
          /*return*/
          , {
            name: appInstanceId,
            bootstrap: [bootstrap],
            mount: [function () {
              return __awaiter(_this, void 0, void 0, function () {
                return __generator(this, function (_a) {
                  switch (_a.label) {
                    case 0:
                      return [4
                      /*yield*/
                      , validateSingularMode(singular, app)];

                    case 1:
                      if (_a.sent() && prevAppUnmountedDeferred) {
                        return [2
                        /*return*/
                        , prevAppUnmountedDeferred.promise];
                      }

                      return [2
                      /*return*/
                      , undefined];
                  }
                });
              });
            }, // 新增 mount hook, 確保每次應用載入前容器 dom 結構已經設定完畢
            function () {
              return __awaiter(_this, void 0, void 0, function () {
                return __generator(this, function (_a) {
                  // element would be destroyed after unmounted, we need to recreate it if it not exist
                  element = element || createElement(appContent, strictStyleIsolation);
                  render({
                    element: element,
                    loading: true
                  });
                  return [2
                  /*return*/
                  ];
                });
              });
            }, // exec the chain after rendering to keep the behavior with beforeLoad
            function () {
              return __awaiter(_this, void 0, void 0, function () {
                return __generator(this, function (_a) {
                  return [2
                  /*return*/
                  , execHooksChain(toArray(beforeMount), app)];
                });
              });
            }, mountSandbox, function (props) {
              return __awaiter(_this, void 0, void 0, function () {
                return __generator(this, function (_a) {
                  return [2
                  /*return*/
                  , mount(__assign(__assign({}, props), {
                    container: containerGetter(),
                    setGlobalState: setGlobalState,
                    onGlobalStateChange: onGlobalStateChange
                  }))];
                });
              });
            }, // 應用 mount 完成後結束 loading
            function () {
              return __awaiter(_this, void 0, void 0, function () {
                return __generator(this, function (_a) {
                  return [2
                  /*return*/
                  , render({
                    element: element,
                    loading: false
                  })];
                });
              });
            }, function () {
              return __awaiter(_this, void 0, void 0, function () {
                return __generator(this, function (_a) {
                  return [2
                  /*return*/
                  , execHooksChain(toArray(afterMount), app)];
                });
              });
            }, // initialize the unmount defer after app mounted and resolve the defer after it unmounted
            function () {
              return __awaiter(_this, void 0, void 0, function () {
                return __generator(this, function (_a) {
                  switch (_a.label) {
                    case 0:
                      return [4
                      /*yield*/
                      , validateSingularMode(singular, app)];

                    case 1:
                      if (_a.sent()) {
                        prevAppUnmountedDeferred = new Deferred();
                      }

                      return [2
                      /*return*/
                      ];
                  }
                });
              });
            }],
            unmount: [function () {
              return __awaiter(_this, void 0, void 0, function () {
                return __generator(this, function (_a) {
                  return [2
                  /*return*/
                  , execHooksChain(toArray(beforeUnmount), app)];
                });
              });
            }, function (props) {
              return __awaiter(_this, void 0, void 0, function () {
                return __generator(this, function (_a) {
                  return [2
                  /*return*/
                  , unmount(__assign(__assign({}, props), {
                    container: containerGetter()
                  }))];
                });
              });
            }, unmountSandbox, function () {
              return __awaiter(_this, void 0, void 0, function () {
                return __generator(this, function (_a) {
                  return [2
                  /*return*/
                  , execHooksChain(toArray(afterUnmount), app)];
                });
              });
            }, function () {
              return __awaiter(_this, void 0, void 0, function () {
                return __generator(this, function (_a) {
                  render({
                    element: null,
                    loading: false
                  });
                  offGlobalStateChange(appInstanceId); // for gc

                  element = null;
                  return [2
                  /*return*/
                  ];
                });
              });
            }, function () {
              return __awaiter(_this, void 0, void 0, function () {
                return __generator(this, function (_a) {
                  switch (_a.label) {
                    case 0:
                      return [4
                      /*yield*/
                      , validateSingularMode(singular, app)];

                    case 1:
                      if (_a.sent() && prevAppUnmountedDeferred) {
                        prevAppUnmountedDeferred.resolve();
                      }

                      return [2
                      /*return*/
                      ];
                  }
                });
              });
            }]
          }];
      }
    });
  });
}
  • registerApplication是single-spa的方法,我們這裡通過loadApp這個方法,對資料進行處理
function registerApplication(appNameOrConfig, appOrLoadApp, activeWhen, customProps) {
  const registration = sanitizeArguments(appNameOrConfig, appOrLoadApp, activeWhen, customProps);
  if (getAppNames().indexOf(registration.name) !== -1) throw Error(formatErrorMessage(21,  `There is already an app registered with name ${registration.name}`, registration.name));
  apps.push(assign({
    loadErrorTime: null,
    status: NOT_LOADED,
    parcels: {},
    devtools: {
      overlays: {
        options: {},
        selectors: []
      }
    }
  }, registration));

  if (isInBrowser) {
    ensureJQuerySupport();
    reroute();
  }
}
  • 上面這個函式,應該是整個微前端框架最複雜的地方,它最終會返回一個函式,當成函式傳遞給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(子應用地址)
function importEntry(entry) {
  var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
  var _opts$fetch3 = opts.fetch,
      fetch = _opts$fetch3 === void 0 ? defaultFetch : _opts$fetch3,
      _opts$getTemplate = opts.getTemplate,
      getTemplate = _opts$getTemplate === void 0 ? defaultGetTemplate : _opts$getTemplate;
  var getPublicPath = opts.getPublicPath || opts.getDomain || _utils.defaultGetPublicPath;

  if (!entry) {
    throw new SyntaxError('entry should not be empty!');
  } // html entry


  if (typeof entry === 'string') {
    return importHTML(entry, {
      fetch: fetch,
      getPublicPath: getPublicPath,
      getTemplate: getTemplate
    });
  } // config entry


  if (Array.isArray(entry.scripts) || Array.isArray(entry.styles)) {
    var _entry$scripts = entry.scripts,
        scripts = _entry$scripts === void 0 ? [] : _entry$scripts,
        _entry$styles = entry.styles,
        styles = _entry$styles === void 0 ? [] : _entry$styles,
        _entry$html = entry.html,
        html = _entry$html === void 0 ? '' : _entry$html;
    return getEmbedHTML(getTemplate(html), styles, {
      fetch: fetch
    }).then(function (embedHTML) {
      return {
        template: embedHTML,
        assetPublicPath: getPublicPath('/'),
        getExternalScripts: function getExternalScripts() {
          return _getExternalScripts(scripts, fetch);
        },
        getExternalStyleSheets: function getExternalStyleSheets() {
          return _getExternalStyleSheets(styles, fetch);
        },
        execScripts: function execScripts(proxy, strictGlobal) {
          if (!scripts.length) {
            return Promise.resolve();
          }

          return _execScripts(scripts[scripts.length - 1], scripts, proxy, {
            fetch: fetch,
            strictGlobal: strictGlobal
          });
        }
      };
    });
  } else {
    throw new SyntaxError('entry scripts or styles should be array!');
  }
}
  • 上面程式碼裡最重要的,如果我們entry傳入字串,那麼就會使用這個函式去載入HTML內容(其實微前端的所有子應用載入,都是把dom節點載入渲染到基座的index.html檔案中的一個div標籤內)
  if (typeof entry === 'string') {
    return importHTML(entry, {
      fetch: fetch,
      getPublicPath: getPublicPath,
      getTemplate: getTemplate
    });
  } // config entry
  • importHTML這個函式,就是我們今晚最重要的一個點
  • 傳入url地址,發起fetch請求(此時由於域名或者埠不一樣,會出現跨域,所有子應用的熱更新開發模式下,webpack配置要做以下處理,部署也要考慮這個問題)
 devServer: {
        contentBase: path.resolve(__dirname, '../'),
        port: CONFIG.serverPort,
        host: 'rental-dev.mysoft.com.cn',
        historyApiFallback: true,
        disableHostCheck: true,
        headers: {
            'Access-Control-Allow-Origin': '*',
        },
        stats: {
            timings: true,
            assets: false,
            entrypoints: false,
            modules: false
        },
        proxy: {
            '/api/*': {
                target: CONFIG.proxyServer,
                changeOrigin: true,
                secure: false,
                pathRewrite: {
                    '^/api': ''
                },
                headers: {
                    // Cookie: 'RENTALCENTER=5fc10ee067fa5d15a9a7840bd4a75dc98dc7f47a'
                }
            }
        },
        before(app) {
            app.get('/cookie/set', (req, res) => {
                const cookies = req.query
                console.log('resolve cookie')
                for (const cookie in cookies) {
                    if (Object.prototype.hasOwnProperty.call(cookies, cookie)) {
                        res.cookie(cookie, cookies[cookie], {
                            httpOnly: true
                        })
                    }
                }
                res.redirect(
                    `${req.protocol}://${req.host}:${CONFIG.serverPort}${
                        CONFIG.baseAlias
                    }`
                )
            })
        },
        inline: true,
        hot: true // 新增
    },

整個importHTML函式好像很長很長,但是我們就看最重要的地方,一個框架(庫),流程線很長+版本迭代原因,需要相容老的版本,所以很多原始碼對於我們其實是無用的

function importHTML(url) {
  var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
  var fetch = defaultFetch;
  var getPublicPath = _utils.defaultGetPublicPath;
  var getTemplate = defaultGetTemplate; // compatible with the legacy importHTML api

  if (typeof opts === 'function') {
    fetch = opts;
  } else {
    fetch = opts.fetch || defaultFetch;
    getPublicPath = opts.getPublicPath || opts.getDomain || _utils.defaultGetPublicPath;
    getTemplate = opts.getTemplate || defaultGetTemplate;
  }

  return embedHTMLCache[url] || (embedHTMLCache[url] = fetch(url).then(function (response) {
    return response.text();
  }).then(function (html) {
    var assetPublicPath = getPublicPath(url);

    var _processTpl = (0, _processTpl2["default"])(getTemplate(html), assetPublicPath),
        template = _processTpl.template,
        scripts = _processTpl.scripts,
        entry = _processTpl.entry,
        styles = _processTpl.styles;

    return getEmbedHTML(template, styles, {
      fetch: fetch
    }).then(function (embedHTML) {
      return {
        template: embedHTML,
        assetPublicPath: assetPublicPath,
        getExternalScripts: function getExternalScripts() {
          return _getExternalScripts(scripts, fetch);
        },
        getExternalStyleSheets: function getExternalStyleSheets() {
          return _getExternalStyleSheets(styles, fetch);
        },
        execScripts: function execScripts(proxy, strictGlobal) {
          if (!scripts.length) {
            return Promise.resolve();
          }

          return _execScripts(entry, scripts, proxy, {
            fetch: fetch,
            strictGlobal: strictGlobal
          });
        }
      };
    });
  }));
}
  • 整個函式,最後返回了一個物件,這裡很明顯,通過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的字串了,而是一個物件,而且指令碼樣式都分離了
return {
    template: template,
    scripts: scripts,
    styles: styles,
    // set the last script as entry if have not set
    entry: entry || scripts[scripts.length - 1]
  };
  • 這個是框架幫我們處理的,必須要設定一個入口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);
}

整個微前端的觸發流程圖

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

最後

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

相關文章