Why do you need a micro front end

  • The Monolith Application is created

    We usually use it when we’re developing front-end applicationsSPASuch architecture (pictured), through routing distributed into different pages (components), and each page is composed of various components, with the advancement of business, new function, increasing application in the rising number of modules, the increasing amount of application code, we deploy and maintain the difficulty is also increasing, gradually became a applicationStone application.

    • (Disadvantages of Monolith)
      1. Difficult to deploy and maintain
      2. Obstacle to frequent deployments
      3. Several proposals are proposed. Several proposals are proposed.
      4. It Makes it diffcult to try out new technologies/framework

    To solve this problem, the concept of micro front end is proposed.

  • Micro Frontend

    • Concept (concept)

      Microfront-end is an architecture similar to microservices. It is an architectural style consisting of multiple independently delivered front-end applications, which are broken down into smaller, simpler applications that can be independently developed, tested, and deployed, but are still cohesive as a single product in the view of users. [1]

    • architecture

      The architecture of the micro front-end is shown in the figure below. We decompose an overall application into smaller applications in some way. Each small application is independently developed and deployed, and finally combined through the parent application route to form a front-end application.

    • The advantages and disadvantages

      • Advantage
        • Application autonomy. You only need to follow the same interface specification or framework, so that the system is integrated together, there is no dependency between each other.
        • Single responsibility. Each front-end application can focus only on what it needs to do.
        • Technology stack irrelevant. React and Vue can be used while using native JS.
      • Disadvantage
        • Maintenance problems
        • Architecture complex
    • Application Scenes

      • Center stage/backstage
      • Large Web applications

Implementation method and Principle Analysis of micro front End

Micro front-end implementation

  • iframe
    • why not iframe[2]
      • Performance issues. Iframe is rerendered every time it enters a child application, and resources are reloaded
      • The global context is completely isolated and memory variables are not shared. Cookies are not shared and a specific channel needs to be set up for communication
      • DOM structures are not shared and global popovers cannot be used
      • Url out of sync, page refresh, unable to use forward/backward
  • Based on the system base implementation
    • The main application builds a pedestal in which the child applications are rendered
    • Qiankun, singleSPA

Flow chart of micro front-end framework (Based on system base)

As shown in the figure above, running a microapplication requires a processregistered.Initialize the.runThree steps.

  • Registration is to store microapplication information and create global state management;
  • Initialization will get the registered application address of the HTML file, and extract static JS, CSS files;
  • When running or routing changes, static JS and CSS files will be run and life cycle rendering microapplications will be called;
  • Functions that need to be implemented
    1. Load the HTML
    2. Loading JS files
    3. Define the life cycle
    4. Use sandbox to isolate execution scope
    5. Create an inter-application communication channel
    6. Routing to monitor
    7. Style isolation

The implementation process

1. Obtain applications

  • Get the HTML file

Before creating the constructor, we need to implement a method that takes an HTML string and intercepts a static JS/CSS file. I’ve made a vite compatible approach here

  • loadHtml
    async function loadHtml(
      entry: string.type: LoadScriptType
    ) :Promise<LoadHtmlResult> {
      const data = await fetch(entry, {
        method: 'GET'});let text = await data.text();
      constscriptArr = text .match(scriptReg) ? .filter((val) = > val)
        .map((val) = > (isHttp.test(val) ? val : `${entry}${val}`));
      conststyleArr = text .match(styleReg) ? .filter((val) = > val)
        .map((val) = > (isHttp.test(val) ? val : `${entry}${val}`));
      text = text.replace(/(<script.*><\/script>)/g.' ');
      console.log(scriptArr);
    
      const scriptText: string[] = [];
      if (type= = ='webpack' && scriptArr) {
        for (const item of scriptArr) {
          let scriptFetch = await fetch(item, { method: 'GET' });
          scriptText.push(awaitscriptFetch.text()); }}return {
        entry,
        html: text,
        scriptSrc: type= = ='webpack' ? scriptText : scriptArr || [],
        styleSrc: styleArr || [],
      };
    }
    Copy the code
  • Run JS(runScript) to define the life cycle

Once you get the HTML file, you need to define a method that executes the microapplication JS file and calls the lifecycle

  • The life cycle
    • BeforeMount Function called before rendering application
    • Mount Calls the render function when mounting the microapplication
    • Unmount Called when the microapplication is unmounted
/** Lifecycle function */
export type LoadFunctionResult = {
  beforeMount: () = > void;
  mount: (props: LoadFunctionMountParam) = > void;
  unmount: (props: UnloadFunctionParam) = > void;
};
export type LoadScriptType = 'esbuild' | 'webpack';

/** Inject the environment variable */
export function injectEnvironmentStr(context: ProxyParam) {
  context[PRODUCT_BY_MICRO_FRONTEND] = true;
  context.__vite_plugin_react_preamble_installed__ = true;
  return true;
}

/** Use import to load script */
export async function loadScriptByImport(scripts: string[]) {
  injectEnvironmentStr(window);
  let scriptStr = ` return Promise.all([`;
  scripts.forEach((val) = > {
    scriptStr += `import("${val}"), `;
  });
  scriptStr = scriptStr.substring(0, scriptStr.length - 1);
  scriptStr += `]); `;
  return await new Function(scriptStr)();
}

/** Executes the js string */
export async function loadScriptByString(
  scripts: string[],
  context: ProxyParam
) {
  const scriptArr: Promise<Record<string.any= > > [] []; injectEnvironmentStr(context); scripts.forEach(async (val) => {
    scriptArr.push(
      await new Function(`
          return (window => {
            ${val}
            return window.middleVue;
          })(this)
    `).call(context)
    );
  });
  return scriptArr;
}

/** Load the JS file */
export async function loadFunction<T extends LoadFunctionResult> (
  context: Window,
  scripts: string[] = [],
  type: LoadScriptType = 'esbuild'
) :Promise<T> {
  let result = {};
  if (type= = ='esbuild') {
    result = await loadScriptByImport(scripts);
  } else {
    result = await loadScriptByString(scripts, context);
  }

  let obj: LoadFunctionResult = {
    beforeMount: () = > {},
    mount: () = > {},
    unmount: () = >{}}; (<Record<string.any>[]>result).forEach((val) = > {
    Object.assign(obj, val);
  });
  return <T>obj;
}
Copy the code

2. Define the constructor (MicroFront)

Above, we’ve implemented methods to get HTML and run JS files. Next you need to define a constructor to call them.

To register an application, we need to pass in an array of objects with the following parameters.

interface MicroFrountendMethod {
  init: () = > void;
  setCurrentRoute: (routeName: string) = > void;
  start: () = > void;
}

export default class MicroFrountend implements MicroFrountendMethod {
  /** List of microapps */
  private servers: RegisterData[];
  /** List of requested applications */
  private serverLoadData: Record<string, LoadHtmlResult>;
  /** Current route */
  public currentRoute: string;
  /** Currently open microapplication container */
  public currentActiveApp: string[];
  /** global store */
  public store: Record<string.any>;

  constructor(servers: RegisterData[]) {
    this.servers = servers;
    this.serverLoadData = {};
    this.currentRoute = ' ';
    this.currentActiveApp = [];
    this.store = createStore();
  }

  /** Initialize */
  public async init() {
    for (let item of this.servers) {
      const serverData = await loadHtml(item.entry, item.type);
      addNewListener(item.appName);
      this.serverLoadData[item.appName] = serverData;
    }

    return true;
  }

  /** Set the route */
  public setCurrentRoute(routeName: string) {
    const appIndex = this.servers.findIndex(
      (val) = > val.activeRoute === routeName
    );
    if (appIndex === -1) return false;
    const appName = this.servers[appIndex].appName;
    const isInclude = Object.keys(this.serverLoadData).includes(appName);
    if(! isInclude) {return false;
    }

    this.currentRoute = routeName;
    return true;
  }

  /** Start loading the micro front-end application */
  public async start() {
    const currentRoute = this.currentRoute || window.location.pathname;
    const appList = this.servers.filter(
      (val) = > val.activeRoute === currentRoute
    );
    for (let val of appList) {
      const appName = val.appName;
      const htmlData = this.serverLoadData[appName];
      const scriptResult = await runScript(val, htmlData, this.store);
      this.serverLoadData[appName].lifeCycle = scriptResult.lifeCycle;
      this.serverLoadData[appName].sandbox = scriptResult.sandBox; }}Copy the code

3. JS sandbox

The introduction of sandbox has a lot of documents in the introduction, here is not more introduction, specific can see here

  • Sandbox type Iframe

        <iframe></iframe>
    Copy the code

    SnapshopSandbox



    LegacySandbox (singleton sandbox)



    ProxySandbox (several sandboxes)

  • Sandbox implementation (multiple sandboxes)

    interface SandBoxImplement {
      active: () = > void;
      inActive: () = > void;
    }
    
    type ProxyParam = Record<string.any> & Window;
    
    /** Sandbox operation */
    class SandBox implements SandBoxImplement {
      public proxy: ProxyParam;
      private isSandboxActive: boolean;
      public name: string;
    
      /** Activate the sandbox */
      active() {
        this.isSandboxActive = true;
      }
    
      /** Close the sandbox */
      inActive() {
        this.isSandboxActive = false;
      }
    
      constructor(appName: string, context: Window & Record<string.any>) {
        this.name = appName;
        this.isSandboxActive = false;
        const fateWindow = {};
        this.proxy = new Proxy(<ProxyParam>fateWindow, {
          set: (target, key, value) = > {
            if (this.isSandboxActive) {
              target[<string>key] = value;
            }
            return true;
          },
          get: (target, key) = > {
            if (target[<string>key]) {
              return target[<string>key];
            } else if (Object.keys(context).includes(<string>key)) {
              return context[<string>key];
            }
    
            return undefined; }}); }}export default SandBox;
    Copy the code
  • Run microapplications in a sandbox

    /** Inject the environment variable */
    function injectEnvironmentStr(context: ProxyParam) {
      context[PRODUCT_BY_MICRO_FRONTEND] = true;
      context.__vite_plugin_react_preamble_installed__ = true;
      return true;
    }
    
    /** Executes the js string */
    async function loadScriptByString(
      scripts: string[],
      context: ProxyParam
    ) {
      const scriptArr: Promise<Record<string.any= > > [] []; injectEnvironmentStr(context); scripts.forEach(async (val) => {
        scriptArr.push(
          await new Function(`
              return (window => {
                ${val}
                return window.middleVue;
              })(this)
        `).call(context)
        );
      });
      return scriptArr;
    }
    Copy the code

4. Manage global communication status

At this point, we are ready to run the child application within the parent application. But a lot of times you need to go between the parent application and the child applicationData communicationOr between subapplicationsData communication.

So we need to implement one moreGlobal state managementState management methods are passed into the microapplication when the lifecycle is invoked. The communication flow is shown in the figure belowPublish — subscribeMethod notifies the subscribed parameter change event to fire the corresponding business function.

  • Creating global State
    /** Create global store */
    export function createStore() {
      const globalStore = new Proxy(<Record<string.any> > {}, {get(target, key: string) {
          return target[key];
        },
        set(target, key: string, value) {
          const oldVal = target[key];
          target[key] = value;
    
          // Trigger a listening event
          triggerEvent({ key, value, oldValue: oldVal });
          return true; }});return globalStore;
    }
    Copy the code
  • Adding listeners
    export type triggerEventParam<T> = {
      key: string;
      value: T;
      oldValue: T;
    };
    /** Listener */
    const listener: Map<
      string,
      Record<string.(data: triggerEventParam<any>) = > void>
    > = new Map(a);/** Add store listener */
    export function addNewListener(appName: string) {
      if (listener.has(appName)) return;
      listener.set(appName, {});
    }
    Copy the code
  • Subscribe to the event
    /** Sets the listening event */
    export function setEventTrigger<T extends any> (
      appName: string,
      key: string,
      callback: (data: triggerEventParam<T>) => void
    ) {
      if (listener.has(appName)) {
        const obj = listener.get(appName);
        if(obj) { obj[key] = callback; }}}Copy the code
  • Triggering event
    /** Changing the field value triggers the event */
    export function triggerEvent<T extends any> (data: triggerEventParam<T>) {
      listener.forEach((val) = > {
        if (val[data.key] && typeof val[data.key] === 'function') { val[data.key](data); }}); }Copy the code

5. Route listening

Route monitoring is a very important and complex link in the micro front end. We need to mount or unmount applications by listening for route changes. We all know that there are two types of front-end routes: Hash and history routes. Listening for hash is as simple as defining the method hashchange in the listener. We can use popState to listen for the history route, but we can’t use popState to listen for pushState and replaceState in history. We need to override these two methods. Put listeners in both methods.

  • Listen for hash route change callbacks
    /** Listen for hash route changes */
    function listenHash(callback: listenCallback) {
      window.addEventListener('hashchange'.(ev) = > {
        callback(getHashPathName(ev.oldURL), getHashPathName(ev.newURL), {});
      });
    }
    
    function getHashPathName(url: string) {
      const pathArr = url.split(The '#');
      return pathArr[1]?` /${pathArr[1]}` : '/';
    }
    Copy the code
  • Listen for the history route change callback
    export type listenCallback = (
      oldPathName: string,
      pathName: string,
      param: any
    ) = > void;
    // Save the native method
    const globalPushState = window.history.pushState;
    const globalReplaceState = window.history.replaceState;
    /** listen for history route changes */
    function listenHistory(callback: listenCallback, currentRoute: string) {
      
      window.history.pushState = historyControlRewrite('pushState', callback);
      window.history.replaceState = historyControlRewrite('replaceState', callback);
      window.addEventListener('popstate'.(ev) = > {
        callback(currentRoute, window.location.pathname, ev.state);
      });
    }
    
    // Override pushState, replaceState methods
    const historyControlRewrite = function (
      name: 'pushState' | 'replaceState',
      callback: listenCallback
    ) {
      const method = history[name];
      return function (data: any, unused: string, url: string) {
        const oldPathName = window.location.pathname;
        if (oldPathName === url) return;
        method.apply(history, [data, unused, url]);
        callback(oldPathName, url || ' ', data);
      };
    };
    Copy the code

6. Style isolation

  • Style isolation solution
    1. Get the style in the micro-front-end framework and add a unique prefix
    2. Conventions in microapplications are handled through PostCSS
  • Use PostCSS to handle style isolation

demo

I’m using the parent applicationNative JS, respectively defined onevueThe framework andreactProject of the framework

Vue project

The React project

run

summary

Source code address: github.com/Ichliebedic…

reference

  1. How did Bytedance land on the micro front end
  2. Why Not Iframe
  3. qiankun
  4. singleSpa
  5. From zero to one to achieve enterprise-level micro-front-end framework, nanny level teaching
  6. Decrypting the micro front end: a view of sandbox isolation from Qiankun