Webpack 5 and Module Federation - A Microfrontend Revolution

marais

Marais Rossouw

Posted on March 2, 2020

Webpack 5 and Module Federation - A Microfrontend Revolution

Picture this: you've got yourself a pretty whiz-bang component, not just any component, but that classic component that seems to just exist on every page... You know the header, or the authenticated state in that header, the CTA on the homepage... You get the gist. Up until now, you've probably been code sharing by simply sharing that component as an npm package, where you then build and deploy every application independently. Which seems reasonable, but there has always been something not quite right.

Now if you are anything like me, you've felt the pain when a designer asks you to change the border, or the background colour of that whiz-bang component. And you are dreading the deployment of having to now build each and every one of those apps. Maybe you will get lucky and it will all go smoothly, but probably not. You are impacting uptime perhaps, or maybe you are statically generating, and now your back-end gets hammered as you rush to get each of your 250k permutations built quickly to get this change out (personally, I've been there).

Introducing Module Federation! 🎉

Module Federation aims to solve the sharing of modules in a distributed system, by shipping those critical shared pieces as macro or as micro as you would like. It does this by pulling them out of the the build pipeline and out of your apps.

To achieve this there are two main concepts to get your head around: the Host's and the Remote's.

Host

A host is an artifact that can be loaded cold. Typically, the one that usually initializes from the window.onload event. A host app contains all the typical features from a SPA, or SSR app. It loads all the initial chunks, boots the app and renders what the user will see first. Now the main difference here is, instead of having that infamous super shared component even remotely bundled, it is referenced. Why? Because that component lives as part of the Remote!

You see, the beauty of this approach is that you can can have the critical JavaScript required to load that first app, and only the required; speaking true to the micro-frontend (MFE) philosophy.

An example config:

const ModuleReferencePlugin = require("webpack/lib/container/ContainerReferencePlugin");

new ModuleReferencePlugin({
    remoteType: 'global',
    remotes: ['app_one', 'app_two'],
    overrides: {
        react: 'react',
    }
});
Enter fullscreen mode Exit fullscreen mode

Remote

A remote can both be a host or strictly a remote. A remote's job is to offer up, or rather expose modules that can be consumed by other Host's and Remote's.

You can also opt this remote into having some (or all) of its dependencies shared in the sense of; if the host already has react, just send it into this runtime, allowing the remote to not have to download its own copy of react.

An example config:

const ModuleContainerPlugin = require("webpack/lib/container/ContainerPlugin");

new ModuleContainerPlugin({
    name: 'app_one',
    library: { type: 'global', name: 'app_a' },
    shared: {
        react: 'react',
    },
    exposes: {
        Title: './src/components/Title'
    }
});
Enter fullscreen mode Exit fullscreen mode

How it works

From the image above, you can see that a, b, c are all exposed components that come from different apps. B, and C come from our app_three container where A come from our app_two container. All three come together to make our green component, which you an also then expose!

To make things a bit simpler, and more uniform; we have a:

Federation Plugin 🕺

The important one!

But most of the time, you will want your apps to both expose and/or consume federated modules.

For this, we have a plugin to rule them all!

const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

new ModuleFederationPlugin({
    name: 'app_two',
    library: { type: 'global', name: 'app_a' },
    remotes: {
      app_one: 'app_one',
      app_three: 'app_three'
    },
    exposes: {
       AppContainer: './src/App'
    },
    shared: ['react', 'react-dom', 'relay-runtime']
}),
Enter fullscreen mode Exit fullscreen mode

What you see up there is an app that can host its own copy of react, react-dom and relay, exposes its own AppContainer — but then has the ability to import the Title from app_one, and have the host share the dependencies, react, react-dom and maybe relay-runtime with that remote. Which means loading in that remote will only download the code needed to power that component, and NONE of the shared modules.

What this will allow you to do in practice is have each one of your MFE's expose its route-map, typically the component fragment that you'd give to react-router.

// AboutApp
// routes.jsx

export default () => (
    <Routes>
        <Route path="/about" component={About} />
    </Routes>
)

// AboutUserApp
// routes.jsx

export default () => (
    <Routes>
        <Route path="/about/:slug" component={AboutUser} />
    </Routes>
)
Enter fullscreen mode Exit fullscreen mode

Marking that routes.jsx file as an exported member of the AboutApp, and AboutUserApp within their respective webpack configs.

// HomepageApp
// routes.jsx

import { lazy } from 'react';

const AboutAppRoutes = lazy(() => import('AboutApp/routes'));
const AboutUserAppRoutes = lazy(() => import('AboutUserApp/routes'));

// assuming you have suspense higher up in your tree 🤞
export default () => (
    <Routes>
        <Route path="/" component={Homepage} />
        <AboutAppRoutes />
        <AboutUserAppRoutes />
    </Routes>
)
Enter fullscreen mode Exit fullscreen mode

and voilà you have yourself a lazy federated application!

magic

whereby; the about app, and about user app are both loaded from their respective bundles - but act like they were all bundled together in the first place!

That's not all, what if you could also now wrap that router in a AppContainer, where you'd typically share headers and footers!

// AppContainerApp
// container.jsx

export default ({ title, children }) => (
    <>
        <Helmet>
            <title>{title}</title>
        </Helmet>
        <Header/>
        <main>
            {children}
        </main>
        <Footer/>
    </>
)
// Please don't actually do the Helmet part, re-renders are bad!

// HomepageApp
// App.jsx

import * as React from 'react';
import { render } from 'react-dom';
import { HashRouter } from 'react-router-dom';

import AppContainer from 'AppContainerApp/Container';
import RouterConfig from './routes';

const App = () => (
    <HashRouter>
        <Suspense fallback={'loading...'}>
            <AppContainer title="I'm the Homepage App">
                <RouterConfig />
            </AppContainer>
        </Suspense>
    </HashRouter>
);

render(App, document.getElementById('app'));

// AboutApp
// App.jsx

import * as React from 'react';
import { render } from 'react-dom';
import { HashRouter } from 'react-router-dom';

import AppContainer from 'AppContainerApp/Container';
import RouterConfig from './routes';

const App = () => (
    <HashRouter>
        <Suspense fallback={'loading...'}>
            <AppContainer title="I'm the About app">
                <RouterConfig />
            </AppContainer>
        </Suspense>
    </HashRouter>
);

render(App, document.getElementById('app'));
Enter fullscreen mode Exit fullscreen mode

Boom! You have yourself an application that;

  1. Has a homepage MFE that can be built, deployed and ran independently from our about app.
  2. Has an about MFE that can also be built, deployed and ran 100% alone.
  3. Has both applications share the common header and footer.
  4. Has the about routes lazied into the homepage app, so... wait for it! You can have SPA transitions between 2 applications! And only download the delta between those 2 apps. react, react-router and such are all shared, so there is no re-download of that!

wowzers

Think about the possibilities: you could share your design system so you could change the background colour of that component we spoke about, and effectively have all your things evergreen across your entire system! You could share the CTA that sits at the bottom of every article. What about that cross-sell component you'd like placed on the checkout and the product pages? Effectively endless.

Considerations

Now this all sounds amazing right? But there are few draw backs.

  1. This is effectively micro-services on the frontend. So version is bound to come up. "Why did you introduce a breaking change"... For this I'd suggest a contract api snapshot jest test
  2. If you are using relay, you cannot spread fragments on queries that wrap a potentially federated module. As the fragment could have changed. For this I'd suggest a QueryRenderer component.
  3. Modules that depend on say a react context, where the provider is never exposed. Those sorts of things.
  4. Loading in the right initial remote chunks is quite tedious at this stage. It requires knowing the chunk filenames ahead of time and manually injecting those. But we've got a few ideas.
  5. Local development activities. Yet to find a nice clean way to not have to run all apps at once, but for now I personally have just been using webpack aliases, to point those app references to folders in my mono-repo.
  6. ... that is about it, in all my trials this solution hasn't surfaced any initial issues.

Community

The community has an amazing response for which Zack and myself (Marais) want to thank you all so much for assisting us, and shedding light to so many potential corner cases, and use cases which we are opening investigation as we grow this technology!

Liquid error: internal

Special thanks to:

Joel Denning author of SystemJS — for navigating us through the SystemJS space, and enlightening us about the world if importmaps, as further investigate dependency url resolution, which is fairly manual at the moment.
Tobias Koopers for webpack, and giving us such an amazing foundation to build this upon, and ultimately helping us carve the code needed to actually bring this concept into a reality.
AutoGuru for giving myself the space to create and experiment with this!

Photo by Daniel Fazio on Unsplash

💖 💪 🙅 🚩
marais
Marais Rossouw

Posted on March 2, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related