angularwebpack-module-federation

Angular Module Federation: How to configure isolated routing for each remote module?


I would like to implement an isolated routing for each remote module of a Webpack Module-Federation Angular application.

Each remote has its own routing module, but depending on the URL passed to router.navigate or routerLink, a remote can override also the base URL which should be exclusively in charge of the host/shell application.

E.g.

What I want:

How can we limit the behaviour of each remote module routing so that it is only able to perform navigation relatively to its own paths, without allowing it to interfere with the other remotes and the shell/host application?

Update

Based on some articles I found

it seems that the closer solution could be as follows:

If your Micro Frontend brings its own router, you need to tell your shell that the Micro Frontend will append further segments to the URL. For this, you can go with the startsWith matcher also provided by @angular-architects/module-federation-tools:

import { 
    startsWith, 
    WebComponentWrapper, 
    WebComponentWrapperOptions 
} 
from '@angular-architects/module-federation-tools';

[...]

export const APP_ROUTES: Routes = [
    [...]
    {
        matcher: startsWith('angular3'),
        component: WebComponentWrapper,
        data: {
            [...]
        } as WebComponentWrapperOptions
    },
    [...]
}

To make this work, the path prefix angular3 used here needs to be used by the Micro Frontend too. As the routing config is just a data structure, you will find ways to add it dynamically.

Could you explain further how this solution works and if it could meet my requirements?


Solution

  • The solution depends on how tightly you want to couple your remotes and your shell and where do you want to put the control over the routes and navigation. One approach that I have used successfully is as follows:

    Remote App Place the portion of your remote that you want to expose into a dedicated NG Module FeatureModule, which imports RouterModule.forChild and setups routes for its components.

    src/app/
         |-app.module.ts  # your main module, - not exposed as mf remote 
         |-feature.module.ts   # your feature module, exposed as mf remote
           |-orders.component.ts
           |-remote-a.component.ts
         ...
    

    When you run your Remote-A app as standalone app, then you can import your FeatureModule eagerly into the main AppModule (or lazily, your call depending on use case, if your have more modules etc). If you use standard(eager) import, then you can setup RouterModule.forRoot in your root module, to navigate to the components of your FeatureModule with whatever paths you want. If you import lazily, then the routes defined in the RouterModule.forChild will be relative to the base path under which this module is lazily loaded to the root router. You need to make sure, that everywhere you navigate inside your feature module, you use relative navigation(for example this.router.navigate(['items'], { relativeTo: this.route })). For example, let say that you have 2 components in your Remote-A project and 2 corresponding routes in the FeatureModule:

    const MY_ROUTES =[
    { 
        path: '', 
        component: RemoteAComponent, 
        pathMatch: 'full'
    },
    { 
        path: 'orders', 
        component: OrdersComponent
    }
    ]
    

    In eager mode you can setup this routes in AppModule via RouterModule.forRoot(MY_ROUTES) and then you can use http://localhost:4201 or http://localhost:4201/orders to navigate to those routes(provided you did not set a different BASE_HREF...). Or, you can use lazy loading to setup those routes as sub-routes of some root path e.g.

    //app.module.tsm import section:
    RouterModule.forRoot([
        {
            path: '',
            component: HomeComponent
        },
        {
            path: 'feature',
            loadChildren: () => import('./feature.module').then(m => m.FeatureModule)
        }
    ])
    
    //feature.module.ts
    RouterModule.forChild(MY_ROUTES)
    

    Now to access the OrdersComponent you would use http://localhost:4201/feature/orders as all routes defined in our FeatureModule would be relative to(children of) the parent route, in this case /feature/

    This way, it does not matter under what root URL your FeatureModule is loaded, its internal routes will be resolved correctly relative to the root url.

    Same for other remotes, make sure you extract the actual remote module into dedicated angular module, with forChild router setup.

    Shell: You need to define your available remotes somehow - either static config, or dynamically construct it ,e.g by making an HTTP call to some registry endpoint, loading JSON file etc.

    const microFrontends = [
        {
            baseUrl: "remote-a", //where should we append this to router
            moduleName: "FeatureAModule" //name of NG module class in your remote
            remoteEntry: 'http://localhost:4201/remoteEntry.js', //remote webpack url
            remoteName: 'remotea', //name of the remote module, 
            exposedModule: './FeatureAModule', //exposed module inside the webpack remote
        },
        {
            baseUrl: "remote-b",
            moduleName: "FeatureBModule"
            remoteEntry: 'http://localhost:4202/remoteEntry.js',
            remoteName: 'remoteb',
            exposedModule: './FeatureBModule',
        }
    ]
    

    Next, you define the routes - both internal to components within your shell app and the routes for remote modules - for remotes you only specify the root path under which the RemoteModule should insert its children route and then you specify loadChildren attribute using the loadRemoteModule fn from Manfred Steyers @angular-architects/module-federation lib. For example:

    //app.module.ts
    RouterModule.forRoot([
        { 
            path: '', //this is an example internal route to component that exists inside shell app
            component: HomeComponent,
            pathMatch: 'full' 
        },
        //we map each microfrontend entry to a lazy loaded Route object, 
        ...microFrontends.map(mf=> ({
            path: mf.baseUrl, // we insert any routes defined in the remote module we load 
            //as children of `mf.baseUrl` route
            loadChildren: () => loadRemoteModule(mf).then(m => m[mf.moduleName])
        }))
    ])
    

    With the example config above, the shell would end up with the following Route tree:

    /                  -> shell/HomeComponent.ts
    /remote-a          -> lazy loaded module FeatureAModule from remote-a remote
        /            -> remote-a/FeatureModule/remote-a.component.ts
        /orders      -> remote-a/FeatureModule/orders.component.ts         
    

    The shell can mount the remote module under any URL, for example foobar and then to navigate to orders component form remote-a we would use /foobar/orders. So the shell has complete control over the root routes/url structure, and the remote controls the routes/urls within its subtree.

    If you ever need to make absolute navigation, or communicate between Shell and Remotes, create a shared angular library, with shared angular module and communicate by a singleton Service (providedIn: root).

    Right now you can not pass parameters or DI tokens to the remote module when you load it(there is an issue open for this in angular), so if your child/remote module needs to know, under what URL/route it has been mounted, you would need to either naively parse the browser URL and assume that the first segment of the path is your root path, or use a shared service(in shell, when you call loadChildren you can put some values to the service, and in your remote module you can read them)