Understanding Micro Frontend Architecture using Next JS
Lets see how it all started and how its going these days ...
Micro Frontends in 2016
ThoughtWorks began assessing a new technique known as "micro frontends" to gain similar benefits that microservices offered, but for the front end.
"An architectural style where independently deliverable frontend applications are composed into a greater whole"
In the November 2016 issue of the Thoughtworks technology radar, we listed micro frontends as a technique that organisations should Assess. We later promoted it into Trial, and finally into Adopt, which means that we see it as a proven approach that you should be using when it makes sense to do so.
Micro Frontends in 2020
Webpack 5 was released and its Module Federation feature has opened the doors to new possibilities in the relatively young micro frontend paradigm.
Micro frontends in a NextJS app
So micro frontends have actually been around for a while now and have been implemented in various ways. One of the newer and exciting additions to the ecosystem is Webpack 5 and its module federation feature.
Micro frontends are an architectural pattern that has become a hot topic in recent years for many good reasons. It has helped organisations break complex UI applications into smaller, more manageable pieces, as well as enabling teams to work end-to-end across a well-defined product or business domain whilst maintaining a consistent user experience across the entire app.
One of the key benefits of this approach is that it enables teams to be autonomous, working independently on deployable UI applications, reducing the risk of accidental coupling and defining clear boundaries around delimited contexts, minimising dependencies.
module federation making it possible
One of the module federation authors also created a library as a solution to allow Next.js applications to expose and consume remote modules. The example works like a charm when you run it… locally. The moment you want to deploy each application on separate hosts, it all falls apart. The problem is that the example relies on a local disk copy of the remote module when it is rendered on the server, which defeats the purpose of separate deployments for micro frontends.
It’s all about tradeoffs and if the server-side rendering of your remote modules isn’t critical, there’s a way to make it work. Let’s dive in. This is one of the common reference architectures of a micro frontends application:
Independent deployable apps are loaded in a single container to orchestrate a consistent user experience
There is a “shell” or “container” that serves as the web platform for the individual micro frontends. Each application is deployed on separate hosts and can be modified without the need to rebuild the entire system. Let’s build 3 different application, here is the link of Github Repo
Link https://github.com/tkssharma/nextjs-micro-frontends
We just have simple example where we are showing two components coming from two different applications to main container application
Our components are simple
First Application
import Image from 'next/image'
import styles from '../styles/Mario.module.css'
const Mario = () => {
return (
<main className={styles.main}>
<Image
src="https://upload.wikimedia.org/wikipedia/en/a/a9/MarioNSMBUDeluxe.png"
alt="Mario"
width={240}
height={413}
/>
<h1 className={styles.title}>
G'day! I'm Mario, a microfrontend.
</h1>
<span>I'm hosted at <a target="_blank" href="https://mf-micro-front-end-activate.vercel.app">https://mf-micro-front-end-activate.vercel.app</a></span>
</main>
)
}
export default Mario
Our next config file
const {
withModuleFederation,
} = require("@module-federation/nextjs-mf");
module.exports = {
future: { webpack5: true },
images: {
domains: ['upload.wikimedia.org'],
},
webpack: (config, options) => {
const { isServer } = options;
const mfConf = {
mergeRuntime: true, //experimental
name: "app2",
library: {
type: config.output.libraryTarget,
name: "app2",
},
filename: "static/runtime/app2remoteEntry.js",
remotes: {
},
exposes: {
"./mario": "./components/mario",
},
};
config.cache = false;
withModuleFederation(config, options, mfConf);
return config;
},
webpackDevMiddleware: (config) => {
// Perform customizations to webpack dev middleware config
// Important: return the modified config
return config;
},
};
Second Application
import Image from 'next/image'
import styles from '../styles/Luigi.module.css'
const Luigi = () => {
return (
<main className={styles.main}>
<Image
src="https://upload.wikimedia.org/wikipedia/en/7/73/Luigi_NSMBUDX.png"
alt="Luigi"
width={240}
height={413}
/>
<h1 className={styles.title}>
G'day! I'm Luigi, a microfrontend.
</h1>
<span>I'm hosted at <a target="_blank" href="https://mf-micro-front-end-main.vercel.app">https://mf-micro-front-end-main.vercel.app</a></span>
</main>
)
}
export default Luigi
Next JS config
const {
withModuleFederation,
} = require("@module-federation/nextjs-mf");
module.exports = {
future: { webpack5: true },
images: {
domains: ['upload.wikimedia.org'],
},
webpack: (config, options) => {
const { isServer } = options;
const mfConf = {
mergeRuntime: true, //experimental
name: "app1",
library: {
type: config.output.libraryTarget,
name: "app1",
},
filename: "static/runtime/app1RemoteEntry.js",
remotes: {
},
exposes: {
"./luigi": "./components/luigi",
},
};
config.cache = false;
withModuleFederation(config, options, mfConf);
return config;
},
webpackDevMiddleware: (config) => {
// Perform customizations to webpack dev middleware config
// Important: return the modified config
return config;
},
};
Now we will get both components on shell container components
const {
withModuleFederation,
} = require("@module-federation/nextjs-mf");
module.exports = {
future: { webpack5: true },
images: {
domains: ['upload.wikimedia.org'],
},
webpack: (config, options) => {
const mfConf = {
name: "shell",
library: {
type: config.output.libraryTarget,
name: "shell",
},
remotes: {
app1: "app1",
app2: "app2",
},
exposes: {
},
};
config.cache = false;
withModuleFederation(config, options, mfConf);
return config;
},
webpackDevMiddleware: (config) => {
// Perform customizations to webpack dev middleware config
// Important: return the modified config
return config;
},
};
Import Module in container application
import dynamic from 'next/dynamic'
const RemoteLuigi = dynamic(
() => import("app1/luigi"),
{ ssr: false }
)
const App2 = () => (<RemoteLuigi />)
export default App2
// another page
import dynamic from 'next/dynamic'
const RemoteMario = dynamic(
() => import('app2/mario'),
{ ssr: false }
)
const App1 = () => (<RemoteMario />)
export default App1
How to Test setup
cd micro-front-end-activate
npm install
nbpm run build
npm run dev
cd micro-front-end-main
npm install
npm run build
npm run dev
cd micro-front-end-shell
npm install
npm run build
npm run dev
Testing setup
- localhost:3000/mario will render mario component from app2
- localhost:3000/luigi will render luigi component from app1
Module federation allows a JavaScript application to dynamically load code from another application — in the process, sharing dependencies, if an application consuming a federated module does not have a dependency needed by the federated code — Webpack will download the missing dependency from that federated build origin.
This demo was only to showcase how it works, i have created demo also on the same Here
Comments