@hsui/micro-app
Version:
Hundsun micro-app framework
289 lines (278 loc) • 13.9 kB
JavaScript
import _regeneratorRuntime from "@babel/runtime/helpers/esm/regeneratorRuntime";
import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties";
import _asyncToGenerator from "@babel/runtime/helpers/esm/asyncToGenerator";
import _toConsumableArray from "@babel/runtime/helpers/esm/toConsumableArray";
var _excluded = ["mount"];
import { noop } from 'lodash';
import { getAppNames, getAppStatus, mountRootParcel, registerApplication, start as startSingleSpa } from 'single-spa';
import { createLogger } from '@hsui/logger';
import { loadApp } from './loader';
import { Deferred, getContainer, getXPathForElement, toArray } from './utils';
import { frameworkConfiguration } from './config';
import { doPrefetchStrategy, prefetchImmediately as prefetchApps } from './prefetch';
frameworkConfiguration.logger = createLogger();
frameworkConfiguration.logger.setLevel('info');
var microApps = [];
var started = false;
var defaultUrlRerouteOnly = true;
var frameworkStartedDefer = new Deferred();
export function registerMicroApps(apps, lifeCycles) {
// 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 = [].concat(_toConsumableArray(microApps), _toConsumableArray(unregisteredApps));
unregisteredApps.forEach(function (_app) {
var name = _app.name,
base = _app.base,
activeWhen = _app.activeWhen,
props = _app.props,
_app$loader = _app.loader,
loader = _app$loader === void 0 ? noop : _app$loader;
// 兼容 activeWhen 为 "/layout/#/foo, /layout/#/bar" 的场景
if (activeWhen && typeof activeWhen === 'string' && activeWhen.includes(',')) {
activeWhen = activeWhen.split(',');
}
registerApplication({
name: name,
app: function () {
var _app2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee3() {
var _yield$loadApp, mount, otherMicroAppConfigs;
return _regeneratorRuntime().wrap(function _callee3$(_context3) {
while (1) switch (_context3.prev = _context3.next) {
case 0:
loader(true);
_context3.next = 3;
return frameworkStartedDefer.promise;
case 3:
_context3.next = 5;
return loadApp(_app, frameworkConfiguration, lifeCycles);
case 5:
_context3.t0 = _context3.sent;
_yield$loadApp = (0, _context3.t0)();
mount = _yield$loadApp.mount;
otherMicroAppConfigs = _objectWithoutProperties(_yield$loadApp, _excluded);
return _context3.abrupt("return", _objectSpread({
mount: [/*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee() {
return _regeneratorRuntime().wrap(function _callee$(_context) {
while (1) switch (_context.prev = _context.next) {
case 0:
return _context.abrupt("return", loader(true));
case 1:
case "end":
return _context.stop();
}
}, _callee);
}))].concat(_toConsumableArray(toArray(mount)), [/*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee2() {
return _regeneratorRuntime().wrap(function _callee2$(_context2) {
while (1) switch (_context2.prev = _context2.next) {
case 0:
return _context2.abrupt("return", loader(false));
case 1:
case "end":
return _context2.stop();
}
}, _callee2);
}))])
}, otherMicroAppConfigs));
case 10:
case "end":
return _context3.stop();
}
}, _callee3);
}));
function app() {
return _app2.apply(this, arguments);
}
return app;
}(),
// 换成 base 可能更好理解一点儿,便于推广
// 但会牺牲一些 single-spa@5 里面一些好用的功能,比如多个 prefix
activeWhen: activeWhen || base,
customProps: props
});
});
}
// 手动加载微应用
var appConfigPromiseGetterMap = new Map();
export function loadMicroApp(app, lifeCycles) {
var props = app.props,
name = app.name;
frameworkConfiguration.logger.debug('手动加载微应用', app);
var getContainerXpath = function getContainerXpath(container) {
var containerElement = getContainer(container);
if (containerElement) {
return getXPathForElement(containerElement, document);
}
return undefined;
};
var wrapParcelConfigForRemount = function wrapParcelConfigForRemount(config) {
return _objectSpread(_objectSpread({}, config), {}, {
// empty bootstrap hook which should not run twice while it calling from cached micro app
bootstrap: function bootstrap() {
return Promise.resolve();
}
});
};
/**
* using name + container xpath as the micro app instance id,
* it means if you rendering a micro app to a dom which have been rendered before,
* the micro app would not load and evaluate its lifecycles again
*/
var memorizedLoadingFn = /*#__PURE__*/function () {
var _ref3 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee4() {
var userConfiguration, container, xpath, parcelConfigGetterPromise, parcelConfigObjectGetterPromise, _xpath;
return _regeneratorRuntime().wrap(function _callee4$(_context4) {
while (1) switch (_context4.prev = _context4.next) {
case 0:
userConfiguration = _objectSpread(_objectSpread({}, frameworkConfiguration), {}, {
singular: false
}); // const { $$cacheLifecycleByAppName } = userConfiguration;
container = 'container' in app ? app.container : undefined;
if (!container) {
_context4.next = 13;
break;
}
// using appName as cache for internal experimental scenario
// if ($$cacheLifecycleByAppName) {
// const parcelConfigGetterPromise = appConfigPromiseGetterMap.get(name);
// if (parcelConfigGetterPromise) return wrapParcelConfigForRemount((await parcelConfigGetterPromise)(container));
// }
xpath = getContainerXpath(container);
if (!xpath) {
_context4.next = 13;
break;
}
parcelConfigGetterPromise = appConfigPromiseGetterMap.get("".concat(name, "-").concat(xpath));
if (!parcelConfigGetterPromise) {
_context4.next = 13;
break;
}
_context4.t0 = wrapParcelConfigForRemount;
_context4.next = 10;
return parcelConfigGetterPromise;
case 10:
_context4.t1 = _context4.sent;
_context4.t2 = (0, _context4.t1)(container);
return _context4.abrupt("return", (0, _context4.t0)(_context4.t2));
case 13:
parcelConfigObjectGetterPromise = loadApp(app, userConfiguration, lifeCycles);
if (container) {
// if ($$cacheLifecycleByAppName) {
// appConfigPromiseGetterMap.set(name, parcelConfigObjectGetterPromise);
// } else {
_xpath = getContainerXpath(container);
if (_xpath) appConfigPromiseGetterMap.set("".concat(name, "-").concat(_xpath), parcelConfigObjectGetterPromise);
// }
}
_context4.next = 17;
return parcelConfigObjectGetterPromise;
case 17:
_context4.t3 = _context4.sent;
return _context4.abrupt("return", (0, _context4.t3)(container));
case 19:
case "end":
return _context4.stop();
}
}, _callee4);
}));
return function memorizedLoadingFn() {
return _ref3.apply(this, arguments);
};
}();
if (!started) {
var _frameworkConfigurati;
// We need to invoke start method of single-spa as the popstate event should be dispatched while the main app calling pushState/replaceState automatically,
// but in single-spa it will check the start status before it dispatch popstate
// see https://github.com/single-spa/single-spa/blob/f28b5963be1484583a072c8145ac0b5a28d91235/src/navigation/navigation-events.js#L101
// ref https://github.com/umijs/qiankun/pull/1071
startSingleSpa({
urlRerouteOnly: (_frameworkConfigurati = frameworkConfiguration.urlRerouteOnly) !== null && _frameworkConfigurati !== void 0 ? _frameworkConfigurati : defaultUrlRerouteOnly
});
}
return mountRootParcel(memorizedLoadingFn, _objectSpread({
domElement: document.createElement('div')
}, props));
}
/**
* @description 启动
* @property singular - 可选, 是否为单实例场景, 单实例指的是同一时间只会渲染一个微应用, 默认为 true
* @property strictGlobal - 可选, 是否走 with 闭包, 这会对性能造成影响, 不走的话要结合 CLI 的构建能力, 默认为 true
* @property prefetch - 可选, 执行预加载策略, 默认为 false
* @todo frameworkConfiguration
* 默认 strictGlobal 配置为 true, 基于 CLI 的构建能力允许关闭
* 可以作为 proxySandbox 沙盒性能问题的临时的解决方案
*/
export function start() {
var opts = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
for (var key in opts) {
if (Object.hasOwnProperty.call(opts, key)) {
frameworkConfiguration[key] = opts[key];
}
}
// 在 $$syncOnly 模式下,默认不走 with 闭包,兼容原有的解决方案
if (frameworkConfiguration.$$syncOnly) {
frameworkConfiguration.logger.warn('$$syncOnly will be removed in next major version');
frameworkConfiguration.strictGlobal = false;
}
var prefetch = frameworkConfiguration.prefetch,
singular = frameworkConfiguration.singular,
strictGlobal = frameworkConfiguration.strictGlobal,
_frameworkConfigurati2 = frameworkConfiguration.urlRerouteOnly,
urlRerouteOnly = _frameworkConfigurati2 === void 0 ? defaultUrlRerouteOnly : _frameworkConfigurati2;
if (prefetch) {
doPrefetchStrategy(microApps, prefetch);
}
if (!window.Proxy) {
frameworkConfiguration.logger.warn('Miss window.Proxy, proxySandbox will degenerate into snapshotSandbox');
if (!singular) {
frameworkConfiguration.logger.warn('Setting singular as false may cause unexpected behavior while your browser not support window.Proxy');
}
}
if (!frameworkConfiguration.$$syncOnly && !strictGlobal && process.env.NODE_ENV === 'development') {
frameworkConfiguration.logger.warn('Disabling with closure requires HUI CLI > 1.6.2 and HUI Bundler > 1.10.6');
}
startSingleSpa({
urlRerouteOnly: urlRerouteOnly
});
started = true;
frameworkConfiguration.logger.info('微前端框架初始化应用配置选项', microApps);
frameworkConfiguration.logger.info('当前注册的微应用集合', getAppNames());
frameworkStartedDefer.resolve();
}
// 部署 single-spa 事件监听
window.addEventListener('single-spa:app-change', function (evt) {
var _evt$detail, _evt$detail$appsByNew, _evt$detail2, _evt$detail2$appsByNe, _evt$detail3, _evt$detail3$appsByNe;
frameworkConfiguration.logger.debug('微前端框架执行应用调度');
frameworkConfiguration.logger.debug('当前处于激活状态的应用', (_evt$detail = evt.detail) === null || _evt$detail === void 0 ? void 0 : (_evt$detail$appsByNew = _evt$detail.appsByNewStatus) === null || _evt$detail$appsByNew === void 0 ? void 0 : _evt$detail$appsByNew.MOUNTED);
((_evt$detail2 = evt.detail) === null || _evt$detail2 === void 0 ? void 0 : (_evt$detail2$appsByNe = _evt$detail2.appsByNewStatus) === null || _evt$detail2$appsByNe === void 0 ? void 0 : _evt$detail2$appsByNe.LOAD_ERROR) && frameworkConfiguration.logger.debug('加载失败的应用', (_evt$detail3 = evt.detail) === null || _evt$detail3 === void 0 ? void 0 : (_evt$detail3$appsByNe = _evt$detail3.appsByNewStatus) === null || _evt$detail3$appsByNe === void 0 ? void 0 : _evt$detail3$appsByNe.LOAD_ERROR);
var statusMap = ['NOT_BOOTSTRAPPED', 'BOOTSTRAPPING', 'NOT_MOUNTED', 'MOUNTING', 'UNMOUNTING', 'UNLOADING'];
var LoadedApps = microApps.filter(function (app) {
var appStatus = getAppStatus(app.name);
return statusMap.indexOf(appStatus) !== -1;
}).map(function (item) {
return item.name;
});
frameworkConfiguration.logger.debug('加载完成的应用', LoadedApps);
});
// 手动预加载
export { prefetchApps };
// 全局的状态管理
export { initGlobalState } from './store';
// 工具方法 https://single-spa.js.org/docs/api
export { checkActivityFunctions, getAppNames, getAppStatus, getMountedApps } from 'single-spa';
// 错误处理 https://single-spa.js.org/docs/api
export { addErrorHandler, removeErrorHandler } from 'single-spa';
// 部署全局的未捕获异常处理
export function addGlobalUncaughtErrorHandler(errorHandler) {
window.addEventListener('error', errorHandler);
window.addEventListener('unhandledrejection', errorHandler);
}
// 移除全局的未捕获异常处理
export function removeGlobalUncaughtErrorHandler(errorHandler) {
window.removeEventListener('error', errorHandler);
window.removeEventListener('unhandledrejection', errorHandler);
}