decode a micro frontend framework

  1. The main Tech Stack
  2. Repository structure
  3. The main stitcher
  4. The mini stitchers
  5. The MFE repositories
  6. How a MFE is loaded
  7. Build
  8. Deployment
  9. Running on the production server
  10. UI library
  11. Testing
  12. Security
  13. Monitoring
  14. File structure in a repo
    1. File structure in the src folder
  15. State Management

One of the projects I am working on uses a proprietary micro frontend framework as the foundation, which is mainly based on Webpack Module Federation. In this post, I am going to decode the overall framework design.

The main Tech Stack

  • Webpack Module Federation
  • React
  • Tailwind

Repository structure

1
2
3
4
5
6
7
8
9
main-stitcher (MAIN STITCHER)
- admin-stitcher (MINI STITCHER)
- user-mgmt (MFE)
- config (MFE)
- task-list-stitcher (MINI STITCHER)
- task-list (MFE)
- appl-stitcher (MINI STITCHER)
- application (MFE)
- borrower (MFE)

Each of them is a Node.js project,which has package.json, webpackge.config.js and Dockerfile in the root folder.

The main stitcher

There are two business modules in it, most others are in the MFE repositories:

  • Dashboard
  • Login

There is also a register-config.json file which has mini stitcher information.

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"stitcher": {
"publicPath": ""
},
"miniStitcherGroups": [
{
"domainPath": "/url/path/to/admin-stitcher",
"miniPublicPath": "/admin",
"applicationConfigPath": "http://example.com/url/path/to/admin-stitcher-config.json"
}
],
"subApps": []
}

Note: The file content of the applicationConfigPath file is defined in the mini stitcher repository, which we will see in the next section.

The mini stitchers

There can be many mini stitchers in this structure. There are not many interesting things in a mini stitcher, it’s just a container for closely related sub applications.

There is a sourcelist.json and mini-stitcher-config.json file in a mini stitcher.

All sub apps are defined in the sourcelist.json file, it is used by pipeline to pull code from specific repositories and generate the deployable image.

1
2
3
4
5
6
7
[
{
"repo": "http://git.example.com/path/to/user-mgmt.git",
"branch": "branch_name",
"directory": "user-mgmt"
}
]

The mini-sitcher-config.json file has runtime configurations in the subapps.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"subApps": [
{
"name": "user-mgmt",
"entry": "/url/path/to/user-mgmt/index.html",
"alias": "user-mgmt",
"activeRule": "/path/to/active/rule/user",
"container": "#user-mgmt",
"props": {},
"configuration": {
"loginRequired": true
}
}
]
}

The MFE repositories

They are in the lowest layer in the structure. In order to be loaded in the main stitcher, modules need to be exposed in webpack.config.js:

1
2
3
4
5
new ModuleFederationPlugin({
exposes: {
[`./${entryName}.index`]: `./src/${entryName}.index`
}
})

We will get a URL for this sub app, something like:

1
https://cdn.example.com/remote/remote-entry.js

How a MFE is loaded

When a menu in the main stitcher clicked, the code in the Dashboard will try to get the MFE repository information. Then inject the remote script in the HTML document:

1
2
3
4
5
6
7
8
const script = document.createElement('script');
document.head.appendChild(script);

script.async = true;
script.onload = () => {
// remote-entry.js has been downloaded and executed.
}
script.src = 'https://cdn.example.com/remote/remote-entry.js';

Note: remote-entry.js does not contain the actual business logic, it is more like a factory.

1
2
3
4
window["remoteApp"] = {
get: (module) => { /* returns a module factory */ },
init: (shareScope) => { /* Init shared dependencies */ }
};

To call the actual business code, we need to load the component:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function loadComponent(scope: any, module: string) {
return async () => {
// Initializes the share scope. This fills it with known provided modules from this build and all remotes.
await __webpack_init_sharing__('default');
const container = scope; // window[scope] or get the container somewhere else

if (!container?.init) return;

// Initialize the container, it may provide shared modules.
// This will make sure that there is only 1 React instance, etc.
await container.init(_webpack_share_scopes__.default_)

// Download the actual chunk
const factory = await scope.get(module);

// Run the module
return factory();
}
}

Build

There is a build script in the main stitcher and a MFE repository, but not in a mini stitcher.

This job is done by a seperate pipeline repository microservice_microfrontend.git for mini stitchers. It will read the sourcelist.json file content in a mini stitcher during the deployment, and checkout all the repositores in it, run npm run build command seperately in each repository, finially copy all the generated files into a folder created for this mini stitcher.

Deployment

  • We deploy the mini stitcher instead of individual MFE repos.
  • Whenever we make changes in MFE, we will re-deploy the mini stitcher.
  • The stitcher images are deployed by Jenkins pipelines using Helm to OpenShift.

Running on the production server

  • A Pod is created for each stitcher deployment, which has Apache httpd server installed.
  • The built static files during the Jenkins build stage are copied into an Apache image.
  • When the Pod starts, Apache servers the static assets, include js files.

UI library

There is a custom UI library built with React for all the MFE repositories.

Testing

  • Jest
  • React Testing library

Security

  • Black Duck
  • Sonar Scan

Monitoring

  • Elastic RUM (Real User Monitoring)

File structure in a repo

1
2
3
4
5
6
7
8
9
10
11
12
13
|-- config
|-- public
|-- scripts
|-- src
|-- .eslintrc
|-- .prettierrc.js
|-- babel.config.js
|-- Dockerfile
|-- jest.config.js
|-- package.json
|-- sonar.properties
|-- tsconfig.json
|-- webpack.config.js

File structure in the src folder

1
2
3
4
5
6
7
8
9
10
11
12
13
|-- app
|-- assets
|-- i18n
|-- styles
|-- types
|-- App.app.tsx
|-- App.bootstrap.tsx // The entry point
|-- App.communication.ts
|-- App.config.ts
|-- App.constants.ts
|-- App.index.tsx
|-- App.interface.ts
|-- typings.d.ts

State Management

The framework uses @ngneat/elf and @ngneat/elf-persist-state to store and manage the global state.