The author selected Creative Commons to receive a donation as part of the Write for DOnations program.
External libraries can add complexity to a new JavaScript project. To be able to install and use external code libraries, you’ll need a build tool that can parse the code and bundle the libraries that you import into a final format. After the build is set up, you can add and integrate new code with ease, but there are still some problems.
For example, you may need a library in just one part of your application, a part of the application most users may never need, like an admin page. But by default most build systems will bundle all the code into one large file. The user will load the code regardless of whether they ever need to execute it. The build systems are flexible enough that they can be configured to load code as needed, but the process takes some work.
Build tools are an important part of the development experience, but a spec called import maps will allow you to both import external code into your project without a build tool and it will only load the code when it is needed at runtime. Import maps won’t completely replace build tools that perform many other valuable actions like building style sheets and handling images, but they will allow you to bootstrap new JavaScript applications quickly and easily using only the native browser functionality.
In this tutorial, you’ll use import maps and JavaScript modules to import code without build tools. You’ll create a basic application that will display a message and you’ll create an import map that will tell your browser where to locate external code. Next, you’ll integrate the imported code into your JavaScript and will use the third-party code without any need to download the code locally or run it through a build step. Finally, you’ll learn about current tools that implement many aspects of import maps and work on all modern browsers.
You will need a development environment running Node.js. This tutorial was tested on Node.js version 14.17.1 and npm version 6.14.23. To install this on macOS or Ubuntu 18.04, follow the steps in How To Install Node.js and Create a Local Development Environment on macOS or the Installing Using a PPA section of How To Install Node.js on Ubuntu 18.04.
You will also need a basic knowledge of JavaScript, which you can find in How To Code in JavaScript, along with a basic knowledge of HTML and CSS. A good resource for HTML and CSS is the Mozilla Developer Network.
In this step, you will create an HTML page, use JavaScript for dynamic activity, and start a local development server to track your changes.
To start, in a new directory, create a blank HTML document.
Open a file called index.html
in a text editor:
- nano index.html
Inside of the file, add a short, blank HTML page:
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Hello World</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
</body>
</html>
This document has a few standard <meta>
tags and an empty <body>
element.
Next add a <script>
tag. The src
attribute for the script tag will be a new JavaScript file you are about to create called hello.js
:
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Hello World</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script defer src="./hello.js"></script>
</head>
<body>
</body>
Notice that you are adding a defer
attribute to the <script>
tag. This will delay execution of the script tag until after the document is parsed. If you don’t defer, you may receive an error that says body
is not found when you try to add to the element.
Next, create a JavaScript file named hello.js
in the same directory asindex.html
:
- nano hello.js
Inside of the file, write some JavaScript to create a new text element with the text "Hello, World"
:
const el = document.createElement('h1');
const words = "Hello, World!"
const text = document.createTextNode(words);
el.appendChild(text);
document.body.appendChild(el);
Now that you have your script, you can open the index.html
file in a browser. In the same directory as your index.html
file, run npx serve
. This will run the serve
package locally without downloading into your node_modules
. The serve
package runs a simple webserver that will serve your code locally.
npx serve
The command will ask you if you want to install a package. Type y
to agree:
Need to install the following packages:
serve
Ok to proceed? (y) y
When you run the command you will see some output like this:
npx: installed 88 in 15.187s
┌────────────────────────────────────────┐
│ │
│ Serving! │
│ │
│ Local: http://localhost:5000 │
│ │
│ Copied local address to clipboard! │
│ │
└────────────────────────────────────────┘
When you open your web browser to http://localhost:5000
, you’ll see your code. You can either leave the server running in a separate tab or window or close it with CTRL+C
after previewing your code.
Now you are displaying a basic page in your browser, but you are not yet able to take advantage of third-party code and JavaScript packages. In the next step, you’ll dynamically import code and import the functions into your script without a build tool.
In this step, you’ll write code that uses external packages. You’ll modify your code to import JavaScript code using ES imports. Finally, you’ll load the code in your browser using the module
type so the browser will know to dynamically load the code.
To begin, open up hello.js
:
- nano hello.js
You are going to import some code from lodash
to dynamically change your text.
Inside of the file, change the text from Hello World
to all lower case: hello world
. Then at the top of the file, import the startCase
function from lodash
using the standard ES6 import
syntax:
import startCase from '@lodash/startCase';
const el = document.createElement('h1');
const words = "hello, world";
const text = document.createTextNode(words);
el.appendChild(text);
document.body.appendChild(el);
Finally, call startCase
with the words
variable as an argument inside of document.createTextNode
:
import startCase from '@lodash/startCase';
const el = document.createElement('h1');
const words = "hello, world";
const text = document.createTextNode(startCase(words));
el.appendChild(text);
document.body.appendChild(el);
If you closed your webserver, open a new terminal window or tab and run npx serve
to restart the server. Then navigate to http://localhost:5000
in a web browser to view the changes.
When you preview the code in a browser, open the developer console. When you do, you’ll see an error:
OutputUncaught SyntaxError: Cannot use import statement outside a module
Since the code is using import
statements, you’ll need to modify the <script>
tag inside of index.html
to handle JavaScript that is now split between multiple files. One file is the original code you wrote. The other file is the code imported from lodash. JavaScript code that imports other code are called modules
.
Close hello.js
and open index.html
:
- nano index.html
To run the script as a module, change the value of the type
attribute on the <script>
tag to module
:
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Hello World</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script type="module" src="./hello.js"></script>
</head>
<body>
</body>
</html>
Notice, you also removed the defer
attribute. JavaScript modules will not execute until the page is parsed.
Open your browser and you’ll still see an error`:
OutputUncaught TypeError: Failed to resolve module specifier "@lodash/startCase". Relative references must start with either "/", "./", or "../"
Now the problem is not the import
statement. The problem is the browser doesn’t know what the import
statement means. It’s expecting to find code on the webserver, so it looks for a file relative to the current file. To solve that problem, you’ll need a new tool called import maps
.
In this step, you learned how to modify your JavaScript code to load external libraries using ES imports. You also modified the HTML script
tag to handle JavaScript modules.
In the next step, you’ll tell the browser how to find code using import maps
.
In this step, you’ll learn how to create import maps to tell your browser where to find external code. You’ll also learn how to import module code and see how code is lazy-loaded in a browser.
An import map
is a JavaScript object where the key is the name of the import (@lodash/startCase
) and the value is the location of the code.
Inside of index.html
add a new script
tag with a type
of importmap
. Inside of the script
tag, create a JavaScript object with a key of imports
. The value will be another object:
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Hello World</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script type="importmap">
{
"imports": {
}
}
</script>
<script type="module" src="./hello.js"></script>
</head>
<body>
</body>
</html>
Be sure that you do not add any trailing commas in the object. The browser will not know how to handle them.
Now that you have your basic object, you can add the location of your code. The structure of an import map will be like this:
{
"imports": {
"nameOfImport": "locationOfCode",
"nameOfSecondImport": "secondLocation"
}
}
You already know the name of your import @lodash/startCase
, but now you need to find where a location to point the import map to.
A great resource is unpkg. Unpkg is a content delivery network (CDN) for any package in npm
. If you can npm install
a package, you should be able to load it via unpkg. Unpkg also includes a browsing option that can help you find the specific file you need.
To find the startCase
code, open https://unpkg.com/browse/lodash-es@4.17.21/ in a browser. Notice the word browse
in the URL. This gives you a graphical way to look through the directory, but you should not add the path to your import map since it serves up an HTML page and not the raw JavaScript file.
Also, note that you are browsing lodash-es
and not lodash
. This is the lodash
library exported as ES modules, which is what you will need in this case.
Browse the code and you’ll notice there is a file called startCase.js
. This code imports other functions and uses them to convert the first letter of each word to upper case:
import createCompounder from './_createCompounder.js';
import upperFirst from './upperFirst.js';
/**
* Converts `string` to
* [start case](https://en.wikipedia.org/wiki/Letter_case#Stylistic_or_specialised_usage).
*
* @static
* @memberOf _
* @since 3.1.0
* @category String
* @param {string} [string=''] The string to convert.
* @returns {string} Returns the start cased string.
* @example
*
* _.startCase('--foo-bar--');
* // => 'Foo Bar'
*
* _.startCase('fooBar');
* // => 'Foo Bar'
*
* _.startCase('__FOO_BAR__');
* // => 'FOO BAR'
*/
var startCase = createCompounder(function(result, word, index) {
return result + (index ? ' ' : '') + upperFirst(word);
});
export default startCase;
The browser will follow the import
statements and import every file necessary.
Now that you have a location for your import map, update the file import map with the new URL. Inside of index.html
, add @lodash/startCase
along with the URL. Once again, be sure to remove browse
:
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Hello World</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script type="importmap">
{
"imports": {
"@lodash/startCase": "https://unpkg.com/lodash-es@4.17.21/startCase.js"
}
}
</script>
<script type="module" src="./hello.js"></script>
</head>
<body>
</body>
</html>
Save the file. Refresh your browser and you will see Hello World.
NOTE: Import maps are not yet widely supported. Open the code in the latest version of Edge or Chrome or check out the latest supported browsers.
Your browser shows “Hello World”, but now something much more interesting is happening. Open the browser console in your browser, select Inspect Elements, and switch to the Network tab.
After opening the Network tab, refresh the page and you’ll see that the browser is loading the code dynamically. Whenever it finds a new import
statement, it imports the relevant code:
More importantly, notice that all of the code is loaded lazily. That means that the browser does not import any code until it is specifically needed. For example, even though startCase
is in the import map and the import map is defined before the script for hello.js
, it is not loaded until after hello.js
loads and imports the code.
If you were to add other entries in your import map, the browser would not load them at all since they are never imported into code. The import map is a map of locations, and doesn’t import any code itself.
One major problem is that import maps are not yet fully supported by all browsers. And even when they are supported, some users may not use a supported browser. Fortunately, there are different projects that use the import map syntax while adding full browser support.
In this step you created an import map. You also learned how to import module code and how the code will be lazy loaded in the browser. In the next step, you’ll import code using SystemJS.
In this step, you’ll use import maps across all browsers using SystemJS. You’ll export code as a SystemJS build and how to set the import map type to use SystemJS format. By the end of this step, you’ll be able to take advantage of import maps in any browser and will have a foundation for building more complex applications and microfrontends.
Import maps will remove many of the complicated build steps from an application, but they are not yet widely supported. Further, not all libraries are built as ES modules so there is some limitation to how you can use them in your code.
Fortunately, there is a project called SystemJS that can use create import maps for cross-browser support and use a variety of package builds.
The lodash library is convenient because it is compiled in an ES format, but that’s not the case for most libraries. Many libraries are exported in other formats. One of the most common is the Universal Module Definition or UMD. This format works in both browsers and node modules.
A major difference is that unlike the ES imports, a UMD build typically combines all of the code into a single file. The file will be a lot larger and you’ll end up with more code then you’ll probably execute.
To update your project to use SystemJS and a UMD build of lodash, first open hello.js
:
- nano hello.js
Change the import
statement to import the startCase
function directly from lodash
.
import { startCase } from 'lodash';
const el = document.createElement('h1');
const words = "hello, world";
const text = document.createTextNode(startCase(words));
el.appendChild(text);
document.body.appendChild(el);
Save and close the file.
Next, to build the file as a SystemJS build, you will need a simple build step. You can use another build tool such as webpack, but in this example you’ll use rollup
.
First, initialize the project to create a package.json
file. Add the -y
flag to accept all of the defaults:
npm init -y
After the command runs you’ll see a success output:
{
"name": "hello",
"version": "1.0.0",
"description": "",
"main": "index.js",
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"homepage": ""
}
Note: Your output may be slightly different depending on the version of npm
you are using.
Next, install rollup
as a devDepenceny
:
npm install --save-dev rollup
After a moment, you will see a success message:
+ rollup@2.56.2
added 1 package from 1 contributor and audited 2 packages in 6.85s
found 0 vulnerabilities
Next, create a simple build configuration. Open a new file called rollup.config.js
:
- nano rollup.config.js
Then add a configuration that will output the code in SystemJS format:
export default {
external: ["lodash"],
input: ["hello.js"],
output: [
{
dir: "public",
format: "system",
sourcemap: true
}
]
};
The external
key tells rollup not to include any of the lodash code in the final build. SystemJS will load that code dynamically when it is imported.
The input
is the location of the root file. The output
tells rollup where to put the final code and the format it should use which in this case is system
.
Save and close the file.
Now that you have a build step, you’ll need to add a task to run it. Open package.json
:
- nano package.json
In the scripts
object, add a script called build
that will run rollup -c
. Change the main
key to hello.js
:
{
"name": "hello",
"version": "1.0.0",
"description": "",
"main": "hello.js",
"devDependencies": {
"rollup": "^2.56.2"
},
"scripts": {
"build": "rollup -c"
},
"keywords": [],
"author": "",
"license": "ISC",
"homepage": ""
}
Save and close the file, and run the build
command:
npm run build
The command will run briefly, then you will see a success message:
> rollup -c
hello.js → public...
created public in 21ms
You will also see a new directory called public
that will contain the built file. If you open public/hello.js
you’ll see your project compiled in a system format.
- nano public/hello.js
The file will look like this. It’s similar to hello.js
with a surrounding System.register
method. In addtion, lodash
is in an array. This will tell SystemJS to load the external library during run time. One of the maintainers created a video that further explains the module format.
System.register(['lodash'], function () {
'use strict';
var startCase;
return {
setters: [function (module) {
startCase = module.startCase;
}],
execute: function () {
const el = document.createElement('h1');
const words = "hello, world";
const text = document.createTextNode(startCase(words));
el.appendChild(text);
document.body.appendChild(el);
}
};
});
//# sourceMappingURL=hello.js.map
Save and close the file.
The final step is to update your index.html
to handle the new file:
Open index.html
- nano index.html
First, you’ll need to import the SystemJS code. Use a regular <script>
tag with the src
attribute pointing to a CDN distribution.
Put the <script>
tag right below the import map:
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Hello World</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script type="importmap">
{
"imports": {
"@lodash/startCase": "https://unpkg.com/lodash-es@4.17.21/startCase.js
}
}
</script>
<script src="https://cdn.jsdelivr.net/npm/systemjs/dist/system.js"></script>
<script type="module" src="./hello.js"></script>
</head>
<body>
</body>
</html>
Next, you’ll need to update your import map. The format is similar to what you completed in Step 3, but there are three changes:
type
.lodash
.hello.js
script.First, update the type
. Since this is not the native browser version of an import map, but a systemjs
version, change the type to systemjs-importmap
:
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Hello World</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script type="systemjs-importmap">
{
"imports": {
"@lodash/startCase": "https://unpkg.com/lodash-es@4.17.21/startCase.js
}
}
</script>
<script src="https://cdn.jsdelivr.net/npm/systemjs/dist/system.js"></script>
<script type="module" src="./hello.js"></script>
</head>
<body>
</body>
</html>
Next, update the references. Change @lodash/startCase
to lodash
. You’ll be importing the full library. Then change the location to the UMD build at unpkg.
Then add a new entry for hello
and point that to the compiled version in the public
directory:
...
<meta name="viewport" content="width=device-width, initial-scale=1">
<script type="systemjs-importmap">
{
"imports": {
"hello": "./public/hello.js",
"lodash": "https://unpkg.com/lodash@4.17.21/lodash.js"
}
}
</script>
...
Now that you are importing systemJS
and have updated the import maps, all that’s left is to load the module.
Change the type
attribute on the script
tag for the module to systemjs-module
. Then change the src
to import:hello
. This will tell systemjs
to load the hello
script and execute:
...
<script type="systemjs-importmap">
{
"imports": {
"hello": "./public/hello.js",
"lodash": "https://unpkg.com/lodash@4.17.21/lodash.js"
}
}
</script>
<script src="https://cdn.jsdelivr.net/npm/systemjs/dist/system.js"></script>
<script type="systemjs-module" src="import:hello"></script>
</head>
...
Save and close the file.
When you do, the browser will refresh and you’ll see Hello World.
Unlike native import maps, this will work in any browser. Here’s the result in FireFox:
If you look at the Network tab. You’ll see that as with import maps, the code is lazy loaded as needed:
In this step, you used import maps across browsers with SystemJS. You changed your script to use the UMD build of lodash, created a rollup build to output the code in system
format, and changed the import map and module types to work with SystemJS
In this tutorial you used import maps to dynamically load JavaScript code. You rendered an application that dynamically loaded an external library without any build step. Then, you created a build process to generate your code in SystemJS format so that you can use import maps across all browsers.
Import maps give you opportunities to start breaking large projects into smaller independent pieces called microfrontends. You also do not need to limit yourself to statically defined maps as you learned in this tutorial; you can also create dynamic import maps that can load from other scripts. You also can use a single import map for multiple projects by using scopes to define different versions of a dependency for different scripts that import them.
There are new features in progress and you can follow them on the official spec.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
I don’t see how the imports in this post are dynamic or lazy. There are two ways in which ES modules can be imported:
import {someName} from 'some-module.js';
import('some-module.js').then((namespaceObj) => {});
If a module is loaded, all modules it statically imports are loaded before the module body is executed (eagerly, not lazily). The same happens with the static imports of those modules (etc.).
Import Maps change what is imported (by remapping module specifiers) but not when it is imported (they never trigger anything).
On the other hand,
import()
does allow you to lazily load modules (only if a button is clicked, etc.).