Lesson 03-In-Depth Analysis of Mainstream Micro Frontend Frameworks

Single-SPA

Core Concepts and Architecture

Single-SPA is one of the earliest micro frontend frameworks, defining the concept and basic architecture of micro frontends. Its core idea is to decompose a frontend application into multiple independent “sub-applications,” managed and coordinated by a “base application” responsible for loading, unloading, and lifecycle management.

The Single-SPA architecture includes three key components:

  1. Base Application (Bootstrap): Manages the control flow of the overall application, handling sub-application registration and lifecycle.
  2. Sub-Application (Application): Independent business modules that can be developed and deployed separately.
  3. Loader: Responsible for dynamically loading sub-application resources.

Single-SPA uses JavaScript module systems to organize code, leveraging SystemJS or native ES Modules for dynamic loading.

Application Registration and Lifecycle Management

Single-SPA uses the registerApplication method to register sub-applications:

import { registerApplication, start } from 'single-spa';

registerApplication({
  name: 'app1',
  app: () => System.import('app1'),
  activeWhen: ['/app1'],
  customProps: {
    // Props passed to the sub-application
  }
});

start();

The lifecycle of sub-applications is managed by Single-SPA, including:

  • bootstrap: Initializes the application.
  • mount: Mounts the application to the DOM.
  • unmount: Unmounts the application from the DOM.
  • unload: Unloads the application resources.

Sub-applications need to export these lifecycle methods:

// app1.js
export const bootstrap = () => Promise.resolve();
export const mount = () => {
  // Mounting logic
};
export const unmount = () => {
  // Unmounting logic
};

Active Application Management Strategies

Single-SPA provides multiple strategies to determine which sub-application is active:

  1. URL Path Matching: Configures path rules using activeWhen.
  2. Custom Logic: Uses a function that returns a boolean to decide.
  3. Manual Control: Dynamically modifies using setActiveWhen.
registerApplication({
  name: 'app1',
  app: () => System.import('app1'),
  activeWhen: [
    '/app1',
    location => location.pathname.startsWith('/dashboard') && location.query.theme === 'dark'
  ]
});

Custom Loader Implementation

Single-SPA allows custom loaders to control how sub-applications are loaded:

function customLoader(appName) {
  return {
    loadApp: () => import(`./${appName}/app.js`),
    unloadApp: () => Promise.resolve()
  };
}

registerApplication({
  name: 'custom-app',
  app: () => customLoader('custom-app').loadApp(),
  // ...
});

More complex loaders can implement caching, error handling, and other features:

const appCache = {};

function advancedLoader(appName) {
  return {
    loadApp: () => {
      if (appCache[appName]) {
        return Promise.resolve(appCache[appName]);
      }
      return import(`./${appName}/app.js`)
        .then(app => {
          appCache[appName] = app;
          return app;
        })
        .catch(err => {
          delete appCache[appName];
          throw err;
        });
    },
    unloadApp: () => {
      delete appCache[appName];
      return Promise.resolve();
    }
  };
}

Qiankun

Basic Usage and Configuration

Qiankun is an open-source micro frontend framework by Alibaba, built on Single-SPA, offering a more comprehensive solution.

Basic configuration example:

// Main application
import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'app1',
    entry: '//localhost:7100',
    container: '#app1-container',
    activeRule: '/app1',
  },
  {
    name: 'app2',
    entry: '//localhost:7101',
    container: '#app2-container',
    activeRule: '/app2',
  },
]);

start();

Sub-applications need to expose lifecycle methods:

// Sub-application entry file
export async function bootstrap() {
  console.log('app1 bootstraped');
}

export async function mount(props) {
  console.log('app1 mount', props);
  // Render application to container
}

export async function unmount() {
  console.log('app1 unmounted');
  // Unmount application
}

Main and Sub-Application Communication Mechanisms

Qiankun provides multiple communication methods:

  1. Props Passing: The main application passes data to sub-applications via props.
registerMicroApps([
  {
    // ...
    props: { user: { name: 'admin' } }
  }
]);

Sub-application receiving:

export async function mount(props) {
  console.log(props.user); // { name: 'admin' }
}
  1. Custom Events: Use browser-native events or a publish-subscribe pattern.
// Main application sending
window.dispatchEvent(new CustomEvent('global-event', { detail: { data: 'test' } }));

// Sub-application listening
window.addEventListener('global-event', (e) => {
  console.log(e.detail.data);
});
  1. Global State Management: Use libraries like Redux or Vuex to share state.

Sandbox Isolation Principles

Qiankun implements two sandbox mechanisms:

  1. Snapshot Sandbox:
    • Records the initial state of global variables.
    • Saves the current state before executing sub-application code.
    • Restores the state after execution.
class SnapshotSandbox {
  constructor() {
    this.proxy = window;
    this.modifyPropsMap = {};
    this.windowSnapshot = {};
  }
  
  active() {
    this.windowSnapshot = {};
    for (const prop in window) {
      if (window.hasOwnProperty(prop)) {
        this.windowSnapshot[prop] = window[prop];
      }
    }
    return this.proxy;
  }
  
  inactive() {
    for (const prop in window) {
      if (window.hasOwnProperty(prop) && window[prop] !== this.windowSnapshot[prop]) {
        this.modifyPropsMap[prop] = window[prop];
        window[prop] = this.windowSnapshot[prop];
      }
    }
    Object.keys(this.modifyPropsMap).forEach(prop => {
      window[prop] = this.modifyPropsMap[prop];
    });
    this.modifyPropsMap = {};
  }
}
  1. Proxy Sandbox:
    • Uses Proxy to intercept object operations.
    • Creates an independent variable space.
class ProxySandbox {
  constructor() {
    this.proxy = new Proxy({}, {
      get: (target, p) => {
        if (p === 'window') return sandbox;
        return sandbox.globalContext[p];
      },
      set: (target, p, value) => {
        sandbox.globalContext[p] = value;
        return true;
      }
    });
    this.globalContext = {};
  }
}

Performance Optimization Strategies

  1. Resource Preloading: Preload sub-application resources.
// Main application
import { prefetchApps } from 'qiankun';

prefetchApps();
  1. On-Demand Loading: Load sub-applications only when needed.
registerMicroApps([
  {
    // ...
    props: { loadOnDemand: true }
  }
]);
  1. Caching Optimization: Cache loaded sub-applications.
const appCache = new Map();

function loadApp(appName) {
  if (appCache.has(appName)) {
    return Promise.resolve(appCache.get(appName));
  }
  return import(`./${appName}/app.js`)
    .then(app => {
      appCache.set(appName, app);
      return app;
    });
}
  1. Code Splitting: Use Webpack’s dynamic imports.
const App = React.lazy(() => import('./App'));

Module Federation

Core Concepts of Webpack Module Federation

Module Federation, introduced in Webpack 5, is a micro frontend solution that allows different build systems to share code.

Core concepts:

  1. Host: The application that consumes remote modules.
  2. Remote: The application that provides modules.
  3. Shared: Dependencies shared across multiple applications.

Shared Dependency Management

Configure shared dependencies using the shared option:

// Main application Webpack configuration
new ModuleFederationPlugin({
  name: 'host',
  remotes: {
    app1: 'app1@http://localhost:3001/remoteEntry.js',
  },
  shared: {
    react: { singleton: true, requiredVersion: '^17.0.2' },
    'react-dom': { singleton: true, requiredVersion: '^17.0.2' }
  }
});

// Sub-application Webpack configuration
new ModuleFederationPlugin({
  name: 'app1',
  filename: 'remoteEntry.js',
  exposes: {
    './Button': './src/Button'
  },
  shared: {
    react: { singleton: true, requiredVersion: '^17.0.2' },
    'react-dom': { singleton: true, requiredVersion: '^17.0.2' }
  }
});

Remote Module Loading Mechanism

Dynamically import remote modules:

import('app1/Button').then(({ default: Button }) => {
  // Use Button component
});

Webpack automatically handles the loading and caching of remote modules.

Dynamic and On-Demand Loading

Combine React.lazy and Suspense for on-demand loading:

const Button = React.lazy(() => import('app1/Button'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Button />
    </Suspense>
  );
}

Dependency Conflict Resolution Strategies

  1. Version Alignment: Ensure shared dependency versions are consistent.
  2. Singleton Mode: Enforce singleton instances for shared dependencies.
shared: {
  react: { singleton: true }
}
  1. Isolated Loading: Create separate contexts for different dependency versions.
new ModuleFederationPlugin({
  // ...
  shared: {
    lodash: {
      requiredVersion: '^4.17.21',
      import: 'lodash-es', // Use different package name
      shareScope: 'lodash-v4'
    }
  }
});
  1. Dependency Hoisting: Elevate shared dependencies to the main application.

Wujie

Features and Advantages

Wujie is a next-generation micro frontend framework with the following features:

  1. Technology Stack Agnostic: Supports all frontend frameworks and pure HTML applications.
  2. High Performance: Optimized loading and rendering mechanisms.
  3. Robust Sandboxing: Provides multiple sandbox isolation solutions.
  4. Developer-Friendly Experience: Supports hot updates and source maps.

Comparison with Qiankun

FeatureWujieQiankun
Technology Stack SupportAll frameworks + HTMLMajor frameworks
SandboxMultiple solutionsSnapshot/Proxy
PerformanceOptimized loadingStandard implementation
Developer ExperienceHot updatesRequires configuration
CommunityNewerMature

Advanced Feature Usage

  1. CSS Isolation:
wujie.start({
  sandbox: {
    strictStyleIsolation: true, // Enforce style isolation
    experimentalStyleIsolation: true // Experimental style isolation
  }
});
  1. Preloading:
wujie.microApp.preload('app1');
  1. Communication:
// Main application
wujie.start({
  plugins: {
    modules: {
      'app1': {
        onCreated(app) {
          app.postMessage({ type: 'init', data: {} });
        }
      }
    }
  }
});

// Sub-application
window.addEventListener('message', (e) => {
  if (e.data.type === 'init') {
    // Handle initialization data
  }
});

Other Frameworks

Piral

Piral is a micro frontend framework focused on building modular frontend applications.

Architecture and Usage

  1. Piral Core: The base application.
  2. Pilet: Sub-applications (plugins).

Installation:

npm install piral-core

Create a base application:

import { createInstance } from 'piral-core';

const instance = createInstance({
  // Configuration
});

instance.run();

Register a Pilet:

{
  "name": "my-pilet",
  "version": "1.0.0",
  "pilet": {
    "entry": "./src/index.tsx"
  }
}

Features

  • React-based.
  • Pluginized architecture.
  • Hot update support.
  • Dependency management.

Luigi

Luigi is an open-source micro frontend framework by SAP, designed for enterprise applications.

Architecture and Usage

  1. Micro Frontends: Sub-applications.
  2. Luigi Core: Coordinator.

Configuration example:

import { LuigiConfig } from 'luigi-client';

LuigiConfig.getConfigValue('clientOptions', {
  routing: {
    useHashRouting: true
  }
});

Register sub-applications:

{
  "applications": [
    {
      "pathSegment": "app1",
      "viewUrl": "http://localhost:4200"
    }
  ]
}

Features

  • Enterprise-grade features.
  • Permission management.
  • Multi-language support.
  • Deep integration with the SAP ecosystem.

Summary

Each micro frontend framework has its own strengths and use cases:

  1. Single-SPA: Ideal for highly customized projects, with a steeper learning curve.
  2. Qiankun: Open-source by Alibaba, with strong community support, suitable for most enterprise applications.
  3. Module Federation: Natively supported by Webpack, ideal for Webpack-based projects.
  4. Wujie: Technology stack agnostic with optimized performance, suitable for exploring new technologies.
  5. Piral: React ecosystem, pluginized architecture, suitable for React projects.
  6. Luigi: Enterprise applications with SAP ecosystem integration, suitable for large enterprises.

When choosing a micro frontend framework, consider:

  • Technology stack compatibility.
  • Team familiarity.
  • Project scale.
  • Performance requirements.
  • Long-term maintenance needs.

While micro frontend architecture is powerful, it also increases system complexity, requiring careful consideration of trade-offs before adoption.

Share your love