8

微前端框架是怎么导入加载子应用的 【3000字精读】

 4 years ago
source link: https://segmentfault.com/a/1190000022424530
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

写在开头:

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

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

  • 下面代码,我指定的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
      }
    },]
  • 微前端到底是怎么回事呢? 我画了一张图

r2ei6vm.png!web

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

那么 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样式的内容)

F77j6nI.png!web

此时我们只要根据我们之前编写的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);
}

整个微前端的触发流程图

2A7jYjj.png!web

相信通过此文,你能真正了解微前端的使用原理, 后期我会出一个手写微前端框架的文章

最后

点个赞支持我吧,转发就更好了


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK