If you’re building web applications, you’ll likely have to write some JavaScript, so why not write the best JavaScript you can? The JavaScript ecosystem has evolved with an emphasis on performance and user experience, and if you’ve avoided writing JavaScript for a while now, you’ll be pleasantly surprised.

Import Maps is the latest advancement, a technique to make managing and consuming EcmaScript modules (ESM) much more straightforward for client-side developers. Ultimately, it brings the same style of development Node developers have enjoyed on the server side of the ecosystem to the browser.

In this post, we’ll compare traditional script referencing to ESM, how to write a simple ESM file, how to use it in your Razor-powered ASP.NET Core applications, and some good-to-know facts.

What is ESM and Why Should I Care?

JavaScript implementations have transitioned from unstructured code living in a global context to codebases depending on easy-to-follow structures. Let’s look at a simple module and how you can consume it.

// file name: simple.js
export default function () {
    console.log('hello world!');
}

Now, let’s use our module that exports a function.

import Simple from "./simple.js";
Simple();

You can see that authoring and consuming a module has intention behind it. You can tell where a module is coming from and what parts of the module you use. There’s no surprise global variables or opportunities for unresolvable conflicts. So why is this better?

If you’ve spent any time doing web development, you’re likely familiar with JQuery and how it’s referenced and used in a web application. The breakdown is not to pick on JQuery but to demonstrate some issues with traditionally-written libraries.

The foremost trait of a traditional script is that you reference it on a web page like so.

<script src="/libs/jquery/3.6.4/jquery.min.js"></script>

While perfectly fine, there are a few drawbacks to referencing scripts this way.

  1. Scripts are blocking and will stop the page from processing until all content is loaded.
  2. Script variables and functions are global and may conflict with other scripts.
  3. Selectively loading dependencies requires server-side templating to toggle elements or client-side code that dynamically injects script tags.

In our ESM example, you can reference scripts using the type of module in your Razor pages.

<script src="~/js/site.js" type="module" asp-append-version="true"></script>

The presence of type=module tells the browser to load the file with the knowledge that it is using ESM and to respect import statements. Only dependencies that are part of the import graph will be loaded, and additionally, all modules are deferred by default, meaning they won’t block the page rendering.

So how do import maps factor into all of this?

Import Maps and Why They’re Important

Import maps allow you to define aliases for internal and external dependencies used in your JavaScript. For example, look at the import map from my sample ASP.NET Core Razor Pages project.

<script type="importmap">
{
    "imports" : {
        "vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js",
        "hello-world": "@Url.Content("~/js/hello-world.js")",
        "simple": "@Url.Content("~/js/simple.js")"
    }
}
</script>

Once you’ve defined an import map, you can use the aliases anywhere in your client-side environment. For example, here is the implementation for hello-world.js.

import { createApp } from "vue";

export default function helloWorld(target, message) {
    createApp({
        data() {
            return {
                message: message
            }
        }
    }).mount(target)
}

You can see that I’m importing the vue dependency, which points to an ESM version of the Vue library. It’s important that any library imported also be written in the ESM style. All without immediately knowing what vue is used during my usage. The flexibility allows for more centralized dependencies updates without affecting large swaths of my codebase.

We can also import a module within our HTML pages using a’ script’ tag element.

<div id="app"></div>

<script type="module">
import helloWorld from 'hello-world';
helloWorld('#app', 'Hello From ASP.NET Core');
</script>

Awesome right?!

Now our ASP.NET Core pages can selectively use dependencies from our JavaScript modules, with the added benefit of only loading the libraries we need. What a fantastic feature.

You might be thinking, how can ASP.NET Core do more in this situation? How about importing JSON files into your clientside apps? That would be cool, right? Well, you can!

Let’s first define a JSON endpoint using Minimal APIs.

app.MapGet("/config", 
    () => new { name = "Khalid" });

Next, we’ll modify our import map to point to this new endpoint.

<script type="importmap">
{
    "imports" : {
        "vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js",
        "hello-world": "@Url.Content("~/js/hello-world.js")",
        "simple": "@Url.Content("~/js/simple.js")",
        "config": "@Url.Content("~/config")"
    }
}
</script>

Finally, we can import and use our JSON response in our client-side script. You must assert that the module import is of type json for it to work correctly.

<script type="module">
import config from 'config' assert { type: 'json' };
import helloWorld from 'hello-world';
helloWorld('#app', `Hello ${config.name} From ASP.NET Core`);
</script>

Note that there might still be compatibility issues with assert statements across browser vendors. Please consult the Mozilla documentation to see if your browser supports particular features with modules.

General Thoughts on Import Maps

I have some thoughts I’d love to share with you about some advantages of this approach.

  1. Modules and import statements are very natural for C# developers, so the move to this style of development should be easier for most folks.
  2. The deferment of these scripts by default means you get an added performance boost to your user experience. Who doesn’t want that?!
  3. The combination of JavaScript code using ASP.NET Core server-rendered content is powerful.
  4. Import maps make it easy to manage dependencies, regardless if you’re using a build tool or want to use prebuilt dependencies. It’s great!
  5. All your dependencies must be ESM compatible, so trying to import commonJS files will not work.
  6. Don’t forget to add type=module to your script tags. It’s easy to forget and can leave you scratching your head.

Conclusion

All major browsers now support import maps, and ESM is the future of writing JavaScript. It’s also a great way to update your existing clientside development workflow without taking on large build toolchains. You’ll need to seek out ESM-compatible libraries, but many of the most popular ones have already started the shift. In combination with ASP.NET Core, you can use the dynamic server-side generation tools to interact and enhance your client-side experience. It’s a win-win for everyone.

I hope you enjoyed this post, and as always, thanks for reading and sharing my content.