Automatically upgrade lazy-loaded Angular modules for Ivy!
Updated 29/05/2019 — Lazy-loading in Angular before v8.0.0! 💾
AngularInDepth is moving away from Medium. More recent articles are hosted on the new platform inDepth.dev. Thanks for being part of indepth movement!
If you ever created a lazy-loaded module in an Angular app before v8.0.0, then the following code might look pretty familiar to you:
It did the job, but it was fairly magical, and it relied on a special string syntax, and some compiler wizardry in the Angular CLI…
Luckily, web standards have evolved since this syntax was introduced, and there’s now a “better” way to split our app and load each parts on demand!
Thanks to the changes in Angular 8, we can now use the import()
operator to fetch a module as we navigate around our application.
The migration described below got merged into the Angular CLI, so you no longer need to use the lint rule I created. You can just follow the normal ng update
process, and your code will be migrated to the new format. Thanks to other changes by the Angular team, it is even backwards compatible so you can use import
without Ivy!
I’ve left the original article here for reference. Enjoy!
Want it right now? Cool! 🎉
Original:
If you’re already playing with Ivy, you can install a TSLint rule with a fixer to upgrading this automatically:
npm install @phenomnomnominal/angular-lazy-routes-fix -D
Add the following to your tslint.json
:
{
“extends”: [
“@phenomnomnominal/angular-lazy-routes-fix”
],
“//”: “either”,
“no-lazy-module-paths”: [true],
“//”: “or”,
“no-lazy-module-paths”: [true, “async”]
}
And then run:
ng lint --fix
Voilà! All your lazy-loaded routes should be upgraded! 🎉
⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ️️The upgraded code will only work with if the Ivy renderer is enabled, or if your app is running in JIT mode.
What stopped us from doing this up until now?
This next section goes pretty deep into how lazy-loading works in Angular right now, and how it’s going to work in Angular in the future! Most of this is going away, but it’s still pretty interesting!
Lazy-loaded routes in Angular 7.x.x
We use RouterModule.forChild()
and RouterModule.forRoot()
to tell Angular about the route structure of our application. But how does it work? Let’s check out the Angular source and find out!
If we dig into the implementation of RouterModule.forChild()
and RouterModule.forRoot()
, we can see that when we pass in the array of routes, they are registered as a multi provider against the ROUTES
InjectionToken
:
This means that at runtime we’re going to have an injectable array of route configuration objects! But how does Angular use theseROUTES
? Again, let’s use the source:
The ROUTES
are injected into the application’sRouter
when it is created. When Angular encounters a route with a loadChildren
property on it, it uses the RouterConfigLoader
to try and figure out how to do that loading. We can see that the RouterConfigLoader
does something differently based on if typeof loadChildren
is a string
or not… but doesn’t loadChildren
have to be a string?
Let’s have a look at the LoadChildren
type:
Isn’t that interesting! Even in a pre-Ivy world, loadChildren
can be a string
or an async function
! So that should mean that our fancy import()
syntax will already work? Let’s try it out:
What? It does work! But how does this work?! Why have we been using the magic string syntax all along?!?!
The answer is there’s a catch… 🎣
Lazy-loaded routes in Angular 7.x.x with Ahead of Time compilation
If we were to take our above application and build it with the prod flag (ng build --prod
), everything appears to work! But when we try to navigate to our lazy-loaded route, we get a big red error:
This error makes sense! We used the --prod
flag to enable the “Ahead-of-time” (AOT) compiler, which means we opted out of the “Just-in-time” (JIT) runtime compiler. If we look at where the error comes from, we can see it’s caused by the call to compileModuleAsync()
in the RouterConfigLoader
:
We end up down that else
path because the instanceof
check fails! When we use the import()
operator with AOT, the object that we import from the lazy-loaded module is an NgModule
instead of a NgModuleFactory
. So how do we make sure that we are loading an NgModuleFactory
?
From NgModule
to NgModuleFactory
with the AOT Compiler:
The Angular compiler’s job is to statically analyse all of the code in our entire application, and to efficiently compile all of our templates and styles. It takes our NgModule
files, and turns them into NgModuleFactory
files, which contain the generated code that will create our views at runtime.
The compiler is able to start at a given file, and navigate through all of the import statements (e.g. import { Thing } from './path/to/thing';
) and build up a tree of all of the referenced modules. In order to split our application into chunks, we have to change our code to explicitly break this tree of references apart, while also making sure that the compiler knows about all the split parts of our application. The way we do this in an Angular application is with the loadChildren
property, specifically with the magic string format:
The Angular AOT compiler finds all the ROUTES
by using the InjectionToken
and then looks for any strings using the ./path/to/my.module#MyModule
format. Each time it finds one, the compiler will start from the given path, build up the tree of referenced files, and compile each NgModule
into an NgModuleFactory
. If we don’t use that format, we don’t end up with the NgModuleFactory
that the runtime needs. If we do use that format, then we end up with a generated file with an unknown path containing the NgModuleFactory
, which means we can’t reference it with import()
…
Altogether, this means that even though the types in Angular 7.x.x allow us to specify an async function for loadChildren
it will never work in a production build of our application 😭😭😭. But why does the import()
operator work in JIT mode?
The import()
operator is another way to declare that we want to lazily reference another part of our application. Modern tooling can detect it, mark the referenced path as another entry point, and lazily load the reference at runtime. Unfortunately, only the Angular CLI knows how to turn a NgModule
into an NgModuleFactory
, and it doesn’t know about import()
. We saw it working because JIT mode only needs an uncompiled NgModule
.
This is where we hit a bit of a dead end in Angular 7.x.x. For us to be able to use import()
, something needs to change with how Angular works. Luckily for us, that change is just around the corner!
You can learn more about how the Angular compiler works in this incredible article by Uri Shaked ❤️:
Lazy-loaded routes in Angular 8.x.x with Ivy:
One of the main design goals of the new Ivy renderer is to remove the differences between the JIT and AOT modes based on the principle of locality. Each file knows about everything that it needs to know about, without extra metadata files — this means no more NgModuleFactory
classes!
That means that we no longer need to run a separate AOT compile, no longer have to worry about generated files with unknown paths, and we can use our import()
operator!
You can learn more about the changes in Ivy in this post by Max Koretskyi aka Wizard ❤️:
Upgrading from magic strings to nice async functions:
Now we know we have a cool new tool that we will be able to use soon! But we also have a lot of existing code that uses the magic string syntax. Wouldn’t it be great if there was an automatic way to upgrade all of our old code?
We can write a custom TSLint rule and fixer to do all this for us! Let’s look at the whole rule first, and then break it down:
First things first, we have a TSQuery selector to choose the part of the code we want to modify:
PropertyAssignment
:not(:has(Identifier[name="children"]))
:has(Identifier[name="loadChildren"])
:has(StringLiteral[value=/.*#*/])
We use this selector in our rule to give us access to the right parts of our code:
We can parse out the magic string, and create the replacement code. The fixer can generate code that uses either a raw Promise
or async/await
:
Finally, we need to (somewhat clumsily) handle any indentation in the source code, and apply our fix:
And there we have it! One nice new sparkly TSLint fixer. If anyone wants to show me/help me how to turn this into an ESLint rule, then that’d be awesome 😇.
The End!
Phew! How’s that for a brain dump of soon to be obsolete knowledge! I hope you learned a thing or two, maybe feel a little bit less scared about reading Angular source code, and maybe feel a bit inspired to write your own automation for upgrading your apps. Please reach out to me with any questions, and I’d love your feedback!
❤️ 🦄
P.S. Big thanks to Thomas Burleson for “encouraging” me to write stuff down!