Code structure when dynamically load a micro frontend via Module Federation

Discussão em 'Angular' iniciado por Enak, Novembro 20, 2024.

  1. Enak

    Enak Guest

    I picked up the git examples of https://www.angulararchitects.io/en...-module-federation-the-good-the-bad-the-ugly/ further referenced as Ex. 1 and https://www.angulararchitects.io/en/aktuelles/dynamic-module-federation-with-angular/ further referenced as Ex. 2.

    Ex. 1 has multiple angular projects in it. For the sake of simplification just use the shell and the mfe1 (micro frontend 1). The example uses a package.json and angular.json for each project, which I want to use either because in the real world, we maybe have plenty of micro frontends and do not want to update all or manage all dependecies at one point. The culprit for me in this example is the fact that it uses a WrapperComponent and a registry.ts. When a route is activated it lazy loads via this function:

    ngAfterContentInit(): void {

    const elementName = this.route.snapshot.data['elementName'];
    const importName = this.route.snapshot.data['importName'];

    const importFn = registry[importName];
    importFn()
    .then(_ => console.debug(`element ${elementName} loaded!`))
    .catch(err => console.error(`error loading ${elementName}:`, err));

    const element = document.createElement(elementName);
    this.vc.nativeElement.appendChild(element);

    }


    The registry contains a mapping like

    export const registry = {
    mfe1: () => import('mfe1/web-components')
    };


    AFAIK there happens some "webpack magic" to do the import and mfe1 is mapped to a different host via webpack.config.js. That means I need to know the the number and names of the micro frontends at compile time.

    Ex. 2 uses a dynamic approach by using a LookupService

    export class LookupService {
    lookup(): Promise<Microfrontend[]> {
    return Promise.resolve([
    {
    // For Loading
    remoteEntry: 'http://localhost:3000/remoteEntry.js',
    remoteName: 'mfe1',
    exposedModule: './Module',

    // For Routing
    displayName: 'Flights',
    routePath: 'flights',
    ngModuleName: 'FlightsModule'
    }
    ] as Microfrontend[]);
    }
    }


    and a function to build the routes

    export function buildRoutes(options: Microfrontend[]): Routes {

    const lazyRoutes: Routes = options.map(o => ({
    path: o.routePath,
    loadChildren: () => loadRemoteModule(o).then(m => m[o.ngModuleName])
    }));

    return [...APP_ROUTES, ...lazyRoutes];
    }


    Now, I want to combine both approaches. So, I copied the approach for Ex. 2 into Ex. 1 and now struggling with the following.

    When simply exposing the AppModule of mfe1 and dynamically load it, I have multiple RouterModule.forRoot() calls, which is not avoidable because mfe1 and shell should also work as standalone. This leads to an error. So, I simply made a "dummy" AppModule, call RouterModule.forRoot() and have a path directing to another module, which is using only RouterModule.forChild() (called the FlightsModule in example). This other FlightsModule is loaded dynamically in shell, hence no more multiple calls to RouterModule.forRoot(). Is this necessary or is there another option?

    Because I only can load a "submodule" of mfe1, I need to shift the content of several common files. mfe1/src/styles.css for global styles needs to be moved to a component always activated by the route with encapsulation: ViewEncapsulation.None to make it globally accessible e.g. when using ngx-toastr this is needed. The styles option in the angular.json can not be used by a micro frontend because it will break when loaded over the shell. Everything normally imported and declared in AppModule needs to be shifted to the submodule. And so on...

    In the end, I think this approach is definetly not the best but I tried so many different things and none, except this one, is working.

    Continue reading...

Compartilhe esta Página