Build Microfrontend with Angular ,React and Vue

jeetendra nayak
10 min readMay 28, 2020

--

This article will show a running example of the MicroFrontend Application. You can use my code on github as a boiler plate to start your project.
https://github.com/jeetunayak1/Microfrontend

If you want to know what microfrontend is, you can follow my article in the link below.
https://medium.com/@jeetu.nayak1/fighting-with-monolithic-microfrontends-564bc7863449

I will illustrate how to do this using a plugin called single SPA.
https://single-spa.js.org/

For demo purpose I will be running two react microfrontend applications which will be consumed by our vue microfrontend Consumer. You can have microfrontend of any technologies ranging from vue, react, angular, dojo, ember and so on.

To get started create a folder “MicroFrontEnd” and checkout(Fork) two copies of my react boiler plate for react with webpack from below link. Note that single SPA doesn't work on projects which are created with react starter kit. We will need to do some webpack configurations for this to work.

Rename the first copy as Application_1 and second as Application_2.
Go to Application_1\webpack.dev.js and change the port to 3001.

const merge = require('webpack-merge');const common = require('./webpack.common.js');const path = require('path');module.exports = merge(common, {mode: 'development',devtool: 'inline-source-map',devServer: {contentBase: path.join(__dirname, '/dist'),compress: true,port: 3001,disableHostCheck: true,historyApiFallback: true,headers: {"Access-Control-Allow-Origin": "*",}}});

Go to Application_2\webpack.dev.js and change the port to 3002.

const merge = require('webpack-merge');const common = require('./webpack.common.js');const path = require('path');module.exports = merge(common, {mode: 'development',devtool: 'inline-source-map',devServer: {contentBase: path.join(__dirname, '/dist'),compress: true,port: 3002,disableHostCheck: true,historyApiFallback: true,headers: {"Access-Control-Allow-Origin": "*",}}});

Lets make few changes to these applications so that they can be consumed by our microfrontend consumer. You will have to do these changes in both application_1 and application_2.

Open package.json and add following content. Edit name field in each package.json with their respective app names.

Notice that I have also added few scripts to run the application standalone. Once we configure the application to run as a microfrontend, it will then only work with microfrontend consumer and to make this work standlaone we will need a different configuration. So added additional scripts for it to run standalone.

{"name": "Application_1","version": "0.1.0","private": true,"dependencies": {"@ibm/plex": "^4.0.2","@testing-library/jest-dom": "^4.2.4","@testing-library/react": "^9.3.2","@testing-library/user-event": "^7.1.2","axios": "^0.19.2","bootstrap": "^4.4.1","chart.js": "^2.9.3","react-bootstrap": "^1.0.0-beta.17","react-chartjs-2": "^2.9.0","react-native-app-auth": "^5.1.1","single-spa-react": "^2.10.2","systemjs-webpack-interop": "^1.1.2","react": "^16.13.0","react-dom": "^16.13.0","react-router-dom": "^5.1.2"},"scripts": {"start": "webpack-dev-server --https --config webpack.dev.js","start:standalone": "webpack-dev-server --https --open  --config webpack.standalone.js","build:prod": "webpack --config webpack.prod.js","build:standalone-production": "webpack  --config webpack.standalone-production.js"},"browserslist": {"production": [">0.2%","not dead","not op_mini all"],"development": ["last 1 chrome version","last 1 firefox version","last 1 safari version"]},"devDependencies": {"@babel/core": "^7.8.7","@babel/preset-env": "^7.8.7","@babel/preset-react": "^7.8.3","babel-loader": "^8.0.6","clean-webpack-plugin": "^3.0.0","css-loader": "^3.4.2","dotenv-webpack": "^1.7.0","html-loader": "^0.5.5","html-webpack-plugin": "^3.2.0","node-sass": "^4.13.1","react-scripts": "^3.4.0","sass-loader": "^8.0.2","style-loader": "^1.1.3","webpack": "^4.42.0","webpack-cli": "^3.3.11","webpack-dev-server": "^3.10.3","webpack-merge": "^4.2.2"}}

Now go to webpack.common.js and add library target as system. The Single SPA library expects that the library target is amd or system for it to consume.

So here is the webpack.common.js

const path = require('path');const htmlwebpackplugin = require('html-webpack-plugin');const { CleanWebpackPlugin } = require('clean-webpack-plugin');const dotenv = require('dotenv-webpack');module.exports = {entry: './src/index.js',output: {path: path.join(__dirname, '/dist'),filename: 'index_bundle.js',publicPath: '/',libraryTarget: "system"},module: {rules: [{test: /\.(js|jsx)$/,exclude: /node_modules/,use: ['babel-loader']},{test: /\.css$/i,use: ['style-loader', 'css-loader']},{test: /\.scss$/i,use: ['style-loader', 'css-loader', 'sass-loader']}]},plugins: [new htmlwebpackplugin({template: './public/index.html'}),new CleanWebpackPlugin(),new dotenv({path: './.env',safe: true,defaults: true})]};

We will need webpackconfigs to run the application standalone. Se lets create webpack.standalone.js

Make sure you edit the ports in the webpack.standalone.js in application_1 and application_2. Run application_1 on 9001 and application_2 on 9002.
Now set process.env.isStandalone this will instruct application to run as standalone application.

const merge = require('webpack-merge');const common = require('./webpack.common.js');const path = require('path');const webpack = require("webpack");module.exports = merge(common, {mode: 'development',devtool: 'inline-source-map',output: {libraryTarget: "var"},devServer: {contentBase: path.join(__dirname, '/dist'),compress: true,port: 9001,disableHostCheck: true,historyApiFallback: true,headers: {"Access-Control-Allow-Origin": "*",}},plugins: [new webpack.DefinePlugin({"process.env.isStandalone": true})]});

Now lets create webpack.standalone-production.js

const merge = require('webpack-merge');const common = require('./webpack.common.js');const path = require('path');const webpack = require("webpack");module.exports = merge(common, {mode: 'production',devtool: 'source-map',output: {libraryTarget: "var"},plugins: [new webpack.DefinePlugin({"process.env.isStandalone": true})]});

Lets make some changes into index.js. This is very important change as this will have the configurations for our application to run as microfrontend.

We need to implement following methods in our applications

  1. singleSpaReact — this takes the react and react do object and the reference to the app file.
  2. mount, unmount and boostrap functions.
  3. The if loop for process.env.isStandalone to make the application run standalone.
import React from 'react';import ReactDOM from 'react-dom';import App from './Component/App';import singleSpaReact from 'single-spa-react';if (process.env.isStandalone) {ReactDOM.render(<App />, document.getElementById('root'));serviceWorker.unregister();}const lifecycles = singleSpaReact({React,ReactDOM,rootComponent: App})export const bootstrap = lifecycles.bootstrap;export const mount = lifecycles.mount;export const unmount = lifecycles.unmount;

Open app.js and add the following line below var username declaration. This is to ensure that the props are set to appropriate value as we wont be sending it from index.js anymore. Also remove the props param in the app function.

const props={myBaseURL:process.env.BASE_URL_REACT};import React from 'react';import './App.scss';import { BrowserRouter, Router, Switch, Route, Link, Redirect } from "react-router-dom";import { Button } from 'react-bootstrap';const App = () => {const isAuthenticated = true;var username = "reactUser";const props={myBaseURL:process.env.BASE_URL_REACT};const Dashboard = (props) => {return (<span>Dashboard</span>)}const Contacts = (props) => {return (<span>Contacts</span>)}const getMoreX = (props) => {return (<span>getMoreX</span>)}const getMoreY = (props) => {return (<span>getMoreY</span>)}return (<BrowserRouter basename={props.myBaseURL}><nav className="navbar navbar-dark bg-dark flex-md-nowrap p-0 shadow"><Link className="navbar-brand col-sm-3 col-md-2 mr-0" to="/">Bolier Plate</Link><ul className="navbar-nav px-3"><li className="nav-item text-nowrap">{isAuthenticated &&<div><span className="text-primary text-sm mr-2">{username}</span><Button variant="danger" size="sm" className="mx-1" onClick={() => logout()}  >Logout</Button></div>}</li></ul></nav><div className="container-fluid" style={{ overflow: "hidden" }}><div className="row">{isAuthenticated &&<nav className="bg-light sidebar"><div className="sidebar-sticky"><ul className="nav flex-column" id="sidebarnav"><li className="nav-item"><Link className="nav-link" to="/">Home</Link></li><li className="nav-item"><Link className="nav-link" to="/Contacts">Contacts</Link></li><li className="nav-item"><Link className="nav-link dropdown-toggle" data-toggle="collapse" data-target="#More" to="/">More</Link><ul className="collapse nav border pl-3" id="More"><li className="nav-item"><Link className="nav-link" to="/GetMore/GetMoreX">GetMoreX</Link></li><li className="nav-item"><Link className="nav-link" to="/GetMore/GetMoreY">GetMoreY</Link></li></ul></li></ul></div></nav>}<main role="main" className={` ${isAuthenticated ? 'main-view' : ''} px-4`}><div className="pt-3 pb-2 mb-3"><Switch><Route path="/home" component={Dashboard} /><Route path="/Contacts" component={Contacts} /><Route path="/GetMore/GetMoreX" component={getMoreX} /><Route path="/GetMore/GetMoreY" component={getMoreY} /><Route path="/">{<Redirect to={"/home"} />}</Route></Switch></div></main></div></div></BrowserRouter >)}export default App;

Open .env file in application_1 and application_2 and make following changes.

.env in application_1

BASE_URL_REACT=/application_1

.env in application_2

BASE_URL_REACT=/application_2

Now go to application_1 and application_2 folders to import the dependencies. Run following command inside the folders.

npm install

Now run the following command to build and start the project to run as a microfrontend. Run below command in application_1 and application_2.

npm start

Once the applications are successfully build. Try running the below url’s in the browser. Keep these url’s handy to start the application in microfrontend Consumer.
If you want to run the application standalone, we will need to run teh standalone script and run the application independently.

https://localhost:3001/index_bundle.js
https://localhost:3002/index_bundle.js

Now Lets create our Microfrontend Consumer. This would consume our micro frontend applications and run into a single application. Lets create this using Vue.

Create a folder MicrofrontendConsumer. Your folder structure should look something like this.

Once we have that ready, lets now get all dependencies for Vuejs project.

create a package.json file and copy paste this content. Dont bother about scripts much now. Navbar loads navigation for the microfrontend consumer. Note that we have added single spa and systemjs as a dependency in package.json. Single SPA renders applications with systemjs or amd library type only.

{"scripts": {"start": "webpack-dev-server --https --config webpack.dev.js","build:prod": "webpack --config webpack.prod.js","build:navbar": "npm run-script build --prefix navbar"},"devDependencies": {"@babel/core": "^7.7.4","@babel/preset-env": "^7.7.4","babel-eslint": "^11.0.0-beta.2","babel-loader": "^8.0.6","clean-webpack-plugin": "^3.0.0","concurrently": "^5.0.2","css-loader": "^3.4.2","dotenv-webpack": "^1.7.0","html-webpack-plugin": "^3.2.0","serve": "^11.2.0","vue-loader": "^15.9.1","vue-style-loader": "^4.1.2","vue-template-compiler": "^2.6.11","webpack": "^4.42.1","webpack-cli": "^3.3.11","webpack-dev-server": "^3.10.3","webpack-merge": "^4.2.2"},"dependencies": {"import-map-overrides": "^1.14.3","single-spa": "^5.3.1","systemjs": "^6.2.6","vue": "^2.6.11","vue-router": "^3.1.6"}}

Create a .env file where we can have all our environment variables if we need any for the application. When we build our application using webpack these variables will be replaced with actual values in the application.

right now lets just add following line

mode=development

Now lets create a file .babelrc

{"presets": ["@babel/preset-env"]}

Create webpack.common.js, this will contain all the common configuration that is needed in dev and prod environment. We would use webpack merge plugin to merge this with dev or prod webpack based on the environment.

const path = require('path');const { CleanWebpackPlugin } = require('clean-webpack-plugin');const dotenv = require('dotenv-webpack');module.exports = {entry: path.join(__dirname, "src/vue-mf-root-config"),output: {path: path.join(__dirname, '/dist'),filename: 'vue-mf-root-config.js',publicPath: '/',libraryTarget: "system"},module: {rules: [{ parser: { system: false } },{test: /\.js$/,exclude: /node_modules/,use: [{ loader: "babel-loader" }]}]},plugins: [new CleanWebpackPlugin(),new dotenv({path: './.env',safe: true,defaults: false})]};

create a file webpack.dev.js with following content.

const merge = require('webpack-merge');const common = require('./webpack.common.js');const htmlwebpackplugin = require('html-webpack-plugin');const path = require('path');module.exports = merge(common, {mode: 'development',devtool: 'inline-source-map',devServer: {headers: {"Access-Control-Allow-Origin": "*"},disableHostCheck: true,historyApiFallback: true,port: 9000},plugins: [new htmlwebpackplugin({inject: false,template: "src/index.ejs",templateParameters: {isLocal: "development"}})]});

Create a file webpack.prod.js and add following content.

const merge = require('webpack-merge');const common = require('./webpack.common.js');const htmlwebpackplugin = require('html-webpack-plugin');module.exports = merge(common, {mode: 'production',devtool: 'source-map',plugins: [new htmlwebpackplugin({inject: false,template: "src/index.ejs",templateParameters: {isLocal: "production"}})]});

Create a src folder inside microfrontendconsumer and add following files in it

1.activity-functions.js
2.index.ejs
3. vue-mf-root-config.js

Your folder structure now should look something like this.

Add following content in index.ejs. To learn what importmap.js does, follow the link below.

Importmap is basically used for assigning the url’s to variables . Also importmap overrides can help you replace these urls on the fly in the browser.

Here we are importing the index_bundle files which we used to host our micro frontend applications. Make sure that the application_1 and application_2 are still running at 3001 and 3002.

<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>Vue Microfrontends</title><meta name="importmap-type" content="systemjs-importmap" /><!--Common Javascript and CSS for all the applications--><link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"><link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css"integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous"><link rel="stylesheet" href="https://unpkg.com/bootstrap-table@1.16.0/dist/bootstrap-table.min.css"><script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n"crossorigin="anonymous"></script><script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"crossorigin="anonymous"></script><script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"crossorigin="anonymous"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.3/Chart.min.js"></script><!--Common Javascript and CSS for all the applications--><% if (isLocal==='development') { %><script type="systemjs-importmap">{"imports": {"@vue-SPA/root-config": "/vue-mf-root-config.js","single-spa": "/node_modules/single-spa/lib/system/single-spa.min.js","@vue-SPA/navbar": "../navbar/dist/navbar.js","vue": "/node_modules/vue/dist/vue.min.js","application_1": "https://localhost:3001/index_bundle.js","application_2": "https://localhost:3002/index_bundle.js","vue-router": "/node_modules/vue-router/dist/vue-router.min.js"}}</script><% } %><% if (isLocal==='production') { %><script type="systemjs-importmap">{"imports": {"@vue-SPA/root-config": "/vue-mf-root-config.js","single-spa": "/node_modules/single-spa/lib/system/single-spa.min.js","@vue-SPA/navbar": "../navbar/dist/navbar.js","vue": "/node_modules/vue/dist/vue.min.js","application_1": "https://localhost:3001/index_bundle.js","application_2": "https://localhost:3002/index_bundle.js","vue-router": "/node_modules/vue-router/dist/vue-router.min.js"}}</script><% } %><script src="/node_modules/import-map-overrides/dist/import-map-overrides.js"></script><script src="/node_modules/systemjs/dist/system.js"></script><script src="/node_modules/systemjs/dist/extras/amd.js"></script><script src="/node_modules/systemjs/dist/extras/named-exports.js"></script></head><body><script>System.import('@vue-SPA/root-config');</script><import-map-overrides-full show-when-local-storage="devtools" dev-libs></import-map-overrides-full></body></html>

Add following content in activity-functions.js. This file will help the microfrontendconsumer to load the microfrontend applications based on the routes.

export function prefix(location, ...prefixes) {return prefixes.some(prefix => (location.href.indexOf(`${location.origin}/${prefix}`) !== -1))}export function application_1(location) {return prefix(location, 'application_1');}export function application_2(location) {return prefix(location, 'application_2');}

Add following content to vue-mf-root-config.js file.
“registerApplication” is a single spa function which is used to register the application into microfrontend consumer.

import { registerApplication, start } from "single-spa";import * as isActive from "./activity-functions";
registerApplication("application_1",() => System.import("application_1"),isActive.application_1);registerApplication("application_2",() => System.import("application_2"),isActive.application_2);
start();

I would skip the option to create a navbar for the microfrontend consumer which will help you navigate through the application. However you will be able to see it on my github repository.

Now go ahead and install the dependencies

npm install

now start the project by running below command.

npm start

Now to consume these applications you can run following urls. Right now the contents of both the applications are same so you can play around the app.js in each application to see the differences.

https://localhost:9000/application_1

https://localhost:9000/application_2

We can add a navbar in microcontroller consumer to route through these Applications.

I have pushed the code into github with a navbar, you can use this as a boiler plate to start your microfrontend.

This is how the final Microfrontend look after integrtaing navbar.

Screen1 of application1

screen2 of application2

--

--