This content originally appeared on DEV Community and was authored by Muhammad | עזרא
But why do we need it?
As the frontend becomes larger, while holding multiple domains of responsibilities (for example — navigation, user, authorization, e. g. ), the frontend application is developed, deployed and hosted as a single application.
Micro-frontends allows us to use those different applications not just on the code base, but also in development, deployment and serve, in order to provide:
- Loosely coupled applications
- Faster development, debugging, and testing flows
- Performance — smaller chunks
- Full Isolation while testing, developing, and deploying
During this article we will review the frontend application that is built out of the shell and 3 applications that represent a domain:
- Shell — Used as the entry point for loading each of our micro-applications based on the URL path. The shell application will also trigger authorization for route guards.
- Navigation — Responsible for navigation logic and state, including the nav-bar component, navigation service
- User — Responsible for user logic and state, including the user query and store logic, user info component, and user management page
- Feed — Responsible for fetching and presenting the feed items, each item contains the user logic and state, including the user query and store logic.
Each application built out the following layers:
- Composition layer — This layer holds a set of application pages with their corresponding routes
- Widgets layer — This layer holds a set of domain-related components used to build the different pages found on the composition layer
- Business logic layer — This layer holds a set of services and utilities responsible for the domain business logic.
- Communication layer — This layer holds a set of services that are used to communicate with the different service providers (Backend services for example).
- Storage layer — This layer holds the logic to persist data into the storage objects
- In memory — State, hooks e.g.
- Disk — local-storage, indexedDB, cookies e.g.
Wait, but where is the chaos?
Thanks to module-federation, we can load Micro-frontends applications during run time without the need to build the entire dependency graph.
This introduces a whole new aspect of stability issues of frontend applications:
- What happens when we are deploying a new version of our application (User in our scenario)?
- How can we identify affected areas?
- How can we guarantee there are no breaking changes hidden behind each deployment?
- How can we prevent tight coupling between multiple applications that are hosted together?
In our example, imagine a developer changed one of the widgets from the User application, this widget is consumed by both the Feed and the Navigation applications. Now, let's imagine the change the developer done is breaking the contract (component API — inputs/outputs, aka. props).
This will lead to a runtime error while loading the new version within the existing applications.
And the result? Cascading failure of our frontend application after deployment of the new User application.
Tackling the problem
First, let’s review the requirements we have from the micro-frontend applications:
- Each application should be built, tested and served as a standalone unit.
- A modification of a single application should be available to be used by any other application.
- Application widgets and services should be reusable and interchangeable.
- Encapsulation of application internal models and business logic — Modifications shouldn’t affect application consumers.
- Identify dependency graph per modification — will help us to trigger only the relevant tests suites and builds.
Following those items, lets’ review the approach from the previous section:
The approach covers bullets 1 to 3 from the requirements list. But, it still fails for both bullets 4 and 5 which promise us the stability of our product.
Let’s review the different approaches to handle this chaos.
The libraries approach
In order to increase the stability of the application, we need to prevent hidden breaking changes.
With the libraries approach, this can achieve easily while using the npm package version. As each build of our applications is sealing the library version it’s using we can prevent consumption of library versions that might contain breaking changes.
Using module federation, we can set the shared libraries, as part of this configuration we can set the satisfied package version using the npm package versioning convention.
This approach helps us to break our monolith into 4 layers:
- Core Library — This layer contains domain agnostic libraries, those libraries provide us the building block for our feature libraries layer.
- Feature libraries layer — This layer contains domain-specific business logic, storage logic, and widgets. Those widgets are developed based on the core libraries component kit and additional components that are part of the specific domain of responsibility.
- Composition applications — This layer contains domain-specific routes and pages. Those pages are built based on widgets, services, and business logic developed as part of the “Feature libraries” layer.
- Shell — The entry point of the application, usually acts as a container and a router to load each of the micro-applications based on the path.
- The shell application might also trigger authorization logic.
Structure:
- apps
- user
- feed
- navigation
- shell
- libs
- users-lib
- feed-lib
- navigation-lib
- auth
Webpack configuration:
plugins: [
new ModuleFederationPlugin({
name: "user",
filename: "remoteEntry.js",
exposes: {
'./bootstrap': './apps/user/bootstrap.module.ts',
},
shared: share({
"@angular/core": { singleton: true, strictVersion: true, requiredVersion: '^12.0.0' },
"@angular/common": { singleton: true, strictVersion: true, requiredVersion: '^12.0.0' },
"@angular/common/http": { singleton: true, strictVersion: true, requiredVersion: '^12.0.0' },
"@angular/router": { singleton: true, strictVersion: true, requiredVersion: '^12.0.0' },
"@mfe/auth": { singleton: true, strictVersion: true, requiredVersion: '^1.0.0' },
"@mfe/user": { singleton: true, strictVersion: true, requiredVersion: '^1.5.0' }, ...sharedMappings.getDescriptors()
})
}),
sharedMappings.getPlugin()
],
Advantages
- Shareable widgets, services, and pages (compositions) across applications.
- Breaking changes prevention — Using a sealed version of the consumed library during the build.
Disadvantages
- Data corruption — Possible due to collision between multiple versions of the same library (override the state, local storage e.g.).
- Bundle size increase — Libraries might be loaded more than once due to different versions.
- Deployment graph complexity — critical modifications require rebuilding and redeploying the entire dependency graph.
The anti-corruption layer approach
What is the anti-corruption layer?
An anti-corruption layer is a set of Public-APIs exposed by an application for integration use, those Public-APIs are acting as contracts in order to isolate the application internal models and business logic complexity, and are used as exported modules, components, façade*, and adapters* classes.
This layer can be uni-directional or bi-directional (fetch or ingest data).
Façade
A service that provides a simple interface to a complex application, encapsulates the complexity of initiating the application.
A façade might provide limited functionality, those are the required sub-set for integrating with the micro-frontend application
Adapter
A service that is responsible for covert the interface and the data model of an object to another structure/interface which is accepted by the consumers.
The updated 4-layers approach
The only modification is casting the Feature layer from libraries to applications, this allows us to serve those widgets and services seamlessly to the consumers. Having said that we will still need to protect from breaking changes, here is where the anti-corruption layer is taking place
- Core Library — This layer contains domain agnostic libraries, those libraries provide us the building block for our feature libraries layer.
- Feature application layer — This layer contains domain-specific business logic, storage logic, and widgets. Those widgets are developed based on the core libraries component kit and additional components that are part of the specific domain.
- The exposed logic and components are protected with an anti-corruption layer to prevent breaking changes.
- Composition applications — This layer contains domain-specific routes and pages. Those pages are built based on widgets, services, and business logic developed as part of the “Feature application” layer.
- Shell — The entry point of the application, usually acts as a container and a router to load each of the micro-applications based on the path. The shell application might also trigger authorization logic.
Structure:
- apps
- user
- src
- modules
- bootstrap
- bootstrap.module.ts
- public-api.ts
- public-api.d.ts
- feed
- src
- modules
- bootstrap
- bootstrap.module.ts
- public-api.ts
- public-api.d.ts
- navigation
- src
- modules
- bootstrap
- bootstrap.module.ts
- public-api.ts
- public-api.d.ts
- shell
- libs
- auth
Webpack Configuration
plugins: [
new ModuleFederationPlugin({
name: "user",
filename: "remoteEntry.js",
exposes: {
'./public-api': './apps/user/public-api.ts',
},
shared: share({
"@angular/core": { singleton: true, strictVersion: true, requiredVersion: '^12.0.0' },
"@angular/common": { singleton: true, strictVersion: true, requiredVersion: '^12.0.0' },
"@angular/common/http": { singleton: true, strictVersion: true, requiredVersion: '^12.0.0' },
"@angular/router": { singleton: true, strictVersion: true, requiredVersion: '^12.0.0' },
"@mfe/auth": { singleton: true, strictVersion: true, requiredVersion: '^1.0.0' },
...sharedMappings.getDescriptors()
})
}),
sharedMappings.getPlugin()
],
Advantages
- Shareable widgets, services, and pages (compositions) across applications
- Seamless propagation of an upgrade
- Breaking changes prevention using Anti-Corruption layer.
- Refactor is becoming more simple thanks to encapsulation.
Disadvantages
- Another layer to be maintain
- Education and learning curve
- Integration testing is required to promise unbreaking changes
This content originally appeared on DEV Community and was authored by Muhammad | עזרא
Muhammad | עזרא | Sciencx (2021-11-18T21:05:53+00:00) Micro Frontend, but it’s chaos!. Retrieved from https://www.scien.cx/2021/11/18/micro-frontend-but-its-chaos/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.