TLDR; To choose a strategy for implementing micro frontends with SPA, you need to know your architectural goals, prioritize them and evaluate them against the options available. This article does this for some common goals and presents a matrix which reflects the results of the evaluation. To provide some orientation, you also might find this decision tree useful:
People ask me on regular basis how to use SPAs and/or Angular in an microservice-based environment. The need for such microfrontends is no surprise, as microservices are quite popular nowadays. The underlying idea of microservices is quite simple: Create several tiny applications -- so called microservices -- instead of one big monolytic applications. This leads for instance (but not only) to smaller teams (per microservice) that can make decisions faster and chose for the "best" technology that suites their needs.
But when we want to use several microservices that form a bigger software system in the browser, we need a way to load them side by side and to isolate them from each other so that they cannot interact in an unplanned manner. The fact that each team can use different frameworks in different versions brings additional complexity into play.
Fortunately, there are several approaches for this. Unfortunately, no approach is perfect -- each of them has it's own pros and cons.
To decide for one, a software architect would evaluate those so called architectural candidates against the architectural goals given for the software system in question. Typical (but not the only) goals for SPAs in microservice-based environments are shown in the next section.
|a) Isolation||Can the clients influence each other in an unplanned way?|
|b) Separate Deployment||Can the microservices be deployed separately without the need to coordinate with other teams responsible for other microservices?|
|c) Single Page Shell||Is the shell, composing the loaded microfrontends a SPA -- or does it at least feel like one for the user (no postbacks, deep linking, holding state)|
|d) Different SPA-Frameworks||Can we use different SPA frameworks (or libraries) in different versions|
|e) Tree Shaking||Can we make use of tree shaking?|
|f) Vendor Bundles||Can we reuse already loaded vendor bundles or do we need to load the same framework several times, if it's used by several microfrontends|
|g) Several microfrontends at the same time||Can we display several microfrontends at the same time, e. g. a product list and a shopping basket|
|h) Prevents version conflicts||Does the approach prevent version conflicts between used libraries?|
|i) Separate development||Can separate teams develop their microfrontends independently of other ones|
|j) One optimized solution (bundle)||Everything is compiled into one optimized solution. You don't have to duplicate libraries or frameworks for different parts of the system.|
Normally, you would break those abstract goals down to concrete, measurable goals for you project. As this is not possible in an overview like this, I'm sticking with those abstract ones.
The following table evaluates some architectural candidates for microfrontends against the discussed goals (a - g).
|I) Just using Hyperlinks||x||x||x||x||x||x|
|II) Using iframes||x||x||x||x||x||x||x||x|
|III) Loading different SPAs into the same page||S||x||x||x||x||x||x|
|IV) Plugins *||S||x||x||x||x|
|V) Deployment Monolith using Packages (npm, etc.) *||S||x||x||x||x||x|
|VI) Deployment Monolith using Monorepos * **||S||x||x||x||x||x||x|
|VII) Web Components||S||x||x||x||x||x||x|
x means that the goal in question is supported. The
S in the column
Isolation means that you can leverage Shadow DOM as well as EcmaScript Modules to archive some amount of isolation.
* This dosn't lead to a typical microservice-based solution. But as the main goal is to reach defined goals, it would be wrong to don't consider these options.
** You can use monorepos with different applications. In this case you would use one of the other integration strategies outlined here.
If you are interested into some of those candidates, the next table provides some additional thoughts on them:
|I) Just using Hyperlinks||We could save the state before navigating to another microfrontend. Using something like Redux (@ngrx/store) could come in handy b/c it manages the state centrally. Also consider that the user can open several microservices in several browser windows/ tabs.|
|II) Using iframes||We need something like a meta router that synchronizes the url with the iframes ones. We also need to solve some issues like resizing the iframes to prevent scrolling within them and elements cannot overlap their borders. Also, iframes are not the most popular feature 😉|
|III) Loading different SPAs into the same page||A popular framework that loads several SPAs into the browser is Single SPA. The main drawback seems to be the lack of isolation, cause all applications share the same global namespace and the same global browser objects. If the latter ones are monkey patched by a framework (like zone.js) this affects all the loaded SPAs|
|IV) Plugins||Dynamically loading parts of a SPA can be done with Angular but webpack and so the CLI demands on compiling everything together. Switching to SystemJS would allow to load parts that have been compiled separately|
|V) Packages||This means, providing each frontend as a package via (a private) npm registry or the monorepo approach and consuming it in a shell application. This also means, that there is one compilation step that goes through each frontend.|
|VI) Deployment Monolith||To structure your deployment monolith into parts which can be developed and tested seperately, you can e. g. use npm packages or a monorepo.|
|VII) Web Components||This is similar to III) but Web Components seem to be a good fit here, b/c they can be used with any framework - at least, in theory. They also provide a bit of isolation when it comes to rendering and CSS due to the usage of Shadow DOM. Angular Elements come very in handy for this. The idea is to compile Angular Components down to Web Components.|
To one thing clear: There is no perfect solution and it really depends on your current situation and upon how important the architectural goals are for you. E. g. I've seen many team writing successful applications leveraging libraries and at companies like Google and Facebook there is a long tradition of using monorepos. Also, I expect that Web Components will be used more and more due to the growing framework and browser support.
One way to work with the presented matrix is to extend it with all the other goals you have and evaluate it against your specific situation. Even though this seems to be straight forward, in practice it can be quite difficult. That's why I've decided to give you some (biased) guidance by pointing out the strength of the approaches presented:
|If ...||Try ...|
|1) You need just use one microfrontend at one time and have little/ no communication on UI level between them||Hyperlinks|
|2) Otherwise: Legacy and need for very strong isolation||iframes|
|3) Otherwise: One optimized and fully integrated UI, just one technology and no separate deployment||Deployment monolith using mono repos|
|4) Otherwise||Bootstrap several SPAs in one browser window, use Shadow DOM for Isolation, consider Web Components|
If you don't have a legacy system and you've already decided to go with microservices, you should consider 1) and 4) in this very order.
Distinguish between Macro and Micro Architecture
When choosing a solution, please keep in mind that you need solutions for two different architecture levels. I'm calling them the macro and the micro architecture:
While the macro architecture is the glue between your micro apps, the micro architecture is the architecture within them. For me, this seperation turned out to be in handy, because you can choose for different solutions on those two levels. While this picture uses web components for both, one could also decide for using iframes for the macro architecture and Angular packages for the micro architecture. By seperating those levels you make sure that you don't mix several aspects when discussing about your target architecture.
It's not an "either/or thing"!
Mixing those approaches can also be a good idea. For instance, you could go with Hyperlinks for the general routing and when you have to display widgets from one microfrontend within an other one, you could choose for libraries or web components.