Independent micro frontends with Single SPA library

July 30, 2018 0 Comments

Independent micro frontends with Single SPA library

 

 

It is a concept of designing large web applications as a composition of small sub-applications. The idea is very similar to micro services. The difference is that micro services are independent backend services, but micro frontends, as the name suggests, are independent frontend components. If we combine these two architectural patterns together, we can design self-contained applications. It means that we can split large system vertically into separate parts, where in ideal world each part has its own frontend, backend services and database if needed. It sounds like a perfect way to escape monolithical UI, in favour of independent and scalable components!

Let’s assume that we want to implement an online shop. It should have a list of available products. A customer can add products to a cart. From the cart level, it should be possible to place an order. Optionally, a recommendation system could suggest products that could potentially interest the customer.

There are two ways of positioning sub-applications on a web page:

  • one application per page — whenever the URL changes, a new application is loaded
Single Angular application on a single page serving products
  • many applications per page — a single URL gives access to many small applications displayed on a single page
3 applications on a single page serving online shop

Each application frontend might be served from distinct locations and has its own API. In order to implement it, we need to have a template, which renders the application in a specific screen position. We can do it in two ways: either server-side or client-side.

For server-side, we could use:

For client-side, we could use:

Both server-side and client-side have their own pros and cons, but I would like to focus on client-side.

  • Applications are self-contained — it means higher cohesion, since each application only has a single responsibility.
  • Applications can be developed independently — developers can focus on their work and deliver business value; no technical synchronization with other teams is needed.
  • Applications can be deployed independently — it’s a freedom of choice when the application is deployed, with no negative impact on other parts of the system.
  • Applications can be implemented in different technologies — in the world of rapid evolution of frontend technologies, it is impossible to choose an ideal JS framework, which wouldn’t be considered as legacy in the next two years. Wouldn’t it be great if we could write code using a new framework, without rewriting the existing system? With micro frontends, we can combine many frameworks. The extra benefit is that we could choose technologies that we like or that are matched to the skills of our team!
  • Consistent look and feel. How can we enforce it? We can use a shared CSS stylesheet, but it means that all applications would depend on one common resource. Is there a better approach? The answer is yes and no. There is no perfect solution, but I would recommend using separate stylesheets for each application. Redundancy causes the user to fetch more data, thus impacting application load-time. Additionally, components would have to be implemented at least once, which impacts development cost and consistency. The benefit of this approach is independence. This way, we can avoid teams’ synchronization problems during development and deployment. Having a common style guide, designed for example in Zeplin, helps to keep the look and feel consistent (but not identical) across the whole system. Alternatively, we could use a common component library included by each application. The disadvantage of this solution is that whenever someone changes the library, they have to ensure that they do not break dependent applications. It would introduce a huge inertia. Moreover, a library in most cases can only be used by a single framework. There is no easy way to implement a UI components library, that could be used by the Angular and React app.
  • Slow loading. If we are going to use at least two JS frameworks (for example, 2 applications use Angular and 1 uses React) then our web browser has to fetch a lot of data.
  • Overall complexity. How to orchestrate and combine applications created in different technologies and by separate teams, into a single product?

What we would like to achieve:

  • create portal-like application with top-header and left-side navigation bar,
  • navigation bar has links that open sub-applications,
  • each sub-application can be implemented in different technology (like Angular 5, React 15 and React 16),
  • we can render a single sub-application on a single page,
  • we can render many sub-applications on a single page,
  • each sub-application can be deployed independently and hosted on different servers,
  • CSS stylesheets are independent,
  • communication between sub-applications is possible.

The Single SPA library is something that could be very helpful to bootstrap the project. It has features like:

  • use of multiple frameworks on the same page without refreshing the page,
  • lazy load code for improved initial load-time,
  • top-level routing.

It sounds amazing! Let’s go ahead!

We are going to create 3 modules:

  • sub-app-angular — Angular 5 application,
  • sub-app-react16 — React 16 application,
  • main-app — React 15 application; it is a template module, which aggregates sub-app-angular and sub-app-react16 as a “portal” application.

We can use create-react-app to create new React sub-app-react16 and main-app applications and Angular CLI to create sub-app-angular. Thanks to these starters, we are able to skip time-consuming setup of new projects.

We would like to have 3 running servers:

It means that whenever we decide that Angular application has to be updated (because of a bug fix), we just have to build new bundles and copy them to a folder which is served on http://localhost:3001.

In order to export a module as a sub-application, we have to export 3 lifecycle functions from the entry point JS file:

  • bootstrap — will be called once, right before the registered application is mounted for the first time,
  • mount — will be called whenever the registered application is mounted,
  • unmount — will be called whenever the registered application is unmounted.

Fortunately, we don’t have to implement these functions manually, since Single SPA provides default implementation for the most common JS frameworks. What we have to do is to include single-spa-react and single-spa-angular2 libraries in the package.json file and write a piece of code.

A React application entry point file could look like this:

Entry point of React 16 application

We have to provide the following properties to single-spa-react:

  • rootComponent — the root component used to render the React application,
  • domElementGetter — function that returns DOM element where the application would be rendered.

An Angular application entry point file could look like this:

Entry point of Angular 5 application

We have to provide the following properties to single-spa-angular2:

  • mainModule — the application root Angular module,
  • domElementGetter — function that returns DOM element where the application would be rendered,
  • template — Angular template.

You might wonder how the main-app application would find bootstrap, mount and unmount functions after fetching them from servers. The answer is that we have to make some assumptions. We assume, that after all Angular application bundles would be fetched and executed, the functions would be accessible in window.angularApp object. Similarly, for the React application, we expect to have these functions in the window.reactApp object. It means that both the sub-application and the template need to know the location in a global namespace of Single SPA lifecycle functions. We can call it a contract between the sub-application and its container.

How can we enforce the target location of these functions? We can use Webpack of course! In order to do it, we have to add 2 entries to module.exports.output object in the Webpack configuration file for each sub-application.

For the React application, it could look like this:

Export React application as window.reactApp

For the Angular application, it could look like this:

Export Angular application as window.angularApp

Cool! Now we are ready to configure a template project — the main-app. What we are going to do is to render:

  • sub-app-react16 for /react or / in address bar
  • sub-app-angular for /angular or / in address bar

We have to prepare DOM placeholder elements for sub-applications:

Sub-applications container

Next, we have to add the single-spa library to the package.json file.

Now we are ready to register sub-applications in the Single SPA library. The React sub-application is registered by registerReactApp function call, the Angular sub-application is registered by registerAngularApp function call. Finally, we have to start the Single SPA library.

main-app entry file

The registerReactApp looks like this:

React application registration

The registerAngularApp looks like this:

Angular application registration

The registerApplication method of singleSpa requires:

  • logical name of the application to register,
  • application loader, which is a function returning Promise that resolves to bootstrap, mount and unmount functions provided by sub-applications,
  • a function (predicate), that accepts window.location as the first argument, and returns true whenever the application should be active for a given URL.

We need some utilities:

The runScript function fetches the external JS script file and runs it by adding a new script element to the document.

You might notice the second contract between the sub-application and the template project: both the sub-application and the template need to know the DOM element ID of the sub-application container.

Now let’s see what we’ve achieved:

It works perfectly!

In an ideal world, all sub-applications should be independent. It means that there should be no CSS selector provided, which would impact other sub-applications, for example: button, h1 etc.

Because our test project uses PostCSS, we could leverage plugins that it provides. One of them is postcss-wrap. It wraps CSS rules in a namespace, e.g. .selector{} is converted into .namespace .selector{}. In our case, each sub-application might have its own namespace, restricted to its container identifier.

This means that for the React application, which is rendered in the DOM element with ID react-app, all CSS selectors could be prefixed with #react-app.

In order to do it, we have to add the postcss-wrap library to the React application dependencies and then configure it in the Webpack configuration file. It could look like this:

Example of postcss-wrap Webpack config

Finally, you can see that the Bootstrap stylesheet has a scope restricted to the React application only:

CSS selectors with namespace

It’s a really straightforward and fully functioning solution!

The last thing missing is inter-application communication. Applications are in theory independent, but they could react to some events sent by other applications. Because we talk about events, as opposed to direct synchronous calls, it does not introduce direct coupling between applications.

As you might notice, our React 16 application displays current time. What we could implement is an event sent from the Angular application, which stops or starts the clock.

How can we send events between applications? The easiest way is to use a native web browser event mechanism, which does not require any additional libraries.

We can send an event from the Angular application like this:

Sending toggleClock event

The recipient of this event is the Clock component inside the React application. It could subscribe on application mount and unsubscribe on unmount:

Now the communication is ready. Very simple code!

What we’ve verified is that implementation based on the client-side rendering, supported by the Single SPA library, might be a working choice. There are probably still some latent issues.

What needs investigation:

  • Would it work well if the same library (for example Lodash), but with different versions, is used by applications?
  • Is there any way to unload JS code of a no-longer-used application?

What can be improved:

  • Bundles caching. It doesn’t make any sense to download a codebase that wasn’t changed since its last use.
  • Decrease coupling between the template application and sub-applications. Currently, the main-app has hardcoded a list of bundles of each sub-application. This list could be moved to the sub-application as a manifest file and exposed via public URL. It means that each sub-application would tell the template application exactly what has to be fetched in order to run the application. The template application would know only the URL to the manifest file for each application.

A code of the Proof of Concept is available at Github: https://github.com/Pragmatists/microfrontends. It’s not production-ready, but the idea was to validate our assumptions and dispel our doubts in a one-day hackathon.

I hope you enjoyed this article! See you soon.


Tag cloud