Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Lisa Tierney
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Lisa Tierney

Hello World With The CLI, AoT, Lazy Loading Routes, Differential Loading, And Ivy In Angular 8.1.0-beta.2

By Ben Nadel on

Up until now, all of my recent Angular demos have been built using a small Webpack configuration file that consumed the AngularCompilerPlugin() provided by @ngtools/webpack. But, with the introduction of Angular 8, there are features that don't fit neatly into my approach. Features like "differential loading" and the Ivy renderer require more than a few configuration tweaks. As such, I've decided to stop fighting the Angular CLI (Command-Line Interface) and try to embrace it. This post is just a note to self on how I got that working.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

If you've followed my blog for any amount of time, you'll likely get the sense that I have a very specific way of formatting my code. That's not an accident; it's a format that I've been refining and redefining my whole career. It's why I am vehemently opposed to things like prettier and gofmt and, essentially, any linting rule that pertains to non-functional issues.

This is a big part of why I have avoided the Angular CLI for so long - the fact that it generates files using 2-space indentation was as sufficient deterrent. But, now that I need the CLI, I've decided to generate my Angular demo and then brute-force my way to formatting that I can live with.

I took the following steps:

  1. Generate the Angular app with ng new.
  2. Replace all single-quotes with double-quotes.
  3. Replace all 2-space indentation with Tab indentation.
  4. Remove any references to linting and testing.
  5. Move "routing module" into app module.
  6. Upgrade all Angular packages to use "next" version.
  7. Add enableIvy: true to tsconfig.json file.

ASIDE: To be clear, I am only removing all of the testing code because this setup is for my demos and I don't need that stuff for my demos. The demo is the test. I don't need tests to test the tests :D

With that said, let's take a look at what I got working. Like I said above, I tried to strip out as much as I possibly could from the generated code. This includes many of the npm dependencies:

{
	"name": "webpack4-angular8-cli",
	"version": "0.0.0",
	"scripts": {
		"build": "ng build --prod",
		"ng": "ng",
		"start": "ng serve --open",
		"start-prod": "ng serve --open --prod"
	},
	"private": true,
	"dependencies": {
		"@angular/animations": "next",
		"@angular/common": "next",
		"@angular/compiler": "next",
		"@angular/core": "next",
		"@angular/forms": "next",
		"@angular/platform-browser": "next",
		"@angular/platform-browser-dynamic": "next",
		"@angular/router": "next",
		"rxjs": "6.4.0",
		"tslib": "1.9.0",
		"zone.js": "0.9.1"
	},
	"devDependencies": {
		"@angular-devkit/build-angular": "next",
		"@angular/cli": "next",
		"@angular/compiler-cli": "next",
		"@angular/language-service": "next",
		"@types/node": "~8.9.4",
		"typescript": "~3.4.3"
	}
}

Because I am not doing any linting or testing of my Angular demos, I was able to strip out a ton of dependencies! Notice that I am using next for all of my Angular packages. This is what the Angular team recommends for the Ivy renderer. Apparently you can get Ivy to work without next; but, I was not able to do so in my testing. But, this is my first attempt at consuming the Angular CLI, so your mileage may vary.

Once I had next in place, I was able to add enableIvy to my tsconfig.json file:

{
	"angularCompilerOptions": {
		"enableIvy": true
	},
	"compileOnSave": false,
	"compilerOptions": {
		"baseUrl": "./",
		"declaration": false,
		"downlevelIteration": true,
		"emitDecoratorMetadata": true,
		"experimentalDecorators": true,
		"importHelpers": true,
		"lib": [
			"es2018",
			"dom"
		],
		"module": "esnext",
		"moduleResolution": "node",
		"noImplicitAny": true,
		"outDir": "./dist/out-tsc",
		"pretty": true,
		"removeComments": false,
		"sourceMap": true,
		"target": "es2015",
		"typeRoots": [
			"node_modules/@types"
		],
		"types": []
	},
	"include": [
		"src/**/*.ts"
	]
}

I believe that this tsconfig.json file also helps with the differential loading feature. But, I'm still getting my bearing on all of this.

Now, instead of trying to use a Webpack configuration file directly, I'm just using the angular.json that is generated and consumed by the Angular CLI. In this file, I've tried to remove as much as I could; but, I'm not completely confident that I got it down to the bare minimum.

{
	"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
	"version": 1,
	"newProjectRoot": "projects",
	"projects": {
		"demo": {
			"projectType": "application",
			"schematics": {
				"@schematics/angular:component": {
					"style": "less"
				}
			},
			"root": "",
			"sourceRoot": "src",
			"prefix": "app",
			"architect": {
				"build": {
					"builder": "@angular-devkit/build-angular:browser",
					"options": {
						"outputPath": "dist/",
						"index": "src/index.htm",
						"main": "src/main.ts",
						"polyfills": "src/polyfills.ts",
						"tsConfig": "tsconfig.json",
						"assets": [
							"src/assets"
						],
						"styles": [],
						"scripts": []
					},
					"configurations": {
						"production": {
							"fileReplacements": [
								{
									"replace": "src/environments/environment.ts",
									"with": "src/environments/environment.prod.ts"
								}
							],
							"optimization": true,
							"outputHashing": "all",
							"sourceMap": false,
							"extractCss": true,
							"namedChunks": false,
							"aot": true,
							"extractLicenses": false,
							"vendorChunk": false,
							"buildOptimizer": true,
							"budgets": [
								{
									"type": "initial",
									"maximumWarning": "2mb",
									"maximumError": "5mb"
								}
							]
						}
					}
				},
				"serve": {
					"builder": "@angular-devkit/build-angular:dev-server",
					"options": {
						"browserTarget": "demo:build"
					},
					"configurations": {
						"production": {
							"optimization": true,
							"sourceMap": true,
							"aot": true
						}
					}
				}
			}
		}},
	"defaultProject": "demo"
}

One thing that I noticed was that the application name that I used when running the ng new command was embedded in this file. However, since I intend to copy-paste this directory for future Angular demos, I replaced all of the original application names (webpack-angular8-cli) with demo. Now, when I duplicate this folder, the naming within this file won't be confusing.

With the Angular project configuration in place, we can finally look at some code. One of the features that I wanted to be sure to try was the new use of import() for lazy-loading modules. As such, I created two modules for this demo: the App module and a Lazy module, both of which have a single component.

Here's the App module:

// Import the core angular services.
import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { RouterModule } from "@angular/router";

// Import the application components and services.
import { AppComponent } from "./app.component";

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

@NgModule({
	imports: [
		BrowserModule,
		RouterModule.forRoot(
			[
				{
					path: "lazy",
					loadChildren: async () => {

						return( ( await import( "./lazy.module" ) ).LazyModule );

					}
				}
			],
			{
				// Tell the router to use the hash instead of HTML5 pushstate.
				useHash: true,

				// Enable the Angular 6+ router features for scrolling and anchors.
				scrollPositionRestoration: "enabled",
				anchorScrolling: "enabled",
				enableTracing: false
			}
		)
	],
	providers: [
		// CAUTION: We don't need to specify the LocationStrategy because we are setting
		// the "useHash" property in the Router module above (which will be setting the
		// strategy provider for us).
		// --
		// {
		// 	provide: LocationStrategy,
		// 	useClass: HashLocationStrategy
		// }
	],
	declarations: [
		AppComponent
	],
	bootstrap: [
		AppComponent
	]
})
export class AppModule {
	// ...
}

For lazy-loaded modules with the import() function, we have to provide the loadChildren property with a Function that returns the Promise of the targeted module. These import() calls are automatically detected during the Angular build and are used to perform code-splitting.

In a lot of Angular 8 demo code, that loadChildren call looks more like this:

() => import('./my.module').then(mod => mod.MyModule)

To be clear, my use of async/await is doing the exact same thing as the line above. I just don't like fat-arrow functions that have no body and use implicit return statements. Like I said before, I have a very particular set of formatting requirements. I am not completely sure that I love my current approach; so, it may evolve over time as I get more experience with the Angular 8 Router. However, for now, it suits me just fine.

The LazyModule itself is fairly simple:

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

// Import the application components and services.
import { LazyComponent } from "./lazy.component";

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

@NgModule({
	imports: [
		RouterModule.forChild([
			{
				path: "",
				component: LazyComponent
			}
		])
	],
	declarations: [
		LazyComponent
	]
})
export class LazyModule {
	// ...
}

The App component and the Lazy component are barely worth showing. But, for completeness, here they are:

// Import the core angular services.
import { Component } from "@angular/core";

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

@Component({
	selector: "app-root",
	styleUrls: [ "./app.component.less" ],
	template:
	`
		<p>
			I am the App component.
		</p>

		<p>
			<a routerLink="./lazy">Load Lazy Route</a>
		</p>

		<router-outlet></router-outlet>
	`
})
export class AppComponent {
	// ....
}

And, the Lazy component:

// Import the core angular services.
import { Component } from "@angular/core";

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

@Component({
	selector: "app-lazy",
	styleUrls: [ "./lazy.component.less" ],
	template:
	`
		<p>
			I am the Lazy component!
		</p>

		<p>
			<img
				src="assets/like-a-boss.gif"
				width="498"
				height="226"
				alt="Animated GIF of the Like a Boss skit from Saturday Night Live."
			/>
		</p>
	`
})
export class LazyComponent {
	// ....
}

Now, if we run this Angular 8 application in the browser and navigate to the "lazy" route, we can see that it is loaded on-demand over the network:

An Angular 8 demo app that shows off the lazy-loading

If you look at the files being loaded, you can see that they all contain es2015 in the filename. This is the differential loading feature in action. At build-time, Angular produces two sets of bundles: one that uses the older target and Polyfills; and, one that assumes a modern browser syntax and feature-set. The latter is, of course, smaller in size and [hopefully] easier and more efficient for the browser to parse.

Also notice that the size of the various JavaScript bundles are relatively small. With gzip compression (provided by GitHub Pages), the main bundle is only 89Kb, leading to a total JavaScript size of only 106Kb. If I compare this to my last Angular 7 demo, which was 162Kb of JavaScript, that's like a 30% drop in payload size. It's not exactly an apples-to-apples comparison; but, this it the kind of benefit we're going to be getting from the Ivy renderer in the future!

This Angular 8 demo, complete with Ahead of Time (AoT) compiling, lazy-loading or routes, differential loading, and the Ivy renderer will lay the foundation for my Angular demos going forward. Of course, this is the first time that I've tried either Angular 8 or the Angular CLI. As such, please take this post with a grain of salt - nothing I've said or demonstrated here is based on any real-world experience. Of course, now that I have the Angular CLI in place, I'm hoping to start experimenting with more of the modern features (like PWA) that Angular has to offer.



Reader Comments

@All,

At first, I tried to get all of this working without the next version of Angular. However, I was running into problems with Lazy-loading routes and AoT compiling and the new import() syntax. I was getting the following error when the lazy-loaded route was being accessed:

Error: Runtime compiler is not loaded

I assume this means that Angular needed the JIT (Just in Time) compiler for lazy-loaded routes when using import(). To fix it, I could either roll-back to using the #MyModule "magic string" in the Router config; or, I could roll-forward to using the next version of Angular. I went with the latter, as shown in the demo.

Reply to this Comment

On your Twitter feed. I couldn't understand why you were taking all these steps. But, I know realise that you want to format your code in a particular way. Thankfully, I just use whatever kind of formatting, I'm presented with. I guess this is because I have worked for so many different kinds of companies with developers, who use a variety of formatting approaches. It's interesting that you prefer double quotes. Personally, I prefer single quotes, because, if you are adding JavaScript generated HTML content, tag attributes usually use double quotes. I think I switched to using single quotes when I started using Angular, because my Linter, at the time, kept complaining about my use of double quotes. I'm so easily persuaded;)

Anyway, I am glad you got everything set up OK. I am looking forward to building a new project with Angular 8, and the new compression benefits of Ivy.

Just out of interest, is it possible to use different versions of the Angular CLI, on a single computer? I am never too sure about global VS local use of Angular. I think all my projects run locally, in their own sandboxed folders. But, I am concerned about installing Angular 8 with a new project, in case it messes with any of my existing Angular 7 projects?

Reply to this Comment

@Charles,

I'm just a man that feels very strongly about code formatting :D Probably just a mental defect ;P

As far as the CLI, that's a good question. I am sure there is a way to do it - run multiple versions - but, it might require a better understand npm than I have. For example, when generating the project for the first time, I used the globally installed CLI. But, then, once the project has been created, you will have the CLI installed in the local node_modules folder, and can probably invoke it using something like npm run ng, or npx ng, or something like that. In that case, you can have whichever version of the CLI you want installed in the existing project (or whatever is required for the version of Angular).

There's almost certainly some way to have different versions of the CLI globally. But, again, I'm not that good at npm. Worst case scenario, you could just have a given version installed somewhere, and then invoke it with an explicit path to the ng binary, rather than relying on the binary paths.

Reply to this Comment

Cheers Ben. Yes. I think you are correct about the CLI stuff. From what I have read, you can install the most up to date version locally for each new project, after the first Global installation. It's kind of cool that way.

So, I have no excuses now!

Reply to this Comment

How can I add hash towards the end of lazyloading files?
ex: path/to/file.js?[hash]

angular-cli(abstract of webpack) adding has in the file name like file.[hash].js

Reply to this Comment

@Priyabrata,

Honestly, I have no idea. Part of the trade-off of using the CLI, as opposed to using Webpack directly, is that there is a new layer of abstraction that hides a lot of stuff from you (and does work for you, obviously). I know that in my old Webpack config JS file, I was able to explicitly add the hash to the entry point file-names. However, with NG8, and the CLI, and all the "differential loading" that it is doing (creating different bundles for different types of browsers), we are far removed from the filename.

Of course, this is my first real exposure to the CLI tool; so, it's possible that there are options for filename type stuff in the angular.json; but, I just don't know what they are (or if they even exist).

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.