360codelab: Casestudy - Microfrontends, a practical piece of a theoretical problem

myWorldMicrofrontends, a practical piece of a theoretical problem

Front-end UI applications today are usually monolithic, and well- this is not really a bad approach, unless...

Unless you must adapt to changing business needs, delegate work to external teams, support old technologies or just work independently in a huge company.

And real independency comes with tools and technologies your team likes to work with. Nobody likes legacy code, right? But how to cope with that when you have to add new functionalities to application made few years ago with frameworks and tools of its own choice?

— 10 min. read by Maciej Kwas

— 05.02.2021

#javascript, #preact, #microfrontends, #ui-architecture

Let's dive in

Introduction

Many of you might ask: why bother with microfrontends? This is totally new architectural approach and perhaps its benefits could be replaced with just proper front-end components codebase. Still, “microfrontend” is rather new term that hasn't yet established its position in the world of frontend development.

So as you might assume there isn't really a lot of articles, helpers, standards and tools, yet, those who work in webdev for a long time really get the idea just when they hear it. In short: Microfrontend is a webapp architecture design where front-end is decomposed into individual, semi-independent “frontend microservices” whose communication is kept on a minimum level.

Here's the case

The problem

Enough theory, here's the real deal we had to handle.

The myWorld frontend team has a lot of work to do and lately with new business needs arising they had to share some of their tasks with external teams- including ours. We were asked to create interactive map with filters and user interactions. Ideally, map should be deployed as some kind of external component, because there are several places on different websites where map shall be embedded. And questions immediately arose:

Question 1

How we are going to deliver the “product”? As a plugin? Universal component? Iframe?

Question 2

Are we able to use any framework and/or build tool to speed up development?

Question 3

How we are going to handle multilingual websites and passing parameters?

Ad. 1, also Ad.2

Well, Iframe comes to mind as a quickest solution. We can prepare independent app with one of our favourite frameworks like react or vue and just embed it wherever. We don't have to worry about current myworld codebase and deployment process. Yet, on the second glance there's something wrong with this approach. So, what really bothers me here:

—   Possible display problems on mobile devices
—   No direct possibility to inherit styles without hacks
—   Clunky way of passing parameters within url's GET attribute
—   Problems with direct communication with parent wrapper
—   Possible problems with warnings and errors reporting (to user and error log service)
—   Unexpected unwanted scroll situations
—   Modal windows will look ridiculous
—   Accesibility concerns
—   Any other iframe issues you may think of

We didn't want to go with direct framework embed, because, although it solves some of iframe issues, in the mean time creates new ones. For example: conflicts with other libraries, dependency chain, global scope pollution, multiple framework versions for different features, bundle size.

To avoid listed issues we could go with super clean, pure javascript approach, it would be trully safe solution, implementation should be as easy, as:

<div id="office-map"></div>
<script src="%APP_HOST%/office-map.js"></script>
<script>new OfficeMap({...config}, '#office-map')</script>

So there is no conflict whatsoever, but also: no help from our favourite tools which is just painful. Rendering methods and DOM helpers would have to be written from ground up, native events handling and all that fun with re-registration when DOM changes, only low level nodes creation available, so lot's of abstraction to do, custom implementation of change detection- in short, reinventing the wheel.

Ad. 3

Our subject from the very start was intended to be used on several websites, which slightly differ in style and perhaps in language. Maybe even different points on the map should be focused, so we have to find a proper way to initiate our component with different starting attributes. And again, passing json config to our constructor smells the best and seemed to be the way we are heading to.

Ok, so at this point we agreed we want some sort of WebComponent even if we directly didn't named it that way. And yay! Both vue and react allow us to wrap whole app inside custom web elements that separates framework itself from any further conflicts and also wraps styles to cut the cascade to the given namespace. Last little issue is bundle size. For sure additional 100-150kb for every component is not a big deal, but it's unnecessary and often redundant. If we only could...

How did we handle that?

The solution

And here comes preact all in white. It's just like react we all love, but it comes with tiny footprint (20kb with babel and webpack) and supports containerization out of the box with official template.

Just a tiny addition to preact config file and we're at home:

...
if (env.isProd) {
  config.output.path = `${config.output.path}/${env.pkg.name}-${env.pkg.version}`;
  config.devtool = false;
  config.output.libraryTarget = 'umd';
}
...

What we did here is a simple versioning system. This way we can bundle new version with new features and don't worry about previous implementations' incompatibilities. Everyone can upgrade just when they need to. Our preact template allow us to build as a npm module as well as a umd script, this way we can easily allow other teams to implement our micro frontend with such piece of code:

<div data-widget-host="office-map">
  <script type="text/props">
    { "title": "Global View" }
  </script>
</div>
<script async src="%HOST%/office-map-0.1.0/bundle.js"></script>

Progressive replacement

What's really great with this approach is progressive replacement at its best. This way we can replace every single functionality of the old solution without affecting any part (or just a little) of old codebase.

And the best part of it is that we are not affected by any legacy code or framework. We don't have to worry about utils file that has more than 30 functions and whether we shall upgrade one of them or add another one on the very bottom. Also we have total freedom in testing tools.

Just like pizza with different slices baked separately but delivered in a single box.

— Original photo by Roam In Color

What about styles?

Cascading nature of styles stands in a little opposition to code encapsulation and separation of concerns. There are two issues on the horizon. First: we need to be assured that our styles won't flood anywhere else on the parent container. Second: We have to allow somehow for slight external changes.

First things first- to prevent our styles from flooding outside we are going to use CSS modules. This way we are able to write styles just like they were normal css, but the output generated by webpack will limit styles to local scope by using random names. This solution is not as good as shadow dom separation, but is surely sufficent.

To allow external containers for slight style overwrites we will introduce special classnames for every important piece of markup so it will be easier to inline our micro frontend with parent app.

<div data-widget-host="office-map">
  <script type="text/props">
    { "title": "Global View", "classNameNamespace": "office-map-0" }
  </script>
</div>
And in our component:
import styles from './styles/style.scss';
const OfficeMap = ({ title = '', classNameNamespace = 'office-map' }) => {
  …
  const classNameCompose = (key) => `${styles[key]} ${classNameNamespace}-${key}`;
  …
  <div className={classNameCompose('mapWrapper')}>

There is also few things to remember:

—   Always inherit fonts
—   Provide light and dark theme
—   Use transparent background whereever possible
—   Do not add any margins and paddings to outer container
—   Use relative rem & em for sizing

How to handle backend

This is a bit tricky, as there might be situations where user will have to authenticate before he might see some data. Right now, as a company, we are working on new SSO implementation, so hopefully all we will have to do in future will be simply passing token down to every microfrontend implementation.

Still, right now we decided- because of versioning- that every microfrontend has its own backend that is versioned internally as well. This way we also cut down possibilities of inproper backend implementations that might cause unexpected errors. This also makes testing and maintaining lots easier.

Next steps

Next step for us will be server-side rendering, as right now this module doesn't require any seo, but surely we have to handle this somehow. Probably every microfrontend will have express service next to it that will be able to just simply render it's content to html.

Also, our eyes are directed towards Module Federation and Single Spa as a meta framework.

In future

There's of course lots more things to consider, like shared components library, shared ui communication and behaviour, assets management.

Still, this is our first approach to handle microfrontends architecture and check how it behaves and fits to our company model.