In this article we’ll have a look at working with dynamic module loading in Aurelia2. I’ll be using dumber.js as module bundler, but the concepts are applicable to other bundlers as well.

Disclaimer! This code was written when Aurelia 2 was in pre-alpha, some API’s will probably change!

๐Ÿ‘ฉโ€๐Ÿ”ฌSetting Up the Test

First off, I create a couple of new components. Under src, a modules folder containing sub-folders named bananas, oranges and peaches.

Like this ๐Ÿ‘‡

Modules prepared for dynamic laoding
Some fruity components

๐Ÿ The Base Bundle/Module Structure

You can find the logic for the bundling build logic in the gulpfile.js file in the root.

The code split is handled for us by the bundler. In our case, dumber.js is handling it for us. The logic for the bundling is located in the gulpfile.js file in the app root. The code looks like follows:

  codeSplit: isTest ? undefined : function(moduleId, packageName) {
    // Here for any local src, put into app-bundle
    if (!packageName) return 'app-bundle';
    // The codeSplit func does not need to return a valid bundle name.
    // For any undefined return, dumber put the module into entry bundle,
    // this means no module can skip bundling.
  },

This will put all the modules with a package name, basically the node modules, into app-bundle.js. Everything else, ie our code, will be placed the bundle named entry-bundle.js.

If we run a build using npm run build we can see the files for entry-bundle.js and app-bundle.js being written (by default a hash will be added to the end of the files) .

Initially, there’s only two bundles.

๐ŸจBreaking Up Code in Different Bundles

Let’s break up our test code by using the modules folder as the root structure for bundles.

  codeSplit: isTest
    ? undefined
    : function (moduleId, packageName) {
        if (!packageName) {
          const nameParts = moduleId.split("/");
          if (nameParts.length > 1) return nameParts[1] + "-bundle";
          return "app-bundle";
        }
      },

This code will create a bundle for any directory more than one level down, in our case that’s just the bananas, oranges and peaches. The name will be the second level folder name + “-bundle”. Building again will show our new bundles:

Our new bundles, ๐ŸŠ, ๐ŸŒ & ๐Ÿ‘!

๐Ÿ’จRunning the Application With the New Bundles

In the main app template I am importing all the components to enable navigating to them.

<import from="./welcome"></import>
<import from="./about.html"></import>
<import from="./missing"></import>
<import from="./modules/peaches/peach"></import>
<import from="./modules/bananas/banana"></import>

<nav>
  <a goto="welcome">Welcome</a>
  <a goto="about">About</a>
  <a goto="banana">๐ŸŒ</a>
  <a goto="peach">๐Ÿ‘</a>
</nav>

<au-viewport default="welcome" fallback="missing"></au-viewport>

This will thus load the components from our newly created modules, opening the network tab we can see the modules loading and the functionality work just as before splitting up the code in bundles.

Bundling, but not yet dynamic
Components loading from the bundles, click for gif action! ๐Ÿ‘†

๐ŸงฑDynamic Module Loading

Bundles? That’s all cool, but where’s that dynamic loading we were promised?

Well, we need to modify some code first, starting with removing the component imports from the main template.

<import from="./welcome"></import>
<import from="./about.html"></import>
<import from="./missing"></import>

<nav>
  <a goto="welcome">Welcome</a>
  <a goto="about">About</a>
  <a goto="banana">๐ŸŒ</a>
  <a goto="peach">๐Ÿ‘</a>
</nav>

<au-viewport default="welcome" fallback="missing"></au-viewport>

With the modified my-app.html template the new component bundles won’t load. This also means we can’t navigate to them since the components does not exist in context.

Module not yet loaded dynamically
No more bananas ๐Ÿ˜ข

Note in the Network tab that the component modules haven’t loaded.

What is the Best way to load modules dynamically?
Well, this will depend on your app and your use cases. I’m just showing how it can be done, this is not a primer on how to build efficient apps ๐Ÿ˜

๐Ÿ“‚Loading Modules From Code

We can modifying the code for my-app.ts, adding a function that loads and navigates to a module.

import { IRouter, IContainer } from "aurelia";

export class MyApp {
  constructor(
    @IRouter private readonly router: IRouter,
    @IContainer private readonly diContainer: IContainer
  ) {}

  public async navigateTo(compName: string): Promise<void> {
    if (compName === "banana") {
      const comp = await import("./modules/bananas/banana");
      this.diContainer.register(comp);
      this.router.goto("banana");
    }
  }
}

Unpacking the above code slightly for those unfamiliar with some Aurelia concepts:
First we require in references to the router and the global DI container by use of DI in the constructor.
We need the router if we want to use it to navigate. For the router to be able to find our dynamic module we need to register it in the DI container.

To dynamically load our modules, we use the import() statement. It’s async and returns a promise, so it’s possible to use await for those who prefer that over the promise syntax. Read more about the import statement over att MDN!

Then use our new method by modifying the link in my-app.html. Using our new method from the template like this:

  <a click.delegate="navigateTo('banana')">๐ŸŒ</a>

OBSERVE!
To enable the code to compile, we have to modify the tsconfig.json file and set the --module flag.
Just add "module": "ESNext" to the compilerOptions.

After clicking the ๐ŸŒ link we can now see the bananas-bundle.js and oranges-bundle.js files being loaded in the Network tab. And the router successfully routed to the ๐ŸŒ page!

Dynamic module loading after pressing link
๐ŸŒ Dynamic Banana! ๐ŸŒ

Why did the oranges-bundle.js get loaded as well, we only loaded the bananas component from our code?! The bundler is clever enough to load it as the banana component is using it as a dependency. The orange-component is imported in the banana.html template. ๐Ÿ‘‡

<import from="./../oranges/orange-component"></import>

<div class="page center-center">
  <div class="big-banana">๐ŸŒ</div>
  <orange-component></orange-component>
</div>

๐Ÿ”ดRouting Drawback

If the user bookmarks the banana page and comes back to it, the page will respond with: “Ouch! Couldn’t find ‘banana’!“. What gives?

Since we removed the import statements in the template file, there is no banana component loaded when the app starts upp. And if the component isn’t loaded into the DI container, the router can’t navigate to it.

โœ…Fixing the Routing Drawback Using the Router

If this still is the way you want to solve dynamic module loading, the way forward is to take care of this issue in another component. As we have set the router to use the missing component as a fallback, we can fix it there.
Modify missing.ts and moving our dynamic module load code there gives:

import { IRouter, IContainer } from "aurelia";

export class Missing {
  public static parameters = ["id"];
  public missingComponent: string;

  constructor(
    @IRouter private readonly router: IRouter,
    @IContainer private readonly diContainer: IContainer
  ) {}

  public async enter(parameters): Promise<void> {
    if (parameters.id === "banana") {
      const comp = await import("./modules/bananas/banana");
      this.diContainer.register(comp);
      this.router.goto("banana");
    }

    this.missingComponent = parameters.id;
  }
}

A new Aurelia 2 concept in the code above is the enter() method. That is one of the router life cycle methods, and is activated by the router when navigating to the page. Like the router hook activate() in Aurelia 1.

If a user now bookmarks the banana page and goes back to the link, the missing page will dynamically load the banana module and re-route to that page. The user will once again see the lovely ๐ŸŒ!

๐Ÿšตโ€โ™€๏ธA Better Way

Perhaps a less convoluted and architecturally more solid way to solve the linking issues is to make a top-level page for all modules, and let them always be accessible from the entry-bundle? Then having logic in those pages load modules/ bundles as needed.

Moving ๐Ÿ‘ Topside

The best alternative would be to move the peaches component to the root level along side about, welcome etc. Then early in the life cycle load the component in the peaches-bundle and render them. However, as Aurelia 2 is in pre-alpha, that’s not implemented yet. So we’ll create a new page, named peach-top, and re-route to the peach component inside the peach-bundle.

To make this work we need to fix a few things

  • Create peach-top.html and peach-top.ts in the src root, along side my-app.html
  • Create a componentRegistry.ts file in the peaches folder and export everything in the peaches module from there
  • Add dynamic module load of the components in the peach-bundle to the enter method in peach-top.ts and do a re-route to the peach page
  • Add an entry to missing.ts to handle any eventual deep links to the peach page
๐Ÿ‘ฉโ€๐Ÿ’ปAll the Codez

Create peach-top.html, and let it be empty. There’s a small chance any content will flash before the user while loading the peach module, but it will be very brief. If you have very large modules to load dynamically, then putting a spinner in this page would be good for the user experience.

Create peach-top.ts ๐Ÿ‘‡

import { IContainer, IRouter } from "aurelia";

export class PeachTop {
  constructor(
    @IContainer private readonly diContainer: IContainer,
    @IRouter private readonly router: IRouter
  ) {
    console.log("๐Ÿ‘-๐Ÿ”");
  }

  public async enter(): Promise<void> {
    const comp = await import("./modules/peaches/componentRegistry");
    this.diContainer.register(comp);
    this.router.goto("peach");
  }
}

Create componentRegistry.ts in the peach modules folder ๐Ÿ‘‡

export * from "./peach-component";
export * from "./peach";

Modify missing.ts ๐Ÿ‘‡

import { IRouter, IContainer } from "aurelia";

export class Missing {
  public static parameters = ["id"];
  public missingComponent: string;

  constructor(
    @IRouter private readonly router: IRouter,
    @IContainer private readonly diContainer: IContainer
  ) {}

  public async enter(parameters): Promise<void> {
    if (parameters.id === "banana") {
      const comp = await import("./modules/bananas/banana");
      this.diContainer.register(comp);
      this.router.goto("banana");
    }

    if (parameters.id === "peach" || parameters.id === "%F0%9F%8D%91") {
      this.router.goto("peach-top");
    }

    this.missingComponent = parameters.id;
  }
}

Note: Adding “%F0%9F%8D%91” as a valid parameter for routing to peach top is just shenanigans to enable the route “http://localhost:9000/#/๐Ÿ‘” ๐Ÿ˜

Modify my-app.html to load peach-top instead of peach ๐Ÿ‘‡

<import from="./welcome"></import>
<import from="./about"></import>
<import from="./missing"></import>

<import from="./peach-top"></import>

<nav>
  <a goto="welcome">Welcome</a>
  <a goto="about">About</a>
  <a click.trigger="navigateTo('banana')">๐ŸŒ</a>
  <a goto="peach-top">๐Ÿ‘</a>
</nav>

<au-viewport default="welcome" fallback="missing"></au-viewport>

๐Ÿ”ฎConclusion

Hope this helps with bundle splitting of your large Aurelia applications! I will re-visit this article again as Aurelia 2 get’s ready for prime time. ๐Ÿ’–

๐Ÿ”—Resources

Find the code on my github
Read more about dumber.js code splitting
Aurelia 2ย documentation

Happy Coding! ๐Ÿ˜

Dynamic Module Imports in Aurelia 2
Tagged on:     

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.