preface

With the development of technology, the content carried by front-end applications is increasingly complex, and various problems arising from this have emerged, from MPA (multi-page Application) to SPA (single-page Application), While it solved the switching experience latency problem, it also introduced the Monolithic application problem caused by long first load times, and the explosive growth of the engineering. For MPA, its deployment is simple, applications are naturally hard isolated, and it has the characteristics of technology stack independence, independent development, and independent deployment. If you can combine these two characteristics, will it lead to a better user experience for users and developers? At this point, under the reference of micro service concept, the micro front end came into being.

An architectural style where independently deliverable frontend applications are composed into a greater whole. [Micro Frontends from martinfowler.com]

(martinfowler.com/articles/mi…).

According to martinfowler’s definition of a microfront end: Micro front end is a kind of multiple front-end application by an independent delivery of the overall architectural style, the front end and micro service is a kind of architectural style, so it is not a framework or library, but a style or a thought, so in order to realize the micro front-end solutions have a lot of kinds, the most common solution has the following kinds:

  1. Routing distribution

  1. iframe

  2. Application microservices

  1. Micro parts,

  1. The application of the

  1. Web Components

Related comparison:

plan Development costs Maintenance costs The feasibility of Same framework requirements To realize the difficulty The potential risk The ground practice
Routing distribution low low high no easy There is no HTTP server reverse proxy, such as nginx configuration location
iframe low low high no easy Seo unfriendly, cookie management, communication mechanism, popover problem, refresh back, security issues The front and back ends do not separate items commonly used
Application microservices high low In the no hard Sharing and isolation granularity is inconsistent Qiankun, ICestark, MOOA and similar single-SPA applications
Micro parts, high In the low is hard Implement the micro component management mechanism There is no
The application of the In the In the high is normal Multiple portfolio projects, each deployment escalation needs to be considered emp
Web Components high low high no normal New API, browser compatibility There is no

For the selection of micro front-end solutions, one or a combination of the above solutions should be selected from existing resources and historical accumulation, and from different dimensions (e.g. : Sharing ability, isolation mechanism, data scheme, routing authentication, etc.) to consider, to achieve smooth migration of the project, so as to achieve the iterative upgrading of the architecture of the gradual reconstruction, avoid by all means for the sake of the architecture and architecture, do not meaningless show off, any technology is appropriate is the best, big qiao does not work, esabre no edge!

Scheme comparison

This paper focuses on the analysis of the application of micro-service and micro-application of several landing schemes, to do a simple exploration of its implementation ideas

  • single-spa
  • qiankun
  • icestark
  • emp
  • piral

The source code parsing

Single – spa source

The whole idea of single-SPA is to use lifecycle hook functions to load applications of hijacked routes, with the core files apps and Reroute

apps.js

export function getAppChanges() { const appsToUnload = [], appsToUnmount = [], appsToLoad = [], appsToMount = []; const currentTime = new Date().getTime(); apps.forEach((app) => { const appShouldBeActive = app.status ! == SKIP_BECAUSE_BROKEN && shouldBeActive(app); switch (app.status) { case LOAD_ERROR: if (appShouldBeActive && currentTime - app.loadErrorTime >= 200) { appsToLoad.push(app); } break; case NOT_LOADED: case LOADING_SOURCE_CODE: if (appShouldBeActive) { appsToLoad.push(app); } break; case NOT_BOOTSTRAPPED: case NOT_MOUNTED: if (! appShouldBeActive && getAppUnloadInfo(toName(app))) { appsToUnload.push(app); } else if (appShouldBeActive) { appsToMount.push(app); } break; case MOUNTED: if (! appShouldBeActive) { appsToUnmount.push(app); } break; }}); return { appsToUnload, appsToUnmount, appsToLoad, appsToMount }; } export function getMountedApps() { return apps.filter(isActive).map(toName); } export function registerApplication( appNameOrConfig, appOrLoadApp, activeWhen, customProps ) { const registration = sanitizeArguments( appNameOrConfig, appOrLoadApp, activeWhen, customProps ); if (getAppNames().indexOf(registration.name) ! == -1) throw Error( formatErrorMessage( 21, __DEV__ && `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(); } } export function unregisterApplication(appName) { if (apps.filter((app) => toName(app) === appName).length === 0) { throw Error( formatErrorMessage( 25, __DEV__ && `Cannot unregister application '${appName}' because no such application has been registered`, appName ) ); } return unloadApplication(appName).then(() => { const appIndex = apps.map(toName).indexOf(appName); apps.splice(appIndex, 1); }); } export function unloadApplication(appName, opts = { waitForUnmount: false }) { if (typeof appName ! == "string") { throw Error( formatErrorMessage( 26, __DEV__ && `unloadApplication requires a string 'appName'` ) ); } const app = find(apps, (App) => toName(App) === appName); if (! app) { throw Error( formatErrorMessage( 27, __DEV__ && `Could not unload application '${appName}' because no such application has been registered`, appName ) ); } const appUnloadInfo = getAppUnloadInfo(toName(app)); if (opts && opts.waitForUnmount) { if (appUnloadInfo) { return appUnloadInfo.promise; } else { const promise = new Promise((resolve, reject) => { addAppToUnload(app, () => promise, resolve, reject); }); return promise; } } else { let resultPromise; if (appUnloadInfo) { resultPromise = appUnloadInfo.promise; immediatelyUnloadApp(app, appUnloadInfo.resolve, appUnloadInfo.reject); } else { resultPromise = new Promise((resolve, reject) => { addAppToUnload(app, () => resultPromise, resolve, reject); immediatelyUnloadApp(app, resolve, reject); }); } return resultPromise; }}Copy the code

reroute.js

export function reroute(pendingPromises = [], eventArguments) { if (appChangeUnderway) { return new Promise((resolve, reject) => { peopleWaitingOnAppChange.push({ resolve, reject, eventArguments, }); }); } const { appsToUnload, appsToUnmount, appsToLoad, appsToMount, } = getAppChanges(); let appsThatChanged, navigationIsCanceled = false, oldUrl = currentUrl, newUrl = (currentUrl = window.location.href); if (isStarted()) { appChangeUnderway = true; appsThatChanged = appsToUnload.concat( appsToLoad, appsToUnmount, appsToMount ); return performAppChanges(); } else { appsThatChanged = appsToLoad; return loadApps(); } function cancelNavigation() { navigationIsCanceled = true; } function loadApps() { return Promise.resolve().then(() => { const loadPromises = appsToLoad.map(toLoadPromise); return ( Promise.all(loadPromises) .then(callAllEventListeners) .then(() => []) .catch((err) => { callAllEventListeners(); throw err; })); }); } function performAppChanges() { return Promise.resolve().then(() => { window.dispatchEvent( new CustomEvent( appsThatChanged.length === 0 ? "single-spa:before-no-app-change" : "single-spa:before-app-change", getCustomEventDetail(true) ) ); window.dispatchEvent( new CustomEvent( "single-spa:before-routing-event", getCustomEventDetail(true, { cancelNavigation }) ) ); if (navigationIsCanceled) { window.dispatchEvent( new CustomEvent( "single-spa:before-mount-routing-event", getCustomEventDetail(true) ) ); finishUpAndReturn(); navigateToUrl(oldUrl); return; } const unloadPromises = appsToUnload.map(toUnloadPromise); const unmountUnloadPromises = appsToUnmount .map(toUnmountPromise) .map((unmountPromise) => unmountPromise.then(toUnloadPromise)); const allUnmountPromises = unmountUnloadPromises.concat(unloadPromises); const unmountAllPromise = Promise.all(allUnmountPromises); unmountAllPromise.then(() => { window.dispatchEvent( new CustomEvent( "single-spa:before-mount-routing-event", getCustomEventDetail(true) ) ); }); const loadThenMountPromises = appsToLoad.map((app) => { return toLoadPromise(app).then((app) => tryToBootstrapAndMount(app, unmountAllPromise) ); }); const mountPromises = appsToMount .filter((appToMount) => appsToLoad.indexOf(appToMount) < 0) .map((appToMount) => { return tryToBootstrapAndMount(appToMount, unmountAllPromise); }); return unmountAllPromise .catch((err) => { callAllEventListeners(); throw err; }) .then(() => { callAllEventListeners(); return Promise.all(loadThenMountPromises.concat(mountPromises)) .catch((err) => { pendingPromises.forEach((promise) => promise.reject(err)); throw err; }) .then(finishUpAndReturn); }); }); } function finishUpAndReturn() { const returnValue = getMountedApps(); pendingPromises.forEach((promise) => promise.resolve(returnValue)); try { const appChangeEventName = appsThatChanged.length === 0 ? "single-spa:no-app-change" : "single-spa:app-change"; window.dispatchEvent( new CustomEvent(appChangeEventName, getCustomEventDetail()) ); window.dispatchEvent( new CustomEvent("single-spa:routing-event", getCustomEventDetail()) ); } catch (err) { setTimeout(() => { throw err; }); } appChangeUnderway = false; if (peopleWaitingOnAppChange.length > 0) { const nextPendingPromises = peopleWaitingOnAppChange; peopleWaitingOnAppChange = []; reroute(nextPendingPromises); } return returnValue; } function callAllEventListeners() { pendingPromises.forEach((pendingPromise) => { callCapturedEventListeners(pendingPromise.eventArguments); }); callCapturedEventListeners(eventArguments); } function getCustomEventDetail(isBeforeChanges = false, extraProperties) { const newAppStatuses = {}; const appsByNewStatus = { [MOUNTED]: [], [NOT_MOUNTED]: [], [NOT_LOADED]: [], [SKIP_BECAUSE_BROKEN]: [], }; if (isBeforeChanges) { appsToLoad.concat(appsToMount).forEach((app, index) => { addApp(app, MOUNTED); }); appsToUnload.forEach((app) => { addApp(app, NOT_LOADED); }); appsToUnmount.forEach((app) => { addApp(app, NOT_MOUNTED); }); } else { appsThatChanged.forEach((app) => { addApp(app); }); } const result = { detail: { newAppStatuses, appsByNewStatus, totalAppChanges: appsThatChanged.length, originalEvent: eventArguments? .[0], oldUrl, newUrl, navigationIsCanceled, }, }; if (extraProperties) { assign(result.detail, extraProperties); } return result; function addApp(app, status) { const appName = toName(app); status = status || getAppStatus(appName); newAppStatuses[appName] = status; const statusArr = (appsByNewStatus[status] = appsByNewStatus[status] || []); statusArr.push(appName); }}}Copy the code

Qiankun source

Qiankun is a framework that encapsulates isolation and sharing mechanisms based on single-SPA, simplifying the related life cycle of single-SPA, and providing sandbox isolation and sharing mechanisms

sandbox

export type SandBox = {
  /** 沙箱的名字 */
  name: string;
  /** 沙箱的类型 */
  type: SandBoxType;
  /** 沙箱导出的代理实体 */
  proxy: WindowProxy;
  /** 沙箱是否在运行中 */
  sandboxRunning: boolean;
  /** latest set property */
  latestSetProp?: PropertyKey | null;
  /** 启动沙箱 */
  active: () => void;
  /** 关闭沙箱 */
  inactive: () => void;
};

// Proxy沙箱
export default class ProxySandbox implements SandBox {
  /** window 值变更记录 */
  private updatedValueSet = new Set<PropertyKey>();

  name: string;

  type: SandBoxType;

  proxy: WindowProxy;

  sandboxRunning = true;

  latestSetProp: PropertyKey | null = null;

  active() {
    if (!this.sandboxRunning) activeSandboxCount++;
    this.sandboxRunning = true;
  }

  inactive() {
    if (process.env.NODE_ENV === 'development') {
      console.info(`[qiankun:sandbox] ${this.name} modified global properties restore...`, [
        ...this.updatedValueSet.keys(),
      ]);
    }

    if (--activeSandboxCount === 0) {
      variableWhiteList.forEach((p) => {
        if (this.proxy.hasOwnProperty(p)) {
          // @ts-ignore
          delete window[p];
        }
      });
    }

    this.sandboxRunning = false;
  }

  constructor(name: string) {
    this.name = name;
    this.type = SandBoxType.Proxy;
    const { updatedValueSet } = this;

    const rawWindow = window;
    const { fakeWindow, propertiesWithGetter } = createFakeWindow(rawWindow);

    const descriptorTargetMap = new Map<PropertyKey, SymbolTarget>();
    const hasOwnProperty = (key: PropertyKey) => fakeWindow.hasOwnProperty(key) || rawWindow.hasOwnProperty(key);

    const proxy = new Proxy(fakeWindow, {
      set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {
        if (this.sandboxRunning) {
          // We must kept its description while the property existed in rawWindow before
          if (!target.hasOwnProperty(p) && rawWindow.hasOwnProperty(p)) {
            const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p);
            const { writable, configurable, enumerable } = descriptor!;
            if (writable) {
              Object.defineProperty(target, p, {
                configurable,
                enumerable,
                writable,
                value,
              });
            }
          } else {
            // @ts-ignore
            target[p] = value;
          }

          if (variableWhiteList.indexOf(p) !== -1) {
            // @ts-ignore
            rawWindow[p] = value;
          }

          updatedValueSet.add(p);

          this.latestSetProp = p;

          return true;
        }

        if (process.env.NODE_ENV === 'development') {
          console.warn(`[qiankun] Set window.${p.toString()} while sandbox destroyed or inactive in ${name}!`);
        }

        // 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的情况下应该忽略错误
        return true;
      },

      get(target: FakeWindow, p: PropertyKey): any {
        if (p === Symbol.unscopables) return unscopables;

        // avoid who using window.window or window.self to escape the sandbox environment to touch the really window
        // see https://github.com/eligrey/FileSaver.js/blob/master/src/FileSaver.js#L13
        if (p === 'window' || p === 'self') {
          return proxy;
        }

        if (
          p === 'top' ||
          p === 'parent' ||
          (process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop'))
        ) {
          // if your master app in an iframe context, allow these props escape the sandbox
          if (rawWindow === rawWindow.parent) {
            return proxy;
          }
          return (rawWindow as any)[p];
        }

        // proxy.hasOwnProperty would invoke getter firstly, then its value represented as rawWindow.hasOwnProperty
        if (p === 'hasOwnProperty') {
          return hasOwnProperty;
        }

        // mark the symbol to document while accessing as document.createElement could know is invoked by which sandbox for dynamic append patcher
        if (p === 'document' || p === 'eval') {
          setCurrentRunningSandboxProxy(proxy);
          // FIXME if you have any other good ideas
          // remove the mark in next tick, thus we can identify whether it in micro app or not
          // this approach is just a workaround, it could not cover all complex cases, such as the micro app runs in the same task context with master in some case
          nextTick(() => setCurrentRunningSandboxProxy(null));
          switch (p) {
            case 'document':
              return document;
            case 'eval':
              // eslint-disable-next-line no-eval
              return eval;
            // no default
          }
        }

        // eslint-disable-next-line no-nested-ternary
        const value = propertiesWithGetter.has(p)
          ? (rawWindow as any)[p]
          : p in target
          ? (target as any)[p]
          : (rawWindow as any)[p];
        return getTargetValue(rawWindow, value);
      },

      // trap in operator
      // see https://github.com/styled-components/styled-components/blob/master/packages/styled-components/src/constants.js#L12
      has(target: FakeWindow, p: string | number | symbol): boolean {
        return p in unscopables || p in target || p in rawWindow;
      },

      getOwnPropertyDescriptor(target: FakeWindow, p: string | number | symbol): PropertyDescriptor | undefined {
        /*
         as the descriptor of top/self/window/mockTop in raw window are configurable but not in proxy target, we need to get it from target to avoid TypeError
         see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/getOwnPropertyDescriptor
         > A property cannot be reported as non-configurable, if it does not exists as an own property of the target object or if it exists as a configurable own property of the target object.
         */
        if (target.hasOwnProperty(p)) {
          const descriptor = Object.getOwnPropertyDescriptor(target, p);
          descriptorTargetMap.set(p, 'target');
          return descriptor;
        }

        if (rawWindow.hasOwnProperty(p)) {
          const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p);
          descriptorTargetMap.set(p, 'rawWindow');
          // A property cannot be reported as non-configurable, if it does not exists as an own property of the target object
          if (descriptor && !descriptor.configurable) {
            descriptor.configurable = true;
          }
          return descriptor;
        }

        return undefined;
      },

      // trap to support iterator with sandbox
      ownKeys(target: FakeWindow): PropertyKey[] {
        const keys = uniq(Reflect.ownKeys(rawWindow).concat(Reflect.ownKeys(target)));
        return keys;
      },

      defineProperty(target: Window, p: PropertyKey, attributes: PropertyDescriptor): boolean {
        const from = descriptorTargetMap.get(p);
        /*
         Descriptor must be defined to native window while it comes from native window via Object.getOwnPropertyDescriptor(window, p),
         otherwise it would cause a TypeError with illegal invocation.
         */
        switch (from) {
          case 'rawWindow':
            return Reflect.defineProperty(rawWindow, p, attributes);
          default:
            return Reflect.defineProperty(target, p, attributes);
        }
      },

      deleteProperty(target: FakeWindow, p: string | number | symbol): boolean {
        if (target.hasOwnProperty(p)) {
          // @ts-ignore
          delete target[p];
          updatedValueSet.delete(p);

          return true;
        }

        return true;
      },
    });

    this.proxy = proxy;

    activeSandboxCount++;
  }
}

// 快照snapshot沙箱
export default class SnapshotSandbox implements SandBox {
  proxy: WindowProxy;

  name: string;

  type: SandBoxType;

  sandboxRunning = true;

  private windowSnapshot!: Window;

  private modifyPropsMap: Record<any, any> = {};

  constructor(name: string) {
    this.name = name;
    this.proxy = window;
    this.type = SandBoxType.Snapshot;
  }

  active() {
    // 记录当前快照
    this.windowSnapshot = {} as Window;
    iter(window, (prop) => {
      this.windowSnapshot[prop] = window[prop];
    });

    // 恢复之前的变更
    Object.keys(this.modifyPropsMap).forEach((p: any) => {
      window[p] = this.modifyPropsMap[p];
    });

    this.sandboxRunning = true;
  }

  inactive() {
    this.modifyPropsMap = {};

    iter(window, (prop) => {
      if (window[prop] !== this.windowSnapshot[prop]) {
        // 记录变更,恢复环境
        this.modifyPropsMap[prop] = window[prop];
        window[prop] = this.windowSnapshot[prop];
      }
    });

    if (process.env.NODE_ENV === 'development') {
      console.info(`[qiankun:sandbox] ${this.name} origin window restore...`, Object.keys(this.modifyPropsMap));
    }

    this.sandboxRunning = false;
  }
}
Copy the code

globalState.js

Function emitGlobal(state: Record<string, any>, prevState: Record<string, any>) { Object.keys(deps).forEach((id: string) => { if (deps[id] instanceof Function) { deps[id](cloneDeep(state), cloneDeep(prevState)); }}); } export function initGlobalState(state: Record<string, any> = {}) {if (state === globalState) {console.warn('[Qiankun] state has not changed! '); } else { const prevGlobalState = cloneDeep(globalState); globalState = cloneDeep(state); emitGlobal(globalState, prevGlobalState); } return getMicroAppStateActions(`global-${+new Date()}`, true); } export function getMicroAppStateActions(id: string, isMaster? : boolean): MicroAppStateActions {return {/** * onGlobalStateChange Global dependency listener ** Dependencies that need to be triggered to collect setState ** Restrictions: Each child application has only one active global listener. The new listener overwrites the old listener. If only part of the listener is listened on, use onGlobalStateChange * * this is designed to reduce memory explosion caused by global listener abuse. callback * } * * @param callback * @param fireImmediately */ onGlobalStateChange(callback: OnGlobalStateChangeCallback, fireImmediately? : boolean) { if (! (callback instanceof Function)) { console.error('[qiankun] callback must be function! '); return; } if (deps[id]) { console.warn(`[qiankun] '${id}' global listener already exists before this, new listener will overwrite it.`); } deps[id] = callback; const cloneState = cloneDeep(globalState); if (fireImmediately) { callback(cloneState, cloneState); }}, /** * setGlobalState updates store data ** 1. Validates the layer 1 properties of the input state. Only the bucket properties declared at initialization will be changed * 2. * * @param state */ setGlobalState(state: Record<string, any> = {}) {if (state === globalState) {console.warn('[Qiankun] state has not changed! '); return false; } const changeKeys: string[] = []; const prevGlobalState = cloneDeep(globalState); globalState = cloneDeep( Object.keys(state).reduce((_globalState, changeKey) => { if (isMaster || _globalState.hasOwnProperty(changeKey)) { changeKeys.push(changeKey); return Object.assign(_globalState, { [changeKey]: state[changeKey] }); } console.warn(' [Qiankun] '${changeKey}' not declared when init state! `); return _globalState; }, globalState), ); If (changekeys. length === 0) {console.warn('[Qiankun] state has not changed! '); return false; } emitGlobal(globalState, prevGlobalState); return true; }, // Cancel dependencies offGlobalStateChange() {delete deps[id]; return true; }}; }Copy the code

Icestark source

Ice is a front-end framework for the whole process of Amoy team, which includes scaffolding, component library, VScode plug-in, Lowcode generation and other related ecology, among which ICestark is the application of related micro-front-end. Ice-related architecture is not extended here. To put it simply, its essence is a powerful ecosystem formed by the form of microkernel and various plug-in markets. This paper focuses on the micro front end, so only the related content of the micro front end is discussed

apps.ts

export function registerMicroApp(appConfig: AppConfig, appLifecyle?: AppLifecylceOptions) {
  // check appConfig.name
  if (getAppNames().includes(appConfig.name)) {
    throw Error(`name ${appConfig.name} already been regsitered`);
  }
  // set activeRules
  const { activePath, hashType = false, exact = false, sensitive = false, strict = false } = appConfig;
  const activeRules: (ActiveFn | string | MatchOptions)[] = Array.isArray(activePath) ? activePath : [activePath];
  const checkActive = activePath
    ? (url: string) => activeRules.map((activeRule: ActiveFn | string | MatchOptions) => {
      if (typeof activeRule === 'function' ) {
        return activeRule;
      } else {
        const pathOptions: MatchOptions = { hashType, exact, sensitive, strict };
        const pathInfo = Object.prototype.toString.call(activeRule) === '[object Object]'
          ? { ...pathOptions, ...(activeRule as MatchOptions) }
          : { path: activeRule as string, ...pathOptions };
        return (checkUrl: string) => matchActivePath(checkUrl, pathInfo);
      }
    }).some((activeRule: ActiveFn) => activeRule(url))
    // active app when activePath is not specified
    : () => true;
  const microApp = {
    status: NOT_LOADED,
    ...appConfig,
    appLifecycle: appLifecyle,
    checkActive,
  };
  microApps.push(microApp);
}

export function registerMicroApps(appConfigs: AppConfig[], appLifecyle?: AppLifecylceOptions) {
  appConfigs.forEach(appConfig => {
    registerMicroApp(appConfig, appLifecyle);
  });
}

// 可以加载module粒度的应用
export async function loadAppModule(appConfig: AppConfig) {
  const { onLoadingApp, onFinishLoading, fetch } = getAppConfig(appConfig.name)?.configuration || globalConfiguration;

  let lifecycle: ModuleLifeCycle = {};
  onLoadingApp(appConfig);
  const appSandbox = createSandbox(appConfig.sandbox);
  const { url, container, entry, entryContent, name } = appConfig;
  const appAssets = url ? getUrlAssets(url) : await getEntryAssets({
    root: container,
    entry,
    href: location.href,
    entryContent,
    assetsCacheKey: name,
    fetch,
  });
  updateAppConfig(appConfig.name, { appAssets, appSandbox });

  cacheLoadMode(appConfig);

  if (appConfig.umd) {
    await loadAndAppendCssAssets(appAssets);
    lifecycle = await loadUmdModule(appAssets.jsList, appSandbox);
  } else {
    await appendAssets(appAssets, appSandbox, fetch);
    lifecycle = {
      mount: getCache(AppLifeCycleEnum.AppEnter),
      unmount: getCache(AppLifeCycleEnum.AppLeave),
    };
    setCache(AppLifeCycleEnum.AppEnter, null);
    setCache(AppLifeCycleEnum.AppLeave, null);
  }
  onFinishLoading(appConfig);
  return combineLifecyle(lifecycle, appConfig);
}

function capitalize(str: string) {
  if (typeof str !== 'string') return '';
  return `${str.charAt(0).toUpperCase()}${str.slice(1)}`;
}

async function callAppLifecycle(primaryKey: string, lifecycleKey: string, appConfig: AppConfig) {
  if (appConfig.appLifecycle && appConfig.appLifecycle[`${primaryKey}${capitalize(lifecycleKey)}`]) {
    await appConfig.appLifecycle[`${primaryKey}${capitalize(lifecycleKey)}`](appConfig);
  }
}

function combineLifecyle(lifecycle: ModuleLifeCycle, appConfig: AppConfig) {
  const combinedLifecyle = { ...lifecycle };
  ['mount', 'unmount', 'update'].forEach((lifecycleKey) => {
    if (lifecycle[lifecycleKey]) {
      combinedLifecyle[lifecycleKey] = async (props) => {
        await callAppLifecycle('before', lifecycleKey, appConfig);
        await lifecycle[lifecycleKey](props);
        await callAppLifecycle('after', lifecycleKey, appConfig);
      };
    }
  });
  return combinedLifecyle;
}

export async function mountMicroApp(appName: string) {
  const appConfig = getAppConfig(appName);
  // check current url before mount
  if (appConfig && appConfig.checkActive(window.location.href) && appConfig.status !== MOUNTED) {
    updateAppConfig(appName, { status: MOUNTED });
    if (appConfig.mount) {
      await appConfig.mount({ container: appConfig.container, customProps: appConfig.props });
    }
  }
}

export async function unmountMicroApp(appName: string) {
  const appConfig = getAppConfig(appName);
  if (appConfig && (appConfig.status === MOUNTED || appConfig.status === LOADING_ASSETS || appConfig.status === NOT_MOUNTED)) {
    // remove assets if app is not cached
    const { shouldAssetsRemove } = getAppConfig(appName)?.configuration  || globalConfiguration;
    emptyAssets(shouldAssetsRemove, !appConfig.cached && appConfig.name);
    updateAppConfig(appName, { status: UNMOUNTED });
    if (!appConfig.cached && appConfig.appSandbox) {
      appConfig.appSandbox.clear();
      appConfig.appSandbox = null;
    }
    if (appConfig.unmount) {
      await appConfig.unmount({ container: appConfig.container, customProps: appConfig.props });
    }
  }
}

// unload micro app, load app bundles when create micro app
export async function unloadMicroApp(appName: string) {
  const appConfig = getAppConfig(appName);
  if (appConfig) {
    unmountMicroApp(appName);
    delete appConfig.mount;
    delete appConfig.unmount;
    delete appConfig.appAssets;
    updateAppConfig(appName, { status: NOT_LOADED });
  } else {
    console.log(`[icestark] can not find app ${appName} when call unloadMicroApp`);
  }
}

// remove app config from cache
export function removeMicroApp(appName: string) {
  const appIndex = getAppNames().indexOf(appName);
  if (appIndex > -1) {
    // unload micro app in case of app is mounted
    unloadMicroApp(appName);
    microApps.splice(appIndex, 1);
  } else {
    console.log(`[icestark] can not find app ${appName} when call removeMicroApp`);
  }
}

export function removeMicroApps(appNames: string[]) {
  appNames.forEach((appName) => {
    removeMicroApp(appName);
  });
}

// clear all micro app configs
export function clearMicroApps () {
  getAppNames().forEach(name => {
    unloadMicroApp(name);
  });
  microApps = [];
}
Copy the code

start.ts

export function reroute (url: string, type: RouteType | 'init' | 'popstate'| 'hashchange' ) { const { pathname, query, hash } = urlParse(url, true); // trigger onRouteChange when url is changed if (lastUrl ! == url) { globalConfiguration.onRouteChange(url, pathname, query, hash, type); const unmountApps = []; const activeApps = []; getMicroApps().forEach((microApp: AppConfig) => { const shouldBeActive = microApp.checkActive(url); if (shouldBeActive) { activeApps.push(microApp); } else { unmountApps.push(microApp); }}); // trigger onActiveApps when url is changed globalConfiguration.onActiveApps(activeApps); // call captured event after app mounted Promise.all( // call unmount apps unmountApps.map(async (unmountApp) => { if (unmountApp.status === MOUNTED || unmountApp.status === LOADING_ASSETS) { globalConfiguration.onAppLeave(unmountApp); } await unmountMicroApp(unmountApp.name); }).concat(activeApps.map(async (activeApp) => { if (activeApp.status ! == MOUNTED) { globalConfiguration.onAppEnter(activeApp); } await createMicroApp(activeApp); })) ).then(() => { callCapturedEventListeners(); }); } lastUrl = url; }; function start(options? : StartConfiguration) { if (started) { console.log('icestark has been already started'); return; } started = true; recordAssets(); // update globalConfiguration Object.keys(options || {}).forEach((configKey) => { globalConfiguration[configKey] = options[configKey]; }); hijackHistory(); hijackEventListener(); // trigger init router globalConfiguration.reroute(location.href, 'init'); } function unload() { unHijackEventListener(); unHijackHistory(); started = false; // remove all assets added by micro apps emptyAssets(globalConfiguration.shouldAssetsRemove, true); clearMicroApps(); }Copy the code

Emp source

The implementation mode of EMP is completely different from single-SPa-like schemes. It utilizes the module federation mechanism of Webpack5 to realize the shared call between modules. The bigs of YY realize the function of service mesh similar to microservice based on the shared transmission of XXX.D. TS of TS. Emp provides complete scaffolding functionality

emp-cli

const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = (env, config, {analyze, empEnv, ts, progress, createName, createPath, hot}) => {
  const isDev = env === 'development'
  const conf = {
    plugin: {
      mf: {
        plugin: ModuleFederationPlugin,
        args: [{}],
      },
    },
  }
  if (ts) {
    createName = createName || 'index.d.ts'
    createPath = createPath ? resolveApp(createPath) : resolveApp('dist')
    conf.plugin.tunedts = {
      plugin: TuneDtsPlugin,
      args: [
        {
          output: path.join(createPath, createName),
          path: createPath,
          name: createName,
          isDefault: true,
        },
      ],
    }
  }
  config.merge(conf)
}
Copy the code

emp-tune-dts-plugin

function tuneType(createPath, createName, isDefault, Operation = emptyFunc) {// Retrieve d.ts file const filePath = '${createPath}/${createName}' const fileData = fs.readFileSync(filePath, {encoding: 'utf-8'}) let newFileData = '' newFileData = fileData isDefault && (newFileData = defaultRepalce(fileData)) // } newFileData && (newFileData = operation(newFileData) ? Operation (newFileData) : newFileData) // Overwrite the original index.d.stfs. WriteFileSync (filePath, newFileData, {encoding: 'utf-8'}) } class TuneDtsPlugin { constructor(options) { this.options = options || {} } apply(compiler) { const _options  = this.options console.log('------------TuneDtsPlugin Working----------') if (compiler.options.output.path) { _options.path = compiler.options.output.path _options.output = `${compiler.options.output.path}/${_options.name}` } compiler.hooks.afterEmit.tap(plugin, function () { setTimeout(function () { generateType(_options) }) }) } }Copy the code

Piral source

Piral is a micro front-end framework based on React. It defines two concepts: One is Piral, which is an application shell for an application. It hosts various applications, including components shared by Pilets that define when these applications are loaded and integrated. The other is Pilet, which is a special feature module that hosts different applications and contains independent resources, which determine the loading time of components. Piral isolates resources by adding a layer of Pilets, and shares data without this layer

piral

// render.tsx export function renderInstance(options? : PiralRenderOptions) { return runInstance((app, selector) => render(app, getContainer(selector)), options); } // run.tsx export function runInstance(runner: PiralRunner, options: PiralRenderOptions = {}) { const { selector = '#app', settings, layout, errors, middleware = noChange, ... config } = options; const { app, instance } = getAppInstance(middleware(config), { settings, layout, errors }); runner(app, selector); return instance; }Copy the code

piral-base

// dependency.ts export function compileDependency( name: string, content: string, link = '', dependencies? : AvailableDependencies, ): Promise<PiletApp> { const app = evalDependency(name, content, link, dependencies); return checkPiletAppAsync(name, app); } // fetch.ts export function defaultFetchDependency(url: string) { return fetch(url, { method: 'GET', cache: 'force-cache', }).then((m) => m.text()); }Copy the code

piral-core

// actions export function initialize(ctx: GlobalStateContext, loading: boolean, error: Error | undefined, modules: Array<Pilet>) { ctx.dispatch((state) => ({ ... state, app: { ... state.app, error, loading, }, modules, })); } export function injectPilet(ctx: GlobalStateContext, pilet: Pilet) { ctx.dispatch((state) => ({ ... state, modules: replaceOrAddItem(state.modules, pilet, (m) => m.name === pilet.name), registry: removeNested<RegistryState, BaseRegistration>(state.registry, (m) => m.pilet === pilet.name), })); ctx.emit('unload-pilet', { name: pilet.name, }); } export function setComponent<TKey extends keyof ComponentsState>( ctx: GlobalStateContext, name: TKey, component: ComponentsState[TKey], ) { ctx.dispatch((state) => ({ ... state, components: withKey(state.components, name, component), })); } export function setErrorComponent<TKey extends keyof ErrorComponentsState>( ctx: GlobalStateContext, type: TKey, component: ErrorComponentsState[TKey], ) { ctx.dispatch((state) => ({ ... state, errorComponents: withKey(state.errorComponents, type, component), })); } export function setRoute<T = {}>( ctx: GlobalStateContext, path: string, component: ComponentType<RouteComponentProps<T>>, ) { ctx.dispatch((state) => ({ ... state, routes: withKey(state.routes, path, component), })); } export function includeProvider(ctx: GlobalStateContext, provider: JSX.Element) { const wrapper: React.FC = (props) => cloneElement(provider, props); ctx.dispatch((state) => ({ ... state, provider: ! state.provider ? wrapper : (props) => createElement(state.provider, undefined, wrapper(props)), })); } // createGlobalState.ts export function createGlobalState(customState: NestedPartial<GlobalState> = {}) { const defaultState: GlobalState = { app: { error: undefined, loading: typeof window ! == 'undefined', layout: 'desktop', }, components: { ErrorInfo: DefaultErrorInfo, LoadingIndicator: DefaultLoadingIndicator, Router: BrowserRouter, Layout: DefaultLayout, }, errorComponents: {}, registry: { extensions: {}, pages: {}, wrappers: {}, }, routes: {}, data: {}, portals: {}, modules: [], }; return Atom.of(extend(defaultState, customState)); }Copy the code

conclusion

There are many practical solutions for micro-front-end implementation. If you want to know more about the framework, you can read this article about 11 micro-front-end frameworks that are very popular in 2020. The essence of the micro front end lies in the isolation and sharing of resources. The granularity here can be applications, modules, or self-defined abstraction layers, which are all for better “high cohesion, low coupling”. As the saying goes, “There is no silver bullet in software engineering”, there is no general formula and general solution that can solve all problems at once. Only by combining the specific business and choosing appropriate technical solutions can we maximize the role of the architecture.

reference

  • [Issue 1917] Practice of micro front end in Xiaomi CRM system
  • Best Practices in Microfront-end based on Qiankun (Swastika) – from 0 to 1
  • [PPT] @Zhang Kejun: Micro front-end architecture system
  • [Issue 1728] Daily Youxian Supply Chain front end team micro front end transformation
  • Six or seven ways to implement front-end microservitization
  • Micro front end self-check list
  • [Issue 2154] EMP microfront-end solutions
  • Best Practices of Microfront-end based on Qiankun (illustrated) – Inter-application Communication
  • # 1789. ToB enterprise applications using Angular to build a micro front-end architecture
  • Probably the most complete microfront-end solution you’ve ever seen
  • Micro front end in meituan takeout practice
  • Bifrost micro-front-end framework and its practice in Meituan flash purchase
  • Iqiyi micro front-end architecture practice
  • Polish and application of front-end microserver in Bytedance
  • Use the way of micro front – end to build class single – page application
  • React based microfront-end: A brief analysis of Piral