DEV Community

Cover image for Let's Create A Web Application With Micro Frontends And Firebase
Issam El Nasiri
Issam El Nasiri

Posted on • Updated on

Let's Create A Web Application With Micro Frontends And Firebase

Hello everyone! This is my first time doing a tutorial so I hope you like it! In this tutorial, we'll learn how to create a very simple web application with Firebase, based on the Micro Frontend Architecture.

Defining Our Micro Frontend

There are two ways to create your own micro frontend. The first one is a horizontal split which basically means that you will have multiple applications running on 1 page/view. The second one is the vertical split which is one application running on 1 page/view.

For the sake of this tutorial, we will be using the vertical split.

Let's start by cloning the starters repo:


run npm install in the root folder.

Overview

In the starters repo, we have the main shell which is made with the Vue framework. Our main shell will show our sub/micro applications. We have two sub-applications. They’re called reactapp and vueapp and are written in React and in Vue respectively.

Steps

We are going to divide this tutorial in multiple steps. First, we are going to implement the react micro app. Afterwards, we'll implement the vue micro app. Lastly, we are going to configure the main shell to show our micro apps.

STEP 1 - React Micro App

We need to setup 4 things before implementing the Firebase connectivity:

  • Add .rescriptsrc.js file
  • Add .env file
  • Add public-path.js file
  • Edit the index.js file

.rescriptsrc.js

The .rescriptsrc.js is a file where we can configure settings for launching the micro application. Create the file in the root folder of the react micro app and add the following code:

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;
  },
};
Enter fullscreen mode Exit fullscreen mode

.env

We also need to create a .env file to store our keys and port.

Create the file and add the following code:

SKIP_PREFLIGHT_CHECK=true
BROWSER=none
PORT=7100
WDS_SOCKET_PORT=7100
REACT_APP_FIREBASE_API_KEY=<key>
REACT_APP_FIREBASE_AUTH_DOMAIN=<key>
REACT_APP_FIREBASE_PROJECT_ID=<key>
REACT_APP_FIREBASE_STORAGE_BUCKET=<key>
REACT_APP_FIREBASE_MESSAGING_SENDER_ID=<key>
REACT_APP_FIREBASE_APP_ID=<key>
Enter fullscreen mode Exit fullscreen mode

public-path.js

In the source (src) folder of the reactapp folder. We need to define the public path for the application. Create the file with the same name as the title and add the following code:

if (window.__POWERED_BY_QIANKUN__) {
    // eslint-disable-next-line no-undef
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
Enter fullscreen mode Exit fullscreen mode

Qiankun will define the public path through the main shell.
Qiankun is a micro frontend framework that helps us by setting everything up in our main shell.

Note The Qiankun dependecy is only needed in the main shell and not in the micro apps. The window object will handle all the rest.

index.js

Now we need to edit the index.js file so that the main shell will find this application.

Copy the code below and paste it in the index.js file:

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

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);


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

// This is to render the micro application when not going through the main shell
if (!window.__POWERED_BY_QIANKUN__) {
  render({});
}

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

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

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

serviceWorker.unregister();
Enter fullscreen mode Exit fullscreen mode

bootstrap, mount and unmount are three lifecycle
hooks that must be implemented in any Qiankun-powered micro application.

A lifecycle function is a function or array of functions that Qiankun will call on a registered application. Qiankun calls these by finding specific named exports from the registered application's main file.

Without those 3 hooks, the micro app will not load through the main shell.

Firebase

in the src folder of reactapp, we need to create a new folder called firebase. In that folder we'll create a new javascript file with the name firebaseConfig.js.

Add the following code to setup your firebase and import extra packages if needed (like storage, firestore,...) In this tutorial, we only need the auth package.

import firebase from 'firebase/app';
import 'firebase/auth';

const config = {
  apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
  authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
  storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.REACT_APP_FIREBASE_APP_ID,
};
var fb = null;
// Check how many times the firebase app is initialized.
if (firebase.apps.length === 0) {
  fb = firebase.initializeApp(config);
  console.log('Firebase [react] Initialized');
} else {
  console.log('Firebase [react] is already Initialized');
}

export default fb;
Enter fullscreen mode Exit fullscreen mode

App.js

now we can start editing our App.js to show two input elements for registering a new user.

First, let's handle the imports. These 3 are the only ones needed so you can remove the other ones.

import React, {useState} from 'react';
import './App.css';
import fb from './firebase/firebaseConfig';
Enter fullscreen mode Exit fullscreen mode

Afterwards, let's setup a state for an email and a password and a function to register:

const [email, setEmail] = useState('');
const [password, setPassword] = useState('');

const onSubmit = () => {
    fb.auth()
      .createUserWithEmailAndPassword(email, password)
      .then(() => {
        console.log("route to vue app.");
        window.history.pushState(null, "/vueapp", "/vueapp");
      })
      .catch((error) => {
        console.log(error);
      });
  };
Enter fullscreen mode Exit fullscreen mode

Now we'll edit the html code to show 2 inputs and a button:

 <div className="app-main">
      <h1>React App</h1>
      <label for="email">Email</label>
      <input
        name="email"
        type="text"
        value={email}
        onChange={(event) => setEmail(event.target.value)}
      ></input>
      <label for="password">Password</label>
      <input
        name="password"
        type="password"
        value={password}
        onChange={(event) => setPassword(event.target.value)}
      ></input>

      <button onClick={onSubmit}>Register</button>
    </div>
Enter fullscreen mode Exit fullscreen mode

That should be it for the reactapp micro application.

STEP 2 - Vue Micro App

After signing up we want to redirect the user to our vue micro app. In that application we'll show the user's email.
Navigate to the vueapp folder.

Now we only need to do 5 things:

  • Create a vue.config.js file
  • Add a public-path.js file
  • Edit the main.js file
  • Configure Firebase
  • Edit the Home.vue

vue.config.js

Let's create vue.config.js in our root folder of the vue micro app. The public path should be the name of your micro app. In our case, we'll put /vueapp.

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

module.exports = {
  publicPath: '/vueapp',
  chainWebpack: config => config.resolve.symlinks(false),
  configureWebpack: {
    output: {
      library: `${name}-[name]`,
      libraryTarget: 'umd',
      jsonpFunction: `webpackJsonp_${name}`
    }
  },
  devServer: {
    port: process.env.VUE_APP_PORT,
    headers: {
      'Access-Control-Allow-Origin': '*'
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

public-path.js

In the src folder of vueapp, we'll specify the public path using the following code:

(function() {
  if (window.__POWERED_BY_QIANKUN__) {
    if (process.env.NODE_ENV === 'development') {
      __webpack_public_path__ = `//localhost:${process.env.VUE_APP_PORT}${process.env.BASE_URL}`;
      return;
    }
    __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
  }
})();
Enter fullscreen mode Exit fullscreen mode

main.js

We need to setup the 3 things that a micro app always needs. So to do that, we need to remove:

const router = new VueRouter({
  base: window.__POWERED_BY_QIANKUN__ ? routerBase : process.env.BASE_URL,
  mode: 'history',
  routes
});

new Vue({
  render: h => h(App),
}).$mount('#app')
Enter fullscreen mode Exit fullscreen mode

and add:

let instance = null;

function render(props = {}) {
  const { container, routerBase } = props;
  const router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN__ ? routerBase : process.env.BASE_URL,
    mode: 'history',
    routes
  });

  instance = new Vue({
    router,
    render: h => h(App)
  }).$mount(container ? container.querySelector('#app') : '#app');
}

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

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

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

  render(props);
}

export async function unmount() {
  instance.$destroy();
  instance.$el.innerHTML = '';
  instance = null;
}
Enter fullscreen mode Exit fullscreen mode

Firebase

This is pretty much the same as in the reactapp micro application.

We'll create a new folder called Firebase in the vueapp src folder. After that we'll create a new file called firebaseConfig.js with following code:

import firebase from 'firebase/app';
import 'firebase/auth';

const config = {
  apiKey: '<key>',
  authDomain: '<key>',
  projectId: ' <key>',
  storageBucket: '<key>',
  messagingSenderId: '<key>',
  appId: '<key>',
  measurementId: '<key>'
};
var fb = null;
if (firebase.apps.length === 0) {
  fb = firebase.initializeApp(config);
  console.log('Firebase [Vue] Initialized');
} else {
  console.log('Firebase [Vue] is already Initialized');
}

export default fb;
Enter fullscreen mode Exit fullscreen mode

Home.vue

Now we need to configure our Home.vue

First, we'll import the firebase config

import fb from '../firebase/firebaseConfig';
Enter fullscreen mode Exit fullscreen mode

After that we want to add two fields [user, isLoaded].

data() {
    return {
      user: {},
      isLoaded: false
    };
  },
Enter fullscreen mode Exit fullscreen mode

In the created method of Home.vue we are going to add the firebase onAuthStateChanged to wait for the user.

  created() {
    fb.auth().onAuthStateChanged(user => {
  this.user = user;
  this.isLoaded = true;
});
  }
Enter fullscreen mode Exit fullscreen mode

The only thing left to do is edit the template tag to show the email.

<template>
 <div>
    <h1>Vue App</h1>
    <h1 v-if="isLoaded">Welcome! {{ user.email }}</h1>
    <h6 v-if="!isLoaded">Loading...</h6>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode

Quick Note
You have to specify a port in a .env file, so create one in the root of the vue
micro application add the following line:

VUE_APP_PORT=7777
Enter fullscreen mode Exit fullscreen mode

That's it for the vue micro app!

STEP 3 - Main Shell

When working with a Micro Frontend Architecture, we need to have one main-shell that will be used to show our other micro applications. Our main shell will be written in Vue.

Creation

Navigate to the main-shell folder.

We will be using the package Qiankun
(This is a micro frontend framework)

Now we only need to do 4 things:

  • Create a vue.config.js file
  • Add a micro-apps.js file
  • Edit the main.js file
  • Edit the App.vue

vue.config.js

In the root folder of the main-shell, we'll create a new file called vue.config.js. In that file we'll add the following code:

module.exports = {
  configureWebpack: {
    module: {
      rules: [
        {
          test: /\.(ttf|otf|eot|woff|woff2)$/,
          use: {
            loader: 'file-loader',
            options: {
              name: 'fonts/[name].[ext]'
            }
          }
        }
      ]
    }
  }
};
Enter fullscreen mode Exit fullscreen mode

This will enable custom fonts in your application.

micro-apps.js

In the src folder of the main-shell, we'll create a new file called micro-apps.js. This file will be used to define our micro applications.

// This is where we define our micro applications
const microApps = [
  {
    name: 'reactapp',
    entry: '//localhost:7100',
    activeRule: '/reactapp'
  },
  {
    name: 'vue',
    entry: '//localhost:7777/vueapp',
    activeRule: '/vueapp'
  }
];

const apps = microApps.map((item) => {
  return {
    ...item,
    // this will be the element where the micro application will be in
    container: "#subapp-viewport",
    props: {
      routerBase: item.activeRule,
    },
  };
});

export default apps;
Enter fullscreen mode Exit fullscreen mode

main.js

This is where the fun begins! πŸ˜„πŸ˜„

First, we need to import methods from Qiankun and the micro-app.js into the main.js file.

import { registerMicroApps, start, setDefaultMountApp } from "qiankun";
import microApps from "./micro-apps";
Enter fullscreen mode Exit fullscreen mode

Secondly, we need to register our micro apps and set a default route. After that we just need to run the start method. Add the following code at the end of the file.

const apps = microApps.map((item) => {
  return {
    ...item,
  };
});

registerMicroApps(apps, {
  beforeLoad: (app) => {
    console.log("before load app.name====>>>>>", app.name);
  },
  beforeMount: [
    (app) => {
      console.log("[LifeCycle] before mount %c%s", "color: green;", app.name);
    },
  ],
  afterMount: [
    (app) => {
      console.log("[LifeCycle] after mount %c%s", "color: green;", app.name);
    },
  ],
  afterUnmount: [
    (app) => {
      console.log("[LifeCycle] after unmount %c%s", "color: green;", app.name);
    },
  ],
});
setDefaultMountApp("/reactapp");
start();
Enter fullscreen mode Exit fullscreen mode

App.vue

The last one!

First, let's change the HTML code.
Change the HTML to this:

<template>
  <div id="subapp-viewport"></div>
</template>
Enter fullscreen mode Exit fullscreen mode

This will put our current micro application inside the div element.

Next up, the script tag in App.vue will have 2 methods. We also need to import micro-app.js again to bind it to the current route and return some fields to our data function.

import microApps from "./micro-apps";
Enter fullscreen mode Exit fullscreen mode
  data() {
    return {
      microApps,
      current: "/reactapp",
    };
  },
Enter fullscreen mode Exit fullscreen mode

Two methods are needed. The first one binds the current path to the current Path field in the App.vue. The second method will listen to any router changes.

  methods: {
    // Binds the current path to the current Path in the main shell
    bindCurrent() {
      const path = window.location.pathname;
      if (this.microApps.findIndex((item) => item.activeRule === path) >= 0) {
        this.current = path;
      }
    },
    // Will listen to any router changes
    listenRouterChange() {
      const _wr = function (type) {
        const orig = history[type];
        return function () {
          const rv = orig.apply(this, arguments);
          const e = new Event(type);
          e.arguments = arguments;
          window.dispatchEvent(e);
          return rv;
        };
      };
      history.pushState = _wr("pushState");

      // will listen when navigating to another micro application
      window.addEventListener("pushState", this.bindCurrent);
      window.addEventListener("popstate", this.bindCurrent);

      this.$once("hook:beforeDestroy", () => {
        window.removeEventListener("pushState", this.bindCurrent);
        window.removeEventListener("popstate", this.bindCurrent);
      });
    },
  },
Enter fullscreen mode Exit fullscreen mode

Lastly, we need to call our bindCurrent method in created and the
listenRouterChange method in mounted:

  created() {
    this.bindCurrent();
  },
  mounted() {
    this.listenRouterChange();
  },
Enter fullscreen mode Exit fullscreen mode

That's it!

Now to see it in action. Run the main-shell and the 2 micro applications with the command npm run start.
Navigate to http://localhost:8080/ where you should be seeing this:
Tutorial

You can find the complete code for this tutorial on the completed/tutorial branch of the starters Repo

I hope you find this tutorial somewhat useful. I apologize for my writing skills as I am still learning on how to explain things in a better way πŸ˜…πŸ˜….

I recommend reading this medium article If you are interested in learning more about micro frontends!

Thank you! 😁

Top comments (2)

Collapse
 
manueldev profile image
ManuelDev

Hey dude, nice article. Just add dont forget run a command to install firebase as dependency inside of vueapp directory. This could generate unexpected build errors ✌

Collapse
 
issamelnass profile image
Issam El Nasiri

I will and Thank you!!