Building an Embeddable Micro-Frontend With Vue.js

As a lover of buzzwords and small things, I only had to read one article about micro-frontends before reserving them a spot at the top of my coding wishlist. If you haven’t come across the term yet, the concept is easy to explain: like microservices (which you might remember from my earlier article) are small backend components you piece together to build an application, micro-frontends are small JavaScript components you piece together to build a frontend. And just like with their backend counterparts, the advantage of micro-frontends is that each has a clear API, a single owning team, its own dependencies, and can be reused.

The first opportunity I saw to use a micro-frontend came a couple months ago with a new compliance requirement. Enova needed to allow certain customers to request access to or remove their personal data, which involved a new UI flow that two of our brands’ frontends would need to implement. At first, we planned on extracting this UI flow to its own webpage, but then we decided to take things a step further and turn it into a micro-frontend that the brands could easily drop-in to their pages. After all, the shared back-end logic was being put into a new microservice — why not do the same with the front-end?

In the rest of this post, I’ll explain how we built out this “embeddable micro-frontend” and how you could do the same.

The Stack

At Enova, our preferred JavaScript framework is Vue.js. We like it because it takes the same reactive, component-based approach as React, but with a little more magic and structure to streamline development. You could pick any framework, though, and adapt the Vue-specific pieces of this post to fit.

So, I picked the latest Vue.js (2.6), created a blank app through vue-cli, and got to work.

The Approach

We want to let our different brands/sites simply drop-in this micro-frontend, configure it, and be done. The best approach for micro-frontends is to use web components, since they exactly fit our needs, but unfortunately aren’t fully supported by browsers — yet. Since we needed our UI to work anywhere, we went with the old-school option of a script tag.

<script type="text/javascript" 
  src="https://my-microfrontend-ui.enova.com/app.js"
></script>

The next piece is configuration. This could be done through another script that the brands write, to invoke some API with values, but to make things even easier, we decided to roll it into data attributes on the script tag itself:

<script type="text/javascript"
  src="https://my-microfrontend-ui.enova.com/app.js"
  data-api-url="/callback/url"
  data-brand=”netcredit”
></script>

To make this actually work with a Vue app, though, we need two things to be true:

  1. The JS file has to include everything our app needs, which means we need to look at our Webpack configuration
  2. Our Vue app’s main.js needs to be able to render itself where the script tag was included, which means we’ll need some custom glue code

The Webpack Configuration

One nice thing about Vue.js is that it wraps most of the config you need to worry about into a babel preset, which you can manipulate through a simpler vue.config.js without touching your raw webpack config. For getting everything down into one file, though, we’ll need to pull back the curtain a bit to do three things:

  • Disable hashing for the filename, so it’s always just app.js and not app-a3b3fe.js
  • Set webpack’s chunking limit to 1, so the file doesn’t get split up at any size
  • Compile all CSS inside the app.js file, not within a separate app.css 

Here’s the vue.config.js we ended up with:

const webpack = require("webpack");

module.exports = {
  filenameHashing: false,
  configureWebpack: {
    plugins: [
      new webpack.optimize.LimitChunkCountPlugin({
        maxChunks: 1
      })
    ]
  },
  css: {
    extract: false
  }
}

The Mounting Code

This replaces the stock main.js generated by vue-cli:

import Vue from "vue";
import App from "./App.vue";

import "./stylesheets/theme.less";

// when the script tag is loaded, it's the last script 
// on the page, so grab it and pull data off of it.
const scriptTags = document.querySelectorAll("script");
const parent = scriptTags[scriptTags.length - 1];

var {
  brand,
  apiUrl,
} = parent.dataset;

// write a mount point to the DOM
document.write(`<div id="mount-vue-my-microfrontend"></div>`);

// render the Vue app
new Vue({
  render: h =>
    h(App, {
      props: { brand, apiUrl }
    })
}).$mount("#mount-vue-my-microfrontend");

Communicating Upwards

What we’ve got so far is great, so long as your micro-frontend is fully independent. Chances are, though, that you’ll need to send some information back up. In the example code above, I’m passing an “api-url” into the script tag, which the micro-frontend sends its XHR requests to, but you might want to send information (like the form being completed successfully) back to the frontend itself. The best way to do this, following the micro-frontends principles, is through standard DOM events.

That is, our embedded widget will emit an event when things happen within it, and the embedding site can define listeners on those events. The only caveat is that the DOM event API requires an element to trigger the event from, so we need to use a ref to call it:

// in the micro-frontend, when the form is complete:
event = new Event("microfrontend:form:complete");
this.$refs.rootDiv.dispatchEvent(event);

// in the embedding site:
window.addEventListener(
  "microfrontend:form:complete",
  function() {
    alert("and we’re done!");
  }
);

And That’s a Wrap

In the end, we found that the micro-frontend approach was great for this use case. It allowed two brand teams to focus only on the backend logic while another team built the full UI implementation — with all the possible error states and edge cases — just once. Our situation might be rare, but there are other benefits to micro-frontends:

  • Dependency freedom: if your frontend is aging and you’re looking to try out something new, your micro-frontend can be in any tech stack you want.
  • Isolation: your new components will have clear dependencies and limited scope, making them easy to test.
  • Ownership: if you have a big team, letting individual teams own pieces of the website — and deploy at their own pace — might boost your productivity significantly.

I hope this gave you something to think about! If you find topics like this interesting and are looking for a job where you can work with new tech in an exciting and fast-paced environment, Enova is always hiring.