Skip to main content
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: Jake Scott
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: Jake Scott

Updating InVision Router Experiment To Use Lazy Loading Feature Modules In Angular 6.1.9

By
Published in

Earlier this year, I embarked on an exciting adventure in trying to recreate the InVision app user interface (UI) with the new Angular 5 router. It took me several months to get it done, taking many tangents along the way to solve intermediary Angular problems. But, I have to say that I was very proud of the final result I was able to build. That said, now that I've been experimenting with lazy-loading feature modules in Angular 6, I wanted to revisit my InVision experiment, and update it to use Ahead of Time (AoT) compiling and lazy-loading feature modules.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

When it comes to my InVision interface experiment, I didn't want all of the feature modules to be lazy-loaded. After all, every lazy-loaded feature module represents a possible point of failure. If the user's network were to fail at exactly the wrong moment then the Angular app would be crippled. In fact, that kind of failure is something I need to start experimenting with.

That said, I wanted some "core" interfaces to be included with the primary bundle; then, defer loading on features that may or may not be accessed by the user. To be honest, I didn't have a well-articulated plan on this front: I just statically loaded the primary landing pages and deferred features that would "take a few clicks" to get to.

What I really enjoyed was that the pattern of route configuration encapsulation that I developed really kept the module isolated. In my approach, a parent module never has to know if its children are lazy-loaded. This is because each child module exports both its own route configuration and its own module configuration.

To see what I mean, take a look at this "Shell Module". The shell module contains child modules that are a mixture of lazy-loaded and statically-loaded modules:

// Import the core angular services.
import { NgModule } from "@angular/core";
import { Routes } from "@angular/router";

// Import the application components and services.
import { BoardsView } from "./boards-view/boards-view.module";
import { ConsoleView } from "./console-view/console-view.module";
import { FreehandsView } from "./freehands-view/freehands-view.module";
import { InboxView } from "./inbox-view/inbox-view.module";
import { ModalView } from "./modal-view/modal-view.module";
import { OopsView } from "./oops-view/oops-view.module";
import { ProductUpdatesView } from "./product-updates-view/product-updates-view.module";
import { RoutableView } from "~/app/app.module";
import { SharedModule } from "~/app/shared/shared.module";
import { ShellViewComponent } from "./shell-view.component";
import { StandardView } from "./standard-view/standard-view.module";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@NgModule({
	imports: [
		SharedModule,
		// NOTE: When a routing module is statically included, then the routing module
		// needs to be explicitly imported. In order to not worry about this divergence,
		// let's let the child module define the importable modules (which may or may
		// not be an EMPTY ARRAY - empty if lazy-loaded).
		...BoardsView.modules,
		...ConsoleView.modules,
		...FreehandsView.modules,
		...InboxView.modules,
		...ModalView.modules,
		...OopsView.modules,
		...ProductUpdatesView.modules,
		...StandardView.modules
	],
	declarations: [
		ShellViewComponent
	]
})
export class ShellViewModule {
	// ...
}

export var ShellView: RoutableView = {
	modules: [
		// NOTE: Since this module's routes are being included directly in the parent
		// module's router definition, we need to tell the parent module to import this
		// module. Otherwise, the application won't know about the declared components
		// and services.
		ShellViewModule
	],
	routes: [
		{
			// NOTE: Normally, I wouldn't include a "path" here because I would defer to
			// the child routes to define their own relevant prefix. However, since the
			// ShellView component has several NAMED OUTLETs (Inbox, Modal), we have to
			// provide a path or the named outlets will break.
			// --
			// Read More: https://github.com/angular/angular/issues/14662
			path: "app",
			children: [
				...BoardsView.routes,
				...ConsoleView.routes,
				...InboxView.routes,
				...ModalView.routes,
				...OopsView.routes,
				...ProductUpdatesView.routes,
				...StandardView.routes,
				...FreehandsView.routes,

				// Handle the "no route" case.
				{
					path: "",
					pathMatch: "full",
					redirectTo: "projects"
				}
			]
		},

		// Handle the "no route" case.
		{
			path: "",
			pathMatch: "full",
			redirectTo: "app/projects"
		},

		// Handle the catch-all for not found routes.
		{
			path: "**",
			redirectTo: "/app/oops/not-found"
		}
	]
};

As you can see, there's no difference in semantics between the lazy-loaded modules and the statically-loaded modules. That's because each child view is responsible for exporting its own modules:

...ModalView.modules

and, exporting it's own routes:

...ModalView.routes

This means that I can transition a module back-and-forth between loading techniques and the parent module never has to be changed.

So, for example, here's a statically-loaded feature module:

// Import the core angular services.
import { NgModule } from "@angular/core";
import { Routes } from "@angular/router";

// Import the application components and services.
import { NotFoundViewComponent } from "./not-found-view/not-found-view.component";
import { OopsViewComponent } from "./oops-view.component";
import { RoutableView } from "~/app/app.module";
import { SharedModule } from "~/app/shared/shared.module";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@NgModule({
	imports: [
		SharedModule
	],
	declarations: [
		NotFoundViewComponent,
		OopsViewComponent
	]
})
export class OopsViewModule {
	// ...
}

export var OopsView: RoutableView = {
	modules: [
		// NOTE: Since this module's routes are being included directly in the parent
		// module's router definition, we need to tell the parent module to import this
		// module. Otherwise, the application won't know about the declared components
		// and services.
		OopsViewModule
	],
	routes: [
		{
			path: "oops",
			component: OopsViewComponent,
			children: [
				{
					path: "not-found",
					component: NotFoundViewComponent
				},

				// Handle the....
				{
					path: "",
					pathMatch: "full",
					redirectTo: "not-found"
				}
			]
		}
	]
};

Notice that it exports both a populated .modules property and a populated .routes property. These values then get "spread" into the configuration of the parent module.

Now, contrast that with a lazy-loaded feature module in the same parent:

// Import the core angular services.
import { NgModule } from "@angular/core";
import { RouterModule } from "@angular/router";
import { Routes } from "@angular/router";

// Import the application components and services.
import { DetailViewComponent } from "./detail-view/detail-view.component";
import { ListViewComponent } from "./list-view/list-view.component";
import { ProductUpdatesViewComponent } from "./product-updates-view.component";
import { RoutableView } from "~/app/app.module";
import { SharedModule } from "~/app/shared/shared.module";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

@NgModule({
	imports: [
		SharedModule,
		// --
		RouterModule.forChild([
			{
				// NOTE: Since this module is being lazy-loaded, the root segment has
				// already been defined (as part of the lazy-load configuration). As
				// such, the root segment here is empty.
				path: "",
				component: ProductUpdatesViewComponent,
				children: [
					{
						path: "",
						pathMatch: "full",
						component: ListViewComponent
					},
					{
						path: ":id",
						component: DetailViewComponent
					}
				]
			}
		])
	],
	declarations: [
		DetailViewComponent,
		ListViewComponent,
		ProductUpdatesViewComponent
	]
})
export class ProductUpdatesViewModule {
	// ...
}

export var ProductUpdatesView: RoutableView = {
	modules: [
		// NOTE: Since this module is being lazy-loaded, the parent module does not
		// need to know about it - Angular will handle the integration when it loads
		// the remote files.
	],
	routes: [
		{
			outlet: "updates",
			path: "product-updates",
			loadChildren: "./views/product-updates-view/product-updates-view.module#ProductUpdatesViewModule"
		}
	]
};

As you can see, this module also exports a .modules and .routes property; but, in this case, the .modules property is empty and the .routes property simply points to a lazy-loaded module (itself). Due to the consist export structure, however, the parent module never has to care - it simply imports the feature module configuration and merges it into its own configuration.

With all of this in place, if we load my InVision Angular application, we get the initial bundles:

Lazy loading feature modules, showing intialt bundles in Angular 6.1.9.

Then, as we navigate to other portions of the application, we can see that the lazy-loaded feature modules are pulled from the server as-needed and merged into the active Angular application:

Lazy loading feature modules are loaded on-demand, from the server, in Angular 6.1.9.

How awesome is that? And, it's incredible easy to switch the loading strategy for each feature module.

Frankly, I don't know how the mechanics of this work. Webpack and the "@ngtools/webpack" plug-in do some sort of voodoo magic to make sure that lazy-loaded modules are compiled into their own bundles. I don't know how they do that. I don't know Angular makes the HTTP request to load the remote modules. I don't know how Angular merges the new configurations into the active application. That's just the beauty and the power of the Angular web application framework. It's sweet ass sweet!

Want to use code from this post? Check out the license.

Reader Comments

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel