Hello World With The CLI, AoT, Lazy Loading Routes, Differential Loading, And Ivy In Angular 8.1.0-beta.2
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:
- Generate the Angular app with
ng new
. - Replace all single-quotes with double-quotes.
- Replace all 2-space indentation with Tab indentation.
- Remove any references to linting and testing.
- Move "routing module" into app module.
- Upgrade all Angular packages to use
"next"
version. - Add
enableIvy: true
totsconfig.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:
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.
Want to use code from this post? Check out the license.
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 newimport()
syntax. I was getting the following error when the lazy-loaded route was being accessed: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 thenext
version of Angular. I went with the latter, as shown in the demo.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?
@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 likenpm run ng
, ornpx 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.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!
@Charles,
Ha ha, one can always find excuses :P I know that move very well!
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
@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).