Module Federation Shared Services

19k views Asked by At

I'm working on a new project using Angular 11 and Webpack 5. I am basing my work on Manfred Steyer's Module Federation Plugin Example repo, which uses Angular CLI. I can't figure out how to share a singleton service from a shared local Angular library between my two apps.

I'll do my best to explain my setup.. Real sorry about how long this is going to be.

Simplified File Structure

root
  package.json
  projects/
    shell/
      src/app/
        app.module.ts
        app.component.ts
      webpack.config.ts <-- partial config
    mfe1/
      src/app/
        app.module.ts
        app.component.ts
      webpack.config.ts <-- partial config
    shared/
      src/lib/
        global.service.ts
      package.json <-- package.json for lib

Angular

both app.component.ts files are identical

import {GlobalService} from 'shared';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html'
})
export class AppComponent {
  constructor(shared: SharedService) {}
}

global.service.ts

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class GlobalService {

  constructor() {
    console.log('constructed SharedService');
  }
}

Webpack

shell/webpack.config.ts

const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

module.exports = {
  output: {
    publicPath: "http://localhost:5000/",
    uniqueName: "shell"
  },
  optimization: {
    // Only needed to bypass a temporary bug
    runtimeChunk: false
  },
  plugins: [
    new ModuleFederationPlugin({
      remotes: {
        'mfe1': "mfe1@http://localhost:3000/remoteEntry.js"
      },
      shared: ["@angular/core", "@angular/common", "@angular/router"]
    })
  ],
};

mfe1/webpack.config.ts

const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

module.exports = {
  output: {
    publicPath: "http://localhost:3000/",
    uniqueName: "mfe1"
  },
  optimization: {
    // Only needed to bypass a temporary bug
    runtimeChunk: false
  },
  plugins: [
    new ModuleFederationPlugin({

      // For remotes (please adjust)
      name: "mfe1",
      library: {type: "var", name: "mfe1"},
      filename: "remoteEntry.js",
      exposes: {
        './Module': './projects/mfe1/src/app/app.module.ts',
      },
      shared: ["@angular/core", "@angular/common", "@angular/router"]
    })
  ],
};

shared library package.json

{
  "name": "shared",
  "version": "0.0.1",
  "main": "src/public-api.ts",
  "peerDependencies": {
    "@angular/common": "^11.0.0-next.5",
    "@angular/core": "^11.0.0-next.5"
  },
  "dependencies": {
    "tslib": "^2.0.0"
  }
}

This configuration compiles and runs, but mfe1 instantiates a new GlobalService. I see "constructed SharedService" logged on app load, then again as soon as the remote module is loaded. I tried to follow another example by ScriptedAlchemy, but I can't figure out how to make it work. It either compiles and runs and creates two instances, or it fails to compile citing a missing module depending on how i mess up my configuration I'm sure.

The ScriptedAlchemy example makes it seem like I need to reference my shared library in my shared library array in the webpack.config.ts files. This makes complete sense but i can't get it to work

shell/webpack.config.ts and mfe1/webpack.config.ts
  ...
  shared: ["@angular/core", "@angular/common", "@angular/router", "shared"]

If I reference the local library this way I inevitably end up with errors during builds

Error: Module not found: Error: Can't resolve 'shared' in '/Path/to/module-federation-plugin-example/projects/shell/src/app'

The examples I posted are simplified. Hopefully not overly so, but here is a link to a repo that shows the issue

1

There are 1 answers

5
Mike On BEST ANSWER

TL;DR

  • Make sure any module dependencies your shared services have are also shared.
  • Make sure you've structured your shared config correctly in webpack.config.ts

I'm going to note that my project is an NX Monorepo using Angular CLI. I'm using (at the moment) Angular 11.0.0-next and Webpack 5, which is only available as an opt-in with ng11 at the time of writing.

If you're using path aliases in your tsconfig, you are used to importing local libraries like "@common/my-lib", but you can't share modules by alias in your webpack config. Furthermore, if you're on NX, your linting will complain if you import from absolute or relative library paths, so there is a disconnect between what Webpack wants, and what nx/tslint wants.

In my project I have some library aliases like the following

tsconfig.base.json
...
"paths": {
    "@common/facades": [
        "libs/common/facades/src/index.ts"
    ],
    "@common/data-access": [
        "libs/common/data-access/src/index.ts"
    ]
}

To make this work in my webpack config. We have to use those aliases, but tell webpack where to find the libraries by using the import option

apps/shell/webpack.config.js
...
plugins: [
    new ModuleFederationPlugin({
        remotes: {
            'dashboard': 'dashboard@http://localhost:8010/remoteEntry.js',
            'reputation': 'reputation@http://localhost:8020/remoteEntry.js'
        },
        shared:         {
            "@angular/core": {
                "singleton": true
            },
            "@angular/common": {
                "singleton": true
            },
            "@angular/router": {
                "singleton": true
            },
            "@angular/material/icon": {
                "singleton": true
            },
            "@common/data-access": {
                "singleton": true,
                "import": "libs/common/data-access/src/index"
            },
            "@common/facades": {
                "singleton": true,
                "import": "libs/common/facades/src/index"
            },
            "rxjs": {
                "singleton": true
            },
            "ramda": {
                "singleton": true
            }
        }
    })
]

That solved the issues I was having with Webpack being unable to find modules at compile time.

My second problem was that I was attempting to share common facades that depended on common data access services. I didn't think to share the data access services because they are stateless, but this caused my MFEs to instantiate new singletons. This must be because the shared services had 'unshared' dependencies. Sharing my data access layer along with my facade layer resulted in shared singletons throughout my app.

The issue of dependencies is the most important thing here, because it's harder to debug. There are no errors or anything -- things just don't work like you expect. You may not even realize that you have two instances of a shared service at first. So, if you run into this issue take a look at your dependencies and make sure you're sharing everything you need. This is probably an argument for keeping minimal shared state/dependencies across your apps.

  • a quick note about @angular/material/icon share config. In my app I populate the MatIconRegistry with icons that every app needs. I had to share the @angular/material/icon specifically to share the singleton across every MFE.