Micro frontends – relatively new, relatively cool word in frontend development world.
~ Me
As micro frontends become more and more of a popular solution to everyday frontend problems, many developers struggle to implement them in a reliable, and easy-to-maintain way.
I’m not going to explain what micro frontends are – you can google it and find whatever you need on your own.
I will try to present a simple, overhead-less possibility to implement them using only Webpack (and tools available in any rendering library ecosystem – router primarily). The purpose of this post is to present the idea of how micro frontends may be implemented without diving into details. Still, you need to know webpack, and your rendering library/framework pretty well, to be able to implement that in your project.
But… why using webpack when I can use XYZ?
You can use all the boilerplates available out there which take up to 100 hours of learning all configuration options to set up a basic app. You can have a working application, without knowing how it works, because everything is abstracted away. You might have to rely on some anonymous maintainers and be at the mercy of their fancy-schmancy PR bots (who needs that?!), which reject your PRs because you forgot to place semicolons here and there.
Of course, I’m over exaggerating, but this might be the answer to the question.
Start small. See what you can build by yourself using only the tools you already probably have. I’m here to give you some idea of how micro frontends can be built. Later on, we should be able to see, what are the pros and cons of a presented solution. Finishing this article will hopefully add another tool to your arsenal.
Overview
- Application is built with N micro-frontends (each application is compiled separately), and one umbrella application (compiled) which is the entry point for the site.
- Each micro frontend exposes a function to
window
(you can think about it as a library), which renders (whether withReactDOM
orVue.mount
and so on) into a given place. - Umbrella application loads micro frontends and launches starting one
Using this method, you may even make one micro-frontend load another micro-frontend. And because it’s so simple, it is also relatively flexible.
We have used this approach in a containerized environment, where each frontend application has its own sandboxed container.
This solution can be easily integrated with popular rendering libraries (React, Vue, others).
Challenges
There is one major challenge: how to handle many webpack applications running at once. This will be solved by setting various, not commonly used webpack variables.
Webpack development configuration
// webpack.config.js
module.exports = {
mode: 'development',
entry: [
path.join(process.cwd(), 'app/entry.js'),
],
output: {
publicPath: `${appPath}/`, // path from which javascript code (filename defined below, and code chunks) will be accessible
filename: 'microfrontend1.js', // name of the main filename
chunkFilename: 'microfrontend1-[name].chunk.js', // name of any chunks generated alongside main file
devtoolNamespace: 'microfrontend1Devtool', // first variable used for scoping multiple apps working at the same time
library: 'microfrontend1Lib', // second variable used for scoping multiple apps working at the same time
},
...
Now you can add this config to the second micro frontend as well – remember to change names appropriately.
Webpack production configuration
// webpack.config.js
module.exports = {
mode: 'production',
entry: [
path.join(process.cwd(), 'app/entry.js'),
],
output: {
filename: 'microfrontend1.js',
chunkFilename: 'microfrontend1-[name].[chunkhash].chunk.js',
path: path.resolve(process.cwd(), 'build'),
publicPath: '${appPath}/',
jsonpFunction: 'microfrontend1Jsonp', // this will allow to load chunks in production environemnt without name clashing
},
...
Entry file configuration
Now the funny part, the entry file. Its job is to add whatever we need to the window object.
// app/entry.js
import('./app.js').then(({ Microfrontend1 }) => {
window.microfrontend1 = new Microfrontend1(); // this may a class, a function, it depends on your case
});
We add dynamic import, to load things asynchronously.
Whatever is exported from ./app.js
and mounted in the global scope, should implement an interface, that is common between all micro frontends, and known to umbrella application. It should have render and unmount functions.
And do the same for other micro frontends.
Umbrella
Umbrella application has one job: load micro frontends. It may show loader, it may decide when micro frontends should be loaded, it may render conditionally one of the applications.
This is done by adding a script tag:
<script src="/${appPath}/microfrontend1.js">
And then calling, for example (this usually will be called inside component or routing function):
window.microfrontend1.render('#app')
What’s best, is that the umbrella app can be easily extended with functionalities like creating an event emitter bus for all micro frontend applications, or passing some shared state, history object, and others into application instances.
Cons & pros
Cons:
- Requires good knowledge of framework/library to implement properly working routing (including switching between applications)
- May cause problems with some libraries (UI frameworks, I’m looking at you! ), that doesn’t work properly when unmounted and remounted into view (during switching between applications without resetting their global context)
- May be trickier to optimize
Pros:
- Dead simple and understandable
- Flexible to do some crazy stuff
- Framework/library-independent may be easily used with vanilla JavaScript applications too
- No config files (besides webpack of course)
Summary
That’s it- it is so simple, that I cannot even write a summary. What a wonderful piece of technology! Feel free to experiment with that, as it serves as a base to implement your own solution upon this.
Author: Mateusz Koteja