Setup a Micro Frontend Architecture With Vue and single-spa

Lawrence B. Almeida
ITNEXT
Published in
10 min readAug 26, 2020

--

A practical walkthrough on building a micro frontend architecture with multiple Vue.js apps using single-spa. Note that you can replace Vue with your framework of choice.

Listen to Views on Vue’s episode 160 where I discuss the details of how we implemented this architecture at Unbabel.

The first article of a three part guide that aims at laying the foundations on how to setup and organize a micro frontend architecture for your projects using single-spa.

Part 1 — setup the orchestrator layer which loads the necessary assets, 3rd party libraries and our micro applications;

Part 2 (TBD) — organizing common styles and reusable components; sharing and manipulating state between apps;

Part 3 (TBD) — deploying to Netlify while avoiding concurrency;

What are Micro Frontends?

The concept of micro frontends has been around for a while now but has been getting more attention in the past couple of years.

Evolution of the search term “Micro Frontend” on Google Trends, 2015–2020

Micro frontends extend the concept of backend micro-services: breaking down a web app (a monolith SPA) into distinct pieces. Then, through an orchestrator layer, each part is assembled (or composed) together. There are multiple ways of doing so. In our case we will do what is called client-side composition using single-spa.

When should I use this approach?

At Unbabel we currently use a micro frontend architecture for one of our new customer-facing product.

Like any technical decision, there are tradeoffs. We weighted a couple of factors when deciding for this approach:

  • The product to build would be comprised of at least 6 distinct areas, i.e. interfaces;
  • Several multi-disciplinary teams would own and have full autonomy on delivering parts of the product;
  • Have the possibility of partially changing parts of the product’s stack. Although Vue is the company’s framework of choice, we don’t know if it’ll still be what we want to use 3 years from now;

You should consider these advantages:

  • Teams have a greater autonomy on delivering value into the product at different paces as their development can be largely independent from other teams;
  • Ability to have totally separate repositories, test and deployment flows;
  • Ability to easily override parts of an application’s interface (i.e. A/B testing, incremental rollout);
  • Use different frameworks side-by-side or perform experiments without affecting other parts of your application;
  • Ability to refactor parts of your product without having to change it all at once;

You should consider these caveats:

  • Increased overhead to set up, deploy and maintain depending on the scope and characteristics of the product you’re building;
  • More moving parts: it’s important to have a solid documentation on how everything is setup and relates as well as defining guidelines that govern how development is to be done within the architecture;
  • Steeper learning for developers to understand the architecture, its lifecycle and dependencies. Hence having thorough documentation is imperative;
  • Micro frontends are still relatively new and there is no one-size-fits-all methodology or well established consensus on how to achieve this. Be ready to do a fair amount of R&D depending on your case;

In my experience, this approach is best when building a relatively large web app where you want to provide flexibility to multiple teams and have enough time to dedicate to governance and documentation.

Having said that, you can definitely leverage on many of the micro frontend advantages with a team of 2-3 people or even alone.

Architecture Diagram

A diagram demonstrating how our architecture relates.
In a nutshell: vendor dependencies get loaded from a CDN, app 1 and 2 bundles get loaded from S3/GCS and our orchestrator composes/bundles it all together.

Building the Orchestrator App

The orchestrator app is nothing but the project holding single-spa which is responsible for determining which applications get loaded depending on their activation function (more on that later).

This walkthrough follows single-spa’s recommended setup. You can also check the vue-microfrontends example repo.

Create project

  1. Create a parent folder that will hold all of your project folders and cd into it;
  2. Create a folder named orchestrator and cd into it;
  3. Run npm init to create an empty package.json — when asked for the entry file name, set it as main.js;
  4. Modify the package.json’s scripts:
...
"scripts": {
"serve": "webpack-dev-server --mode=development --env.isLocal=true",
"lint": "eslint src",
"prettier": "prettier --write './**'",
"build": "webpack --mode=production"
},
...

5. Install the required dependencies:

npm i -D @babel/core @babel/preset-env @types/systemjs babel-eslint babel-loader clean-webpack-plugin concurrently eslint eslint-config-important-stuff eslint-config-prettier eslint-plugin-prettier html-webpack-plugin mini-css-extract-plugin prettier pretty-quick webpack webpack-cli webpack-dev-server dotenv-webpack

Add index.ejs

This is what our users will hit when visiting our application. It uses EJS to easily generate the HTML markup on build time.

On the root of the Orchestrator folder, create an index.ejs file and add the following content:

What’s happening here?

  1. We want to make use of “bare import specifiers” in the browser through import maps. Because the import map specification is only implemented in Chrome, we’ll use SystemJS to load our import map (line 9) and import the modules we want (line 25);
  2. Lines 12–13 allow us to load additional modules or override modules defined in importmap.json;
  3. Line 25 loads our project’s entry point (main.js);
  4. Because SystemJS is a dynamic JS module loader and by using import-map-overrides (line 27), we’re able to dynamically reload modules on-the-fly (more on that later);
  5. The style tag is temporary for now, simply used to style the Snackbar we’ll use in part 2 of the walkthrough;

Using this approach of browser ES Modules + import-maps has several advantages, but mainly, it allows us to only load once the dependencies shared across micro apps (ex. Vue) and easily share common code between apps.

Add importmap.json

Create a file in the project’s root named importmap.json and add the following content:

This file determines the location of the imports we want to do in-browser. For now we’re only loading libraries we need for our architecture, but it will also point to the locations of our applications once these are made available. We’re loading it locally for now, but this JSON file should also be served independently, allowing you to change it separately from any CI/CD process. For instance, you can use jsonbin.io to host JSON (for tests only) and easily change its content.

Besides the fundamental dependencies (single-spa, vue & vue-router), we’re also importing two libraries we’ll use in the 2nd part of this walkthrough:

  • pubsub-js, a publish/subscribe pattern implementation that will act as an event bus;
  • snackbar, a Material style notification toast;

Add local-importmap.json

Having a local import map is crucial for local development, otherwise we’d need to spin every micro frontend locally in order to have the whole architecture run properly. local-importmap.json allows us to add and override anything defined in importmap.json (i.e. override an app in staging or production or load a different version of Vue).

For now we’ll only define the entry point for our Orchestrator app running locally:

Add this file to your .gitignore, allowing team members to import different apps depending on what they’re working on without affecting other’s imports.

Add webpack.config.js

Create a webpack.config.js in the project’s root and add the following content to it:

Add .env

Creaste a .env file with the content:

NODE_ENV=development

Add main.js

Add a main.js file with the following content:

For now the entry point is pretty simple: it imports single-spa, waits for all modules to be imported (pubsub-js & snackbar) and once loaded, starts single-spa and displays a success toast. We’ll add more to this file as we evolve the architecture.

You can now run npm run serve and visit http://localhost:5000. If all goes well, you should see a toast on the bottom left corner saying “Single SPA loaded”.

We now have the basic setup that we’ll use to load and orchestrate the micro frontends we’ll build.

The orchestrator app should look like this.

Create the first Vue App

Our Vue app will need to suffer a couple of minor modifications in order to be registrable with single-spa. Luckily, single-spa allows to easily integrate with all major frameworks.

In our case, we’ll use the vue-cli-plugin-single-spa which performs those changes for us.

  1. Within the parent folder, create a Vue app named app-one:
vue create app-one

2. cd into app-one and install the single-spa Vue plugin which will perform the changes as described here:

vue add single-spa

3. Install webpack’s EventHooksPlugin:

npm i -D event-hooks-webpack-plugin

4. Cleanup the boilerplate code from Vue by deleting the components, assets and public folders, and modify App.vue like this:

5. Create a vue.config.js and add the following content:

A couple of things to note here:

  • We’re telling webpack that we want the output to be done at the root of the bundle folder (line 17);
  • We’re telling webpack not to include some dependencies in the final bundle through config.externals, as Vue and vue-router will be provided by the Orchestrator app;
  • We’re not hashing file names;
  • We’re removing index.html from the dist built folder as it’s not necessary;

App-one should now be setup like shown in this branch. Let’s spin it up and make sure its entry file is accessible.

Run npm run serve and visit http://localhost:8080/app.js. You should be able to see the compiled Javascript.

Register the app with single-spa

We now need to tell single-spa we want to register an app in order to mount it within the Orchestrator app. Within the Orchestrator app:

  1. Add the module entry to local-importmap.json so that SystemJS can import it:

Note that the key value “app-one” must be the same used by the app’s setPublicPath function located in src/set-public-path.js.

2. Modify main.js so that it looks like this:

Here registerApplication is used to register an app within single-spa.

activeWhen is an important function (activation function) that essentially determines when should this app be mounted. It must return true to mount it. In this example, we simply want the current location to start with “/” but you could verify many additional aspects, such as whether a user has a specific role, device type, etc.

Inspecting the app through Chrome’s Dev tools

Now visit http://localhost:5000 and you should see the text “App One”. If we inspect the browser’s code using developer tools, we can see single-spa is appending our app to the body tag.

You can go ahead and change the content of the H1 tag and verify that the hot code reloading functionality is working as intended.

Create the second Vue App

We’ll now create another Vue app by following the steps 1 through 5 from the previous section, to the exception of these changes:

  1. Name it “app-two”;
  2. Change the port’s number in vue.config.js to 8081 to avoid clashing ports with app-one;
  3. Change the content of the H1 tag in App.vue to “App Two”;

Register app-two

  1. Again, we’ll add the module entry point to our local-importmap.json file which will look like:

2. Now register it in main.js but this time we’ll abstract a bit how apps are registered:

If we now visit http://localhost:5000, we should see both of our apps:

Our two apps shown on top of each other.

At this stage your code should look like this for orchestrator, like this for app-one and like this for app-two.

Controlling the Layout

Notice in the above image our apps are stacked on top of each other.

That’s single-spa’s default behavior. If you only allow to mount one single app per location (i.e. app-one for /foo and app-two for /bar), that shouldn’t be a problem.

But what if you want two apps mounted simultaneously and control how they are positioned? For that, we can define where we want Vue to mount the app, using appOption.el.

Defining the placeholder elements

We’ll be mounting app-one and app-two inside the elements with ids “appOne-placeholder and “appTwo-placeholder respectively. We’ve replaced the style tag with what determines how the placeholders are positioned (we’ll apply the previous styles in a moment).

  1. First we must modify Orchestrator’s index.ejs to have the receiving placeholders for the micro frontends. Modify the file so it looks like:

2. Modify app-one’s main.js like such:

Notice the value of containerSelector (in this case and id attribute) must match that of the placeholder we defined previously. We’re also adding lines 23 through 29 in order to be able to normally use single-spa’s extension highlight functionality.

3. Do the same on app-two’s main.js except change containerSelector’s value to “#appTwo-placeholder”;

As you can see, we’re now able to have the two apps side by side:

Apps mounted in their respective placeholder containers.

At this stage you code should look like this for orchestrator, like this for app-one and like this for app-two. You should now have a pretty good understanding of how to create a micro frontend architecture using single-spa.

But should we place all of our CSS within index.ejs? Should it live inside the orchestrator app? How do applications “share state”? Can I reuse components? The second part of this guide will focus on showing you how to do these and more.

In the meantime…

Reading Material

.. here are some more resources to learn and discover more about micro frontend architecture:

Give this article a 👏 if you found it useful and follow me to get part 2 as soon as it’s out. Thanks for reading!

--

--