Module Federation Series Part 1: A Little in-depth
Not a member of Medium? Read this article for free here.
When you research approaches for building micro frontends, you would constantly see a module federation which is a new game-changer plugin. The problem is that devs (like me) may find it hard to understand the philosophy of module federation. For example: how things like sharing dependencies happen behind the scenes. It took me days/weeks in order to better figure out it. Since module federation is relatively new, for now, most blogs cover only the basic parts, unfortunately.
Building micro frontends with module federation is a big topic, so I decided to split my post mainly into 3 separate blogs:
✅ Module Federation Series Part 1: A little deep dive
⏳ Module Federation Series Part 2: Things you should know when building microfrontends with Angular + Module Federation
⏳ Module Federation Series Part 3: Example Angular app with Module Federation + Router + NgRx + Angular Material + Communication
About me: I am a junior front-end developer.
☕ In this blog, I would try to share some knowledge with you about things I learned while reverse engineering module federation.
Before start, a big thanks 👏👏👏 to Manfred Steyer who contribted a lot for explaining about Module Federation details and how to integrate it with Angular . He even published a FREE Enterprise Angular book about module federation. Defintely recommended to read it👍.
You can checkout the Youtube Clone using ModuleFederation which uses some of the below techniques:
https://github.com/vugar005/youtube-webapp-turborepo
What is Module Federation?
As per Zack Jakson who is the author of this amazing plugin.
Module federation allows a JavaScript application to dynamically load code from another application. 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.
The world BEFORE module federation:
Before module federation, in order to implement micro frontends, developers were mostly using 2 approaches. The first approach, bundle the whole app as a web component(like angular elements). The main disadvantage of using web components without module federation is our bundle would be very big since we could not share common dependencies 😕. Even if our application consisted of the same version of framework/library like Angular v13, we would still load each app’s big chunk separately. Another approach was bundling our app with libraries such as with ng-packagr. In this case, the bundle was minimalistic but it was relying on peer dependencies on parent like angular/core, angular/material which means our whole app should be Evergreen and consist of same version of dependencies😕. If we use Angular v12 other apps should also have used v12. Module federation solves those issues by sharing common dependencies and it works with multiple versions or types of framework/library.😎
Some Basic Definitions🛠:
This blog is based on micro frontend example by Manfred Steyer in REPO.
Assume we have a shell(host) application that loads other micro frontends(remotes). In Module federation shell(host) is called ContainerReferencePlugin while separately loaded apps are called ContainerPlugin.
Our shell app uses Angular v13🅰, mfe1(remote) uses Angular v13 and mfe2 and mfe3(remotes) use Angular v12. Mfe4(remote) uses reactjs.
As per mentioned REPO, the basic configuration of the remote app(mfe1) would like below.
Name: here we provide the name of our remote so it can be accessed with this name from shell.
Library: here we specify the type of library. Starting from Angular v13 our app is bundled with ESM module so we specify type: module while below Angular v13 and other libraries like ReactJs or VueJs are bundled as plain js so we should specify them with type: var. The difference is we can load ESM module directly via import statement while to import plain js bundles we should generate <script> tags.
Exposes: here we expose our components. Usually only 1 file like bootstrap.ts. The module federation will generate the file(remoteEntry.js) which consists of instruction from exposes and shared config.
Remotes: Shell config on the other hand has references to remote micro frontends which expose their files via exposes. By the way, you can even load micro frontends dynamically, so even module federation shell app has no idea it has remotes. For more details LINK.
Shared config and remoteEntry.js:
This is the config you would most modify while building apps. Hence, this blog is mostly about this config. To better understand this section, I configured namedChunks in angular.json/webpack so we can clearly see the names of loaded chunks.
namedChunks: true
Shared config scenario 1: Empty
Assume our shell’s and remote (mfe1) app’s shared config is empty like below:
This is one of the most retarded scenarios😅 since we don’t use the power of the module federation.
Bundle overview:
In this case, we simply bundle all our app dependencies into a single file. Hence when we navigate to MFE1 from shell we can see our full mfe1 bundle(bootstap.js 2.0 mb unoptimized mode) which we exposed.
remoteEntry.js content:
Now let’s look at part of generated remoteEntry.js., it does not share anything at all. Check LINK to a gist file.
In others words, the module federation says like: ‘Hmm… Ok’. 😅
Also, we would see this error on the console:
Error: inject() must be called from an injection context
The reason for it is, since we don’t share dependencies our (angular/core) will be loaded twice(shell + mfe1).
As JoostK mentioned here:
This error is because having multiple versions of angular/core
, as inject
depends on the global state for it to work but with multiple copies of @angular/core
you'll end up with multiple copies of the global state at runtime, which causes problems like these.
Shared config scenario 2: Auto versioning
Now to solve the issue above, we should load our angular app once. In order to achieve that, we should split dependencies here(angular/core) into separate chunks.
This would split those packages into chunks and will try auto-assign the version numbers.👍👍 See below how:
Bundle overview:
Now we see that, angular core and other shared dependencies were loaded only once. In addition, our mfe1 bootstrap.js is much smaller now(150kb unoptimized mode).
remoteEntry.js content:
Now let’s look at part of generated remoteEntry.js. LINK to full gist.
We start to see module federation now auto assigns versions of dependencies and loads them if it is needed.
Shared config scenario 3: No required version specified
Now let’s use HttpClientModule in angular and decide to share it also.
mfe1 webpack config
shared: ["@angular/core",
"@angular/common",
"@angular/router",
"angular/common/http"]
When we load the app we would see below warning:
No required version is specified and unable to automatically determine one. Unable to find the required version..
As mentioned previously, if we don’t specify version manually, module federation would auto define it’s version from package.json.
According to manfred amazing book. The reason for this warning above is the secondary entry point. @angular/common/http
which is a bit like an npm package within an npm package. Technically, it’s just another file exposed by the npm package @angular/common
. Unsurprisingly, @angular/common/http
uses @angular/common
and webpack recognizes this. For this reason, webpack wants to find out which version of @angular/common
is used. For this, it looks into the npm package’s package.json ( @angular/common/package.json ) and browses the dependencies there. However, @angular/common
it itself is not a dependency of @angular/common
and hence, the version cannot be found.
So basically it is not angular related issue. Module federation is smart to auto define versions but in some cases, it can’t find versions of secondary entry points.
For more details LINK.
Solution: Define versions manually.
mfe1 webpack config
shared: {"@angular/core": {requiredVersion: '13.1.1'},
"@angular/common": {requiredVersion: '13.1.1'},
"@angular/router": {requiredVersion: '13.1.1'},
"@angular/common/http": {requiredVersion: '13.1.1'}}
Now warnings are gone.
Shared config Scenario 4: singleton and strictVersion
This one pretty simple scenario, if we define our dependencies as singleton. We instruct module federation that one and only one copy of this dependency should be loaded. If it finds out that there are multiple copies later then it will complain.
mfe2 webpack config
shared: {"@angular/core": {requiredVersion: '12.2.15', singleton: true},
"@angular/common": {requiredVersion: '12.2.15', singleton: true},
"@angular/router": {requiredVersion: '12.2.15', singleton: true},
"angular/common/http": {requiredVersion: '12.2.15', singleton: true}}
Now if we navigate from Shell v13 to MFE2 v12 Angular, we would see this warning.
In other words, module federation says like: “Come on man, you promised that you will only have same version angular”😅
In addition: instead of showing warning, we can make it stricter to show error instead.
Result:
Eager
We can also specify eager flag in shared config. It is used to instruct module federation whether to load our shared dependencies synchronously or asynchronously. By default, it is false(asynchronous). One thing I should point out is that, if we share our dependencies with eager: true
, when we, for example, navigating from shell v13 to remote app(mfe1) the mfe1 dependencies will be still loaded. For more details LINK.
In conclusion,
After playing with shared config in module federation, I realized that it is a safe bet to always specify versions of dependencies manually via requiredVersion. If you find it cumbersome to manually specify versions you can either use angular-architects/module-federation plugin or specify like below:
That's all for this post. Hope you enjoyed it and found it useful.🍍
References:
📗 Book: https://www.angulararchitects.io/en/book/
🗎 Example Code: https://github.com/vugar005/youtube-webapp-turborepo
🗎 Documentation: https://webpack.js.org/concepts/module-federation
Twitter: https://twitter.com/Vugar005
You can also check out my other blogs: