Micro frontends, in practice
“Microfrontends” is a hot topic nowadays. Since front-end projects become big and feature-rich, as the codebase gets bigger, tooling and organization are essential for a successful project.
I would rather think of micro-frontends in terms of “polyglot front-ends”. Javascript ecosystem is growing fast, and every year or two we encounter new libraries, tools, and frameworks. Migrating or rewriting everything from the grounds up to the new stack seems impossible.
As our application grows, it becomes harder to keep up the pace.
Polyglot front-ends approach enables us to keep existing modules intact, written in their stack. The polyglot front-end approach let us run one application written in several technologies, every module has its build chain and tools, and even developed by different teams.
In the last year, I have managed to successfully implement a production-grade polyglot front-end, written originally in AngularJS and new modules added written in Angular 7. The application shares runtime resources, but not just static files: It shares runtime code. The application is designed to be able to contain as many modules as you like, in ANY technology. We are ready to add React, or Vue, or Whatever.
The concept is to wrap everything in one shell application, orchestrating friendly IFrame elements. Friendly means that the IFrames has no src attribute, and everything is injected in runtime. This enables the frame to share runtime code via references. A shared application context holds everything we need. AngularJS provides several services, and Angular 7 provides others. The routing is synchronized between them by the shell application, which listens to every URL / history API change in each of the frames, including the root.
The solution was based on my open-source library “microfronts”.
import {Microfronts} from 'microfronts';
const application = Microfronts();
const context = application.getAppContext(); // this is the link were we share runtimes
const router = application.getRouter(); // this watches changes and synchronizes/orchestrates the frames
const LEGACY_UI = {
base: 'https://app1.server.com/', // this is where we deploy the legacy UI
appId: 'legacy'
};
const ANGULAR_APP = {
base: 'https://app2.server.com/', // this is where the newer features are deployed
appId: 'angular'
};
router.registerRoute('*', { active: [LEGACY_UI] }); // handles all routes, excluding the following ones
router.registerRoute('settings', { active: [ANGULAR_APP] });
router.registerRoute('admin', { active: [ANGULAR_APP] });
router.init();
On the HTML side, the implementation is straightforward
<iframe is="app-container" app-id="legacy"></iframe>
<iframe is="app-container" app-id="angular"></iframe>
For every route starting with “settings” or “admin”, the application will show only the new UI module, as for other routes it will display the legacy UI. The memory and state are kept intact, and the navigation between the different pages is smooth.
There are shared runtimes injected from the shell application, the API wrapper, the base services, etc.
For this to work, the servers must provide proper cross-origin policy. The accepted referrer should be where the shell application is deployed, or if your application is accessible from anywhere, a simple “*” would be enough.
The development machine could either host a simple proxy server to route production server to the local server, or the configuration can be injected using a build tool (i.e. Webpack).
I’d be happy to get feedback for this library, please visit https://microfronts.dev
The GitHub repo has some examples built in a mono repo project, using lerna.