Frontend Development 14 min read

Micro Frontends: Concepts, Scenarios, and Implementation with single-spa and qiankun

The article explains micro‑frontend concepts, why large apps are split into independent modules, critiques iframe drawbacks, details single‑spa’s resource loader and configuration approach with code examples, outlines its limitations, and shows how qiankun builds on single‑spa to automate resource discovery, sandboxing, and style isolation, concluding that micro‑frontends are a valuable strategy for large‑scale front‑end projects.

vivo Internet Technology
vivo Internet Technology
vivo Internet Technology
Micro Frontends: Concepts, Scenarios, and Implementation with single-spa and qiankun

This article provides a comprehensive overview of micro frontends, describing the typical scenarios that lead to splitting a large web application into multiple independent front‑end modules. It compares the micro‑frontend approach to backend micro‑services and explains why simple iframe solutions are insufficient.

Typical problems of using iframes include limited display area, loss of navigation history, isolated global context, and slower performance.

The article then introduces single-spa as a technology‑agnostic solution that combines the advantages of MPA and SPA, allowing multiple applications to run within a single page.

Key components of a single‑spa based micro‑frontend architecture are described:

Resource loader : loads child application bundles (usually UMD) via SystemJS or another module loader.

Application configuration table : records the URLs of each child app’s entry resources and is updated by the server when hashes change.

1. Framework entry (HTML demo)

<!DOCTYPE html>
<html>
<head>
  <!-- Register modules in SystemJS -->
  <script type="systemjs-importmap">
    {
      "imports": {
        "app1": "http://localhost:8081/js/app.js",
        "app2": "http://localhost:8082/js/app.js",
        "single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/4.3.7/system/single-spa.min.js",
        "vue": "https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js",
        "vue-router": "https://cdn.jsdelivr.net/npm/[email protected]/dist/vue-router.min.js",
        "vuex": "https://cdnjs.cloudflare.com/ajax/libs/vuex/3.1.2/vuex.min.js"
      }
    }
  </script>
</head>
<body>
  <div></div>
  <!-- Load SystemJS -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/system.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/amd.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/named-exports.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/named-register.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/6.1.1/extras/use-default.min.js"></script>
  <script>
    (function () {
      // Load common libraries
      Promise.all([
        System.import('single-spa'),
        System.import('vue'),
        System.import('vue-router'),
        System.import('vuex')
      ]).then(function (modules) {
        var singleSpa = modules[0];
        var Vue = modules[1];
        var VueRouter = modules[2];
        var Vuex = modules[3];
        Vue.use(VueRouter);
        Vue.use(Vuex);
        // Register child applications
        singleSpa.registerApplication('app1', () => System.import('app1'), location => location.pathname.startsWith('/app1'));
        singleSpa.registerApplication('app2', () => System.import('app2'), location => location.pathname.startsWith('/app2'));
        // Start the orchestrator
        singleSpa.start();
      });
    })();
  </script>
</body>
</html>

2. Child application entry (Vue example)

import './set-public-path';
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import singleSpaVue from 'single-spa-vue';

Vue.config.productionTip = false;

if (process.env.NODE_ENV === 'development') {
  // Directly mount in development mode
  new Vue({
    router,
    render: h => h(App)
  }).$mount('#app');
}

const vueLifecycles = singleSpaVue({
  Vue,
  appOptions: {
    render: h => h(App),
    router
  }
});

export const bootstrap = vueLifecycles.bootstrap;
export const mount = vueLifecycles.mount;
export const unmount = vueLifecycles.unmount;

The set-public-path.js file ensures that asynchronous resources loaded by a child app use the correct public path when the app runs inside a single‑spa container.

import { setPublicPath } from 'systemjs-webpack-interop';
setPublicPath('app1', 2);

The implementation of setPublicPath (from systemjs-webpack-interop ) validates parameters, resolves the module URL via SystemJS, and sets __webpack_public_path__ to the appropriate directory level.

export function setPublicPath(systemjsModuleName, rootDirectoryLevel) {
  if (!rootDirectoryLevel) {
    rootDirectoryLevel = 1;
  }
  if (typeof systemjsModuleName !== "string" || systemjsModuleName.trim().length === 0) {
    throw Error("systemjs-webpack-interop: setPublicPath(systemjsModuleName) must be called with a non‑empty string 'systemjsModuleName'");
  }
  if (typeof rootDirectoryLevel !== "number" || rootDirectoryLevel <= 0 || !Number.isInteger(rootDirectoryLevel)) {
    throw Error("systemjs-webpack-interop: setPublicPath(systemjsModuleName, rootDirectoryLevel) must be called with a positive integer 'rootDirectoryLevel'");
  }
  let moduleUrl;
  try {
    moduleUrl = window.System.resolve(systemjsModuleName);
    if (!moduleUrl) {
      throw Error();
    }
  } catch (err) {
    throw Error("systemjs-webpack-interop: There is no such module '" + systemjsModuleName + "' in the SystemJS registry. Did you misspell the name of your module?");
  }
  __webpack_public_path__ = resolveDirectory(moduleUrl, rootDirectoryLevel);
}

function resolveDirectory(urlString, rootDirectoryLevel) {
  const url = new URL(urlString);
  const pathname = new URL(urlString).pathname;
  let numDirsProcessed = 0,
    index = pathname.length;
  while (numDirsProcessed !== rootDirectoryLevel && index >= 0) {
    const char = pathname[--index];
    if (char === "/") {
      numDirsProcessed++;
    }
  }
  if (numDirsProcessed !== rootDirectoryLevel) {
    throw Error("systemjs-webpack-interop: rootDirectoryLevel (" + rootDirectoryLevel + ") is greater than the number of directories (" + numDirsProcessed + ") in the URL path " + url.href);
  }
  url.pathname = url.pathname.slice(0, index + 1);
  return url.href;
}

3. Limitations of single‑spa

If a child app has multiple initialization files (e.g., separate CSS and npm modules), developers must maintain a custom resource list, which adds complexity.

Integrating several child apps on the same page can cause CSS/JS conflicts. Naming conventions, CSS prefixing tools, or sandboxing techniques are required, but implementing robust sandboxes is non‑trivial.

4. qiankun

qiankun, an open‑source library built on top of single‑spa, addresses many of the above pain points:

It parses the child app’s HTML entry instead of a single JS file, so the entry URL never changes and all resources are automatically discovered.

When a child app is mounted, its DOM (including dynamically added style tags) is confined to a dedicated root node, and the whole DOM is removed on unmount, preventing style clashes.

Provides a JavaScript sandbox that proxies the global window object and hijacks global event listeners, ensuring isolation between child apps.

The article concludes that while micro‑frontend solutions are not a silver bullet, they are worth exploring for large‑scale front‑end projects.

References:

single-spa

qiankun

可能是你见过最完善的微前端解决方案

JavaScriptfrontend architectureqiankunVue.jsmicro frontendssingle-spa
vivo Internet Technology
Written by

vivo Internet Technology

Sharing practical vivo Internet technology insights and salon events, plus the latest industry news and hot conferences.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.