Serving styles and assets with Webpack 5 module federation

Viewed 5690

I've successfully implemented the relatively new webpack 5 module federation system in my Angular 11 app, so it can load modules remotely on-demand from another build.

One thing I've found nothing about is how to handle assets like stylesheets and images. For example, there's a menu element in the federated module that requires its own styles:

  • Putting them in the component's stylesheet bloats the chunks and the compiler complains about that, plus they're not loaded until the menu is shown
  • If the styles are instead on the federated module's global stylesheet, they don't get loaded at all, because I'm requesting a sub-module and not the main one (I presume)
  • The style is specific to the federated module, so it can't be put in the loader application

I suppose that the styles could be compiled and put in the federated module's build assets, but that'd break links when it's used with and without federation.

I'm still experimenting with this, but I thought it'd be good to ask. Anybody had this issue?

3 Answers

I think the most elegant way to achieve that would be to import your global styles.scss of the micro frontend into it's entry module component.scss and in your entry module component.ts set encapsulation: ViewEncapsulation.None in order to break style encapsulation for it, which will in turn lead for that styles to be applied globally.

Well, I'm gonna post what I came up with, it's not pretty, but it seems to work fine for CSS assets.

First of all, I separated them in the remote module's build: angular.json:

"styles": [
  "projects/xxx-admin/src/styles/styles.scss",
  "projects/xxx-admin/src/styles/admin.scss",
  {
    "input": "projects/xxx-admin/src/styles/admin.scss",
    "bundleName": "admin_module_styles",
    "inject": false
  }
],

This generates a CSS with a clear non-autogenerated name. Then we load the federated module from the app in the routes:

{
  path: "admin",
  component: AdminPanelComponent,
  canActivate: [XxxAdminGuard],
  loadChildren: () => {
    const baseUrl = getAdminFrontendBaseUrl();
    return loadAdminStyles().then(
      () => loadRemoteModule({
        remoteName: "xxx_admin",
        remoteEntry: `${baseUrl}/remoteEntry.js`,
        exposedModule: "AdminModule",
      }).then((m) => m.AdminModule));
  },
},

...

export function loadAdminStyles(): Promise<void> {
  return new Promise((resolve => {
    const baseUrl = getAdminFrontendBaseUrl();
    const el = document.getElementById("admin-module-styles");

    // Load one instance, do it like this to handle errors and retrying
    if (el) {
      el.remove();
    }
    const headEl = document.getElementsByTagName("head")[0];
    const styleLinkEl = document.createElement("link");
    styleLinkEl.rel = "stylesheet";
    styleLinkEl.id = "admin-module-styles";
    styleLinkEl.href = `${baseUrl}/admin_module_styles.css`;
    headEl.appendChild(styleLinkEl);
    resolve();
  }));
}

It's suboptimal, but I couldn't figure anything better.

In microfront, transfer assets to assets/mfe_name. By the name of microfront, you can do proxying from the parent host.

HOST:

"/assets/*mfe_name*/*": {
           "target": "*mfe_host*",
           "secure": true,
           "changeOrigin": true
    },

Start: ng serve --proxyConfig=proxy

So you can get files both from localhost and on the prod to access from a remote host

Related