Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Importing a lazy-loaded module with routes in a module with a route breaks routing

Our angular 4.3.6 application has lazy-loaded modules, such as Fleet, Maintenance, Car, etc.

My top-level app router looks like this:

const routes: Routes = [
  { path: '', redirectTo: 'fleet', pathMatch: 'full' },
  {
    path: '',
    component: AppComponent,
    canActivate: [AuthenticationGuard],
    children: [
      {
        path: 'fleet',
        loadChildren: "./modules/fleet.module",
        canActivate: [AuthenticationGuard]
      },
      {
        path: 'car/:id',
        loadChildren: "./modules/car.module",
        canActivate: [AuthenticationGuard]
      },
      {
        path: 'maintenanceProgram',
        loadChildren: "./modules/maintenanceProgram.module",
        canActivate: [AuthenticationGuard]
      }
}

We do have a shared module with generic components (we have lots of them) used throughout the application. There are, however, some components like modals which are shared only by the MaintenanceProgram and the Car modules, but not used anywhere else.

In an effort to keep the shared module reasonable, I include these once-reused components only in the MaintenanceProgram module, export them, and import the MaintenanceProgram module into the Car module to have access to the exported components.

Both the Car and MaintenanceProgram modules have the following embedded child routes invoked in their respective @NgModule.imports[]:

Car:

const CarModuleRoutes = RouterModule.forChild([
  {
    path: '',
    component: CarComponent,
    canActivate: [AuthenticationGuard]
  },
  {
    path: ':id',
    component: CarComponent,
    canActivate: [AuthenticationGuard]
  }
]);

and Maintenance Program:

const MaintenanceProgramModuleRoutes = RouterModule.forChild([
  {
    path: '',
    component: MaintenanceProgramComponent,
    canActivate: [AuthenticationGuard]
  }
]);

This is obviously not the correct approach either

  1. to child routing, or
  2. to module transclusion

because when I load the Car route, I get the Maintenance Program's default route.

I have tried:

  1. Changing the order of the import of MaintenanceProgramModuleRoutes and CarModuleRoutes in CarModule's @NgModule.imports[],
  2. Removing the empty path from CarModule.

*Is the only solution to create another shared module that contains the shared components without a router, or is there another, more elegant way to do this?*

This is a reduced example, we actually have many routes and hundreds of components which are reused only twice or three times. This problem will surely persist into the future as the application grows, so I need a scalable and maintainable solution. Creating tens or more extra shared modules is just infeasible.

like image 211
msanford Avatar asked Sep 01 '17 14:09

msanford


3 Answers

Actually, when creating a shared module, there is no need to care about modules which uses a little part of the shared module, because Angular's tree-shaking will keep only the used code from an imported module, and remove the rest.

I've prepared a minimal project to demonstrate it: https://github.com/youkouleley/Angular-treeshaking-demo

This project has two lazy modules: AModule and BModule. Both of these modules import SharedModule. SharedModule exports three components:

  • AComponent which is used in AModule only
  • BComponent which is used in BModule only
  • BothComponent which is used in AModule and BModule

Here is what you'll get when ng building this project in production mode and opening the 4-es2015.<hash>.js file:

(window.webpackJsonp = window.webpackJsonp || []).push([[4], {
      KImX: function (n, l, u) {
        "use strict";
        u.r(l);
        var t = u("8Y7J");
        class a {
          constructor() {}
          ngOnInit() {}
        }
        var r = u("phyl");
        class o {}
        var c = u("pMnS"),
        s = t.gb({
            encapsulation: 0,
            styles: [[""]],
            data: {}
          });
        function b(n) {
          return t.sb(0, [(n()(), t.ib(0, 0, null, null, 1, "p", [], null, null, null, null, null)), (n()(), t.rb(-1, null, ["a works!"]))], null, null)
        }
        function i(n) {
          return t.sb(0, [(n()(), t.ib(0, 0, null, null, 1, "app-a", [], null, null, null, b, s)), t.hb(1, 114688, null, 0, a, [], null, null)], function (n, l) {
            n(l, 1, 0)
          }, null)
        }
        var p = t.eb("app-a", a, i, {}, {}, []),
        e = u("gJxL"),
        f = u("SVse"),
        h = u("iInd"),
        d = u("PCNd");
        u.d(l, "AModuleNgFactory", function () {
          return v
        });
        var v = t.fb(o, [], function (n) {
            return t.ob([t.pb(512, t.j, t.T, [[8, [c.a, p, e.a]], [3, t.j], t.u]), t.pb(4608, f.i, f.h, [t.r, [2, f.o]]), t.pb(1073742336, f.b, f.b, []), t.pb(1073742336, h.l, h.l, [[2, h.q], [2, h.k]]), t.pb(1073742336, d.a, d.a, []), t.pb(1073742336, o, o, []), t.pb(1024, h.i, function () {
                  return [[{
                        path: "a",
                        component: a
                      }, {
                        path: "both",
                        component: r.a
                      }
                    ]]
                }, [])])
          })
      },
      PCNd: function (n, l, u) {
        "use strict";
        u.d(l, "a", function () {
          return t
        });
        class t {}
      },
      gJxL: function (n, l, u) {
        "use strict";
        var t = u("8Y7J"),
        a = u("phyl");
        u.d(l, "a", function () {
          return s
        });
        var r = t.gb({
            encapsulation: 0,
            styles: [[""]],
            data: {}
          });
        function o(n) {
          return t.sb(0, [(n()(), t.ib(0, 0, null, null, 1, "p", [], null, null, null, null, null)), (n()(), t.rb(-1, null, ["both works!"]))], null, null)
        }
        function c(n) {
          return t.sb(0, [(n()(), t.ib(0, 0, null, null, 1, "app-both", [], null, null, null, o, r)), t.hb(1, 114688, null, 0, a.a, [], null, null)], function (n, l) {
            n(l, 1, 0)
          }, null)
        }
        var s = t.eb("app-both", a.a, c, {}, {}, [])
      },
      phyl: function (n, l, u) {
        "use strict";
        u.d(l, "a", function () {
          return t
        });
        class t {
          constructor() {}
          ngOnInit() {}
        }
      }
    }
  ]);

Note that the BComponent from the SharedModule is missing from the AModule chunk. Sames goes for BModule chunk that excludes AComponent.

Also, note that this behavior is obtained when setting commonChunk to false in the build options. This option allows you to choose between:

  • false: Bundle the needed parts of the SharedModule directly into the lazy modules that imported it. Pro: Even loading time between lazy modules. Con: Some code from SharedModule is duplicated between lazy modules chunks, higher app size overall
  • true (default): Have a common chunk that contains the parts of the SharedModule which are used at least by two lazy modules (the rest is bundled into the lazy modules themselves). Pro: No duplicated code, lower app size overall. Con: The first lazy module is slower to load (it loads the common chunk even if the current route doesn't need it)

As a conclusion, Angular build provides optimizations for the SharedModule with commonChunk set either to true or false (depending on your context) you don't really have to worry about your SharedModule size. Thus, you don't have to try strange patterns like you did, with hybrid modules fulfilling the feature module role, and the shared module role at the same time.

like image 170
Guerric P Avatar answered Oct 06 '22 03:10

Guerric P


Try this in top-level app router:

const routes: Routes = [
  { path: '', redirectTo: 'fleet', pathMatch: 'full' },
  {
    path: '',
    component: AppComponent,
    canActivate: [AuthenticationGuard],
    children: [
      {
        path: 'fleet',
        loadChildren: "./modules/fleet.module#FleetModule",
        canActivate: [AuthenticationGuard]
      },
      {
        path: 'car/:id',
        loadChildren: "./modules/car.module#CarModule",
        canActivate: [AuthenticationGuard]
      },
      {
        path: 'maintenanceProgram',
        loadChildren: "./modules/maintenanceProgram.module#MaintenanceProgrammodule",
        canActivate: [AuthenticationGuard]
      }
}

Your needed change: add #module_class_name in end of router path.

like image 40
Hasan Fathi Avatar answered Oct 06 '22 03:10

Hasan Fathi


The way that I use to handle big shared module is to create a module per component. In your example, you should created a ModalModule, declare and export your ModalComponent. It should look something like this

@NgModule({
  declarations: [
    ModalComponent,
  ],
  imports: [
    RouterModule,
    CommonModule, // any modules that your modal need
  ],
  exports: [
    ModalComponent,
  ]
})
export class ModalModule {}

Then simply include ModalModule in your CarModule imports, and MaintenanceProgramModule imports.

@NgModule({
  declarations: [CarComponent],
  imports: [ CarRoutingModule, ModalModule ],
})
export class CarModule

You should create module for all shared component, and only include each shared component's module in the route that need it. It do help greatly reduced the size of each page.

like image 21
Rinne Hmm Avatar answered Oct 06 '22 04:10

Rinne Hmm