Frontend,  Tools

Top Micro Frontend Frameworks for Web

In this post, we will share micro frontend frameworks for web development that enable teams to work independently, adopt different technologies, and deliver seamless user experiences.

With the widespread application of Single Page Application (SPA), a new issue arises: the need for scalability in applications.

On one hand, the rapid increase in functionality leads to proportional increases in packaging time, while urgent releases require shorter times, creating a contradiction.

On the other hand, when a codebase integrates all functionalities, daily collaboration becomes extremely challenging. Moreover, in the past decade, frontend technology has rapidly evolved, with a new era emerging every two years, necessitating colleagues to upgrade projects or even switch frameworks.

The initial solution involved the use of iframe to split and scale applications based on their main functional modules, with inter-application navigation. However, this approach posed significant issues such as page reloading and white screens.

So, what are some better solutions? In the following section, we will introduce several micro frontend frameworks.

Introduction

Micro frontends are an architectural pattern that involves breaking down a monolithic frontend application into smaller, self-contained and independent parts.

Each micro frontend represents a distinct feature or functionality and can be developed, deployed, and maintained by separate teams or individuals.

By adopting a micro frontend approach, organizations can achieve greater flexibility, scalability, and agility in their web development process.

Micro frontends allow teams to work autonomously, leverage different technologies, and independently deploy and update their respective components.

This approach promotes modularity, reusability, and easier maintenance, making it easier to scale and evolve complex frontend applications.

There is a main application, referred to as a base application, which is responsible for managing the loading and unloading of various subordinate applications:

The fundamental principles of micro frontends encompass three key aspects: autonomous development, autonomous deployment, and autonomous execution.

Single-spa

Firstly, the routing of all apps is registered in the base application, and single-spa serves as a micro frontend controller, storing the mapping relationship between sub-applications’ routes. When a URL is requested, the sub-application’s route is matched and the sub-application is loaded and rendered.

In this example, we will illustrate the construction of a single-spa framework using a Vue project as the foundation. The configuration of the framework will be implemented in the main.js file, which serves as the entry point for the Vue project.

Base application

//main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import { registerApplication, start } from 'single-spa'

Vue.config.productionTip = false

const mountApp = (url) => {
  return new Promise((resolve, reject) => {
    const script = document.createElement('script')
    script.src = url

    script.onload = resolve
    script.onerror = reject

    // sub-application mount
    const firstScript = document.getElementsByTagName('script')[0]
    firstScript.parentNode.insertBefore(script, firstScript)
  })
}

const loadApp = (appRouter, appName) => {
  return async () => {
    await mountApp(appRouter + '/js/chunk-vendors.js')
    await mountApp(appRouter + '/js/app.js')
   
    return window[appName]
  }
}

// sub-applications list
const appList = [
  {

    name: 'app1',
    app: loadApp('http://localhost:8083', 'app1'),
    activeWhen: location => location.pathname.startsWith('/app1'),
    customProps: {}
  },
  {
    name: 'app2',
    app: loadApp('http://localhost:8082', 'app2'),
    activeWhen: location => location.pathname.startsWith('/app2'),
    customProps: {}
  }
]

// sub applications register
appList.map(item => {
  registerApplication(item)
})
 
new Vue({
  router,
  mounted() {
    start()
  },
  render: h => h(App)
}).$mount('#app')

Sub-applications

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import singleSpaVue from 'single-spa-vue'

Vue.config.productionTip = false

const appOptions = {
  el: '#microApp',
  router,
  render: h => h(App)
}


if (!process.env.isMicro) {
  delete appOptions.el
  new Vue(appOptions).$mount('#app')
}

const appLifecycle = singleSpaVue({
  Vue,
  appOptions
})

export const bootstrap = (props)  => {
  console.log('app2 bootstrap')
  return appLifecycle.bootstrap(() => { })
}

export const mount = (props) => {
  console.log('app2 mount')
  return appLifecycle.mount(() => { })
}

export const unmount = (props) => {
  console.log('app2 unmount')
  return appLifecycle.unmount(() => { })
}

Bundling code using UMD

//vue.config.js
const package = require('./package.json')
module.exports = {
  publicPath: '//localhost:8082',
  devServer: {
    port: 8082
  },
  configureWebpack: {
    output: {
      library: package.name,
      libraryTarget: 'umd'
    }
  }

Setting up environment variables for sub-applications

// .env.micro 
NODE_ENV=development
VUE_APP_BASE_URL=/app2
isMicro=true

The central aspect of configuring a child application is the generation of the child route configuration using singleSpaVue, which necessitates the exposure of its lifecycle functions.

Qiankun

📦 🚀 Blazing fast, simple and complete solution for micro frontends.

Advantages:

  1. It provides a more user-friendly API based on the encapsulation of single-spa. Compared to single-spa, qiankun offers a sandbox environment.
  2. It is technology-agnostic, allowing applications of any technology stack to use/access it, whether it’s React/Vue/Angular/JQuery or other frameworks.
  3. It offers an HTML Entry integration method, making it as simple as using an iframe to integrate micro-applications.
  4. It ensures style isolation, preventing interference between micro-applications.
  5. It provides a JS sandbox to prevent conflicts between global variables/events of micro-applications.
  6. It supports resource preloading, allowing the preloading of micro-application resources that have not been opened during idle time in the browser, thus speeding up the opening speed of micro-applications.

Base application

import { registerMicroApps, start } from 'qiankun';


registerMicroApps([
  {
    name: 'reactApp',
    entry: '//localhost:3000',
    container: '#container',
    activeRule: '/app-react',
  },
  {
    name: 'vueApp',
    entry: '//localhost:8080',
    container: '#container',
    activeRule: '/app-vue',
  },
  {
    name: 'angularApp',
    entry: '//localhost:4200',
    container: '#container',
    activeRule: '/app-angular',
  },
]);

start();

Sub-applications

For example, let’s take a React project generated using Create React App and pair it with React Router DOM.

1. Add a new file “public-path.js” in the “src” directory to resolve conflicts when accessing static resources during the mounting of sub-applications.

if (window.__POWERED_BY_QIANKUN__) {
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
  }

2. Set the base attribute for history mode routing

<BrowserRouter basename={window.__POWERED_BY_QIANKUN__ ? '/app-react' : '/'}>

3. index.js

import './public-path';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';


function render(props) {
  const { container } = props;
  ReactDOM.render(<App />, container ? container.querySelector('#root') : 
  document.querySelector('#root'));
}


if (!window.__POWERED_BY_QIANKUN__) {
  render({});
}


export async function bootstrap() {
  console.log('[react16] react app bootstraped');
}


export async function mount(props) {
  console.log('[react16] props from main framework', props);
  render(props);
}


export async function unmount(props) {
  const { container } = props;
  ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') :  
  document.querySelector('#root'));
}

4. webpack configuration

npm i -D @rescripts/cli

Add .rescriptsrc.js in project root directory.

const { name } = require('./package');


module.exports = {
  webpack: (config) => {
    config.output.library = `${name}-[name]`;
    config.output.libraryTarget = 'umd';
    config.output.jsonpFunction = `webpackJsonp_${name}`;
    config.output.globalObject = 'window';


    return config;
  },


  devServer: (_) => {
    const config = _;


    config.headers = {
      'Access-Control-Allow-Origin': '*',
    };
    config.historyApiFallback = true;
    config.hot = false;
    config.watchContentBase = false;
    config.liveReload = false;


    return config;
  },
};

Micro-app

The micro-app framework draws inspiration from the WebComponent concept and leverages CustomElement in conjunction with a customized ShadowDom to encapsulate micro frontends as WebComponent-like components.

This approach enables the modular rendering of micro frontends. Additionally, the use of a customized ShadowDom eliminates the need for micro-app to require modifications to the rendering logic or the exposure of methods in the sub-applications, as is the case with single-spa and qiankun. Furthermore, there is no need to modify the webpack configuration.

Advantages:

  1. Ease of use:
    All functionalities are encapsulated within a single WebComponent class, allowing for the seamless integration of a micro-frontend application with just one line of code in the host application. Additionally, micro-app provides a comprehensive set of features including JavaScript sandboxing, style isolation, element isolation, preloading, data communication, and static resource completion.
  2. Zero dependencies:
    Micro-app does not rely on any external dependencies, which enables it to have a compact size and greater scalability.
  3. Compatibility with all frameworks:
    To ensure the ability for independent development and deployment of various business components, micro-app has implemented numerous compatibility measures, allowing it to function properly in any technology framework.

Base application

// index.js
import React from "react"
import ReactDOM from "react-dom"
import App from './App'
import microApp from '@micro-zoe/micro-app'

const appName = 'my-app'

microApp.preFetch([
  { name: appName, url: 'xxx' }
])

microApp.setData(appName, { type: '新的数据' })

const childData = microApp.getData(appName)

microApp.start({
  globalAssets: {
    js: ['url1', 'url2', ...], // js url
    css: ['url3', 'url4', ...], // css url
  }
})

Assign a route to a sub-application

// router.js
import { BrowserRouter, Switch, Route } from 'react-router-dom'

export default function AppRoute () {
  return (
    <BrowserRouter>
      <Switch>
        <Route path='/'>
          <micro-app name='app1' url='http://localhost:3000/' baseroute='/'></micro-app>
        </Route>
      </Switch>
    </BrowserRouter>
  )
}

Sub-applications

// index.js
import React from "react"
import ReactDOM from "react-dom"
import App from './App'
import microApp from '@micro-zoe/micro-app'

const appName = 'my-app'


if (window.__MICRO_APP_ENVIRONMENT__) {
  __webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__
}

window.microApp.dispatch({ type: '子应用发送的数据' })

const data = window.microApp.getData() // 返回基座下发的data数据


export function mount() {
  ReactDOM.render(<App />, document.getElementById("root"))
}


export function unmount() {
  ReactDOM.unmountComponentAtNode(document.getElementById("root"))
}


if (window.__MICRO_APP_ENVIRONMENT__) {
  window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount }
} else {
  mount()
}

Configure sub-application route

import { BrowserRouter, Switch, Route } from 'react-router-dom'

export default function AppRoute () {
  return (
    <BrowserRouter basename={window.__MICRO_APP_BASE_ROUTE__ || '/'}>
      ...
    </BrowserRouter>
  )
}

Module Federation

Module Federation is a concept introduced by Webpack 5 to address the issue of code sharing between multiple applications, enabling a more elegant implementation of cross-application code sharing.

The objectives of Module Federation align with those of micro frontends, which involve breaking down an application into multiple independent applications that can be developed and deployed separately. With Module Federation, an application can dynamically load and execute code from another application, facilitating dependency sharing between applications.

To achieve this functionality, Module Federation introduces several key concepts in its design.

Container

A module packaged by ModuleFederationPlugin is called a Container. In simple terms, if our application is built using ModuleFederationPlugin, it becomes a Container. It can load other Containers and can be loaded by other Containers.

Host & Remote

1. host: consumer, which dynamically loads and executes code from other containers.

2. remote: provider, which exposes properties (such as components and methods) for the host to use.

It can be understood that the terms “host” and “remote” are relative, as a container can serve as both a host and a remote.

Shared

A Container can share its dependencies (such as react, react-dom) with other Containers, which means sharing dependencies.

Configuration

// webpack.config.js
const { ModuleFederationPlugin } = require("webpack").container;
new ModuleFederationPlugin({
  name: "appA",
  filename: "remoteEntry.js",
  exposes: {
    "./input": "./src/input",
  },
  remotes: {
    appB: "appB@http://localhost:3002/remoteEntry.js",
  },
  shared: ['react', 'react-dom'],
})

Leave a Reply

Your email address will not be published. Required fields are marked *