In this lesson, you will dive into JavaScript Modules also known as ES (EcmaScript) Modules.
ES Modules: A New Way to Organize Your Code
With JavaScript modules, instead of having to manage a bunch of script tags in your HTML file, you can instead have a single "entry point" script tag. Then, you can import the additional scripts you need from within your JavaScript files. This allows you to create more complex JavaScript structures, and not have to worry about the order of your script tags in your HTML file.
Overall it offers a modern and less error-prone way to organize your code which brings it more in line with other programming languages with a module system.
Another huge advantage it brings is that editors and IDEs will be able to understand your code better and provide you with better code completion and error checking.
How to Import a JavaScript Module in Your HTML
Say you had a project with two JavaScript files, index.js and lib.js and an HTML file. In the HTML file, instead of having to put the script tags after the content of the page, like so:
<head>
<!-- ... -->
</head>
<body>
<!-- ... -->
</body>
<script src="lib.js"></script>
<script src="index.js"></script>
You can instead put a single script tag in the head element:
<head>
<script type="module" src="index.js"></script>
</head>
<body>
<!-- ... -->
</body>
If all your JavaScript files are modules, this may be the only script tag you need. Being able to put the script tag in the head element is also a nice bonus because it's more familiar to put "things that are required" at the start of a document.
When you use the "module" type, the script is deferred automatically. That means that execution of the script only happens after all the HTML has loaded.
You can also defer scripts that aren't modules with the defer attribute in the script tag:
<script defer src="index.js"></script>
How to Use Modules in JavaScript Files
Now that you have a module entry point that is loaded into your HTML, you can start using the extra features that modules provide in your JavaScript.
Typically, the entry point for a module is a minimal file that imports other modules and then calls functions from those modules. But currently, you don't have anything to import, so, in the lib.js file, you can create some things that you might want to import:
// lib.js
export const IMPORTANT_CONSTANT = 42;
export function drawTriangle() {
console.log('Drawing triangle');
}
export function drawCircle() {
console.log('Drawing circle');
}
The export statement is used for things that you want to be able to import from other modules.
With the exports in place, in the index.js file, you can import things from lib.js:
// index.js
import { drawTriangle, drawCircle } from './lib.js';
drawTriangle(); // Drawing triangle
drawCircle(); // Drawing circle
In this case, you're importing the drawTriangle() and drawCircle() functions directly into the current scope. Note the use of destructuring to grab the two imports by their specific name.
You can even rename the imports:
// index.js
import { drawTriangle as triangle, drawCircle as circle } from './lib.js';
triangle(); // Drawing triangle
circle(); // Drawing circle
In this case, you're importing the drawTriangle() and drawCircle() functions and renaming them to triangle and circle respectively. This is useful if you have a function name that is too long or conflicts with another name.
You can also import everything from a module into a single object:
// index.js
import * as lib from './lib.js';
lib.drawTriangle(); // Drawing triangle
lib.drawCircle(); // Drawing circle
console.log(lib.IMPORTANT_CONSTANT); // 42
In this example you've importing everything from lib.js into a lib object. Then you can use the functions and constants from lib.js by prefixing them with lib..
There are other variations of importing. Explore the linked documentation to explore them further.
Default Exports
In addition to named exports, you can also have a default export. This is useful if you want to export a single thing from a module. For example, you could have a module that exports all the functions your need in a single object:
// lib.js
function drawTriangle() {
console.log('Drawing triangle');
}
function drawCircle() {
console.log('Drawing circle');
}
export default { drawTriangle, drawCircle };
As you can see, you can just have one export default statement with everything that you want to export. If you are using a default export, you can only have one per module. But you can have a default export and named exports in the same module.
Then, in the index.js file, you can import the default export like so:
// index.js
import drawLib from './lib.js';
drawLib.drawTriangle(); // Drawing triangle
drawLib.drawCircle(); // Drawing circle
In this case, you're importing the default export from lib.js and naming it as drawLib. Then you can use the functions from lib.js by prefixing them with drawLib.. This example is a shorthand for the following:
import { default as lib } from './lib.js';
// ...
You'll find that a lot of libraries and frameworks use default exports, often packing up a bunch of functions into a single object.
Working with Modules and Libraries
A lot of libraries and frameworks give you the option of using a module version of their code. In the next main section of this tutorial, you'll see how to work around libraries or scripts that don't support modules. But first, you'll look at how to use modules with libraries that do support them.
For example, if you look at Lodash, you'll see that they have a page of custom builds, one of which is the lodash-es version. The es stands for ES modules.
Often, you'll have to do some digging to find the module version of a library. There isn't a direct link to the es version of Lodash from the main page, for example. But if you do a Google search for lodash-es cdn, you may find a link to the jsDelivr CDN, which serves the es version of Lodash.
Some popular and reliable CDN's for JavaScript libraries are:
Sometimes it's quicker to go and search for the library on one of these CDN's directly.
Unpkg can be very handy because it serves directly from the NPM registry. NPM is the principle package manager for JavaScript libraries and often the package will have a link to it's GitHub page. So, if you know the name of the library you want to use, you can go to the NPM website and search for it. Then you can use this format to get the file in the unpkg CDN:
unpkg.com/:package@:version/:file
You'll see an example of it coming up soon.
Once you've found the module version of a library, you can import it into your JavaScript module using the URL of the library. For example, you could import Lodash like so:
import _ from 'https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js';
console.log(_.random(1, 999)); // 194
Here you're importing the default export from the Lodash module and naming it _. Naming lodash _ is a convention that is often used for lodash, hence the name of the library.
As you can see, Lodash does have a default export, or else the above example would throw an error like this one:
Uncaught SyntaxError: The requested module '_' does not provide an export named 'default'
You'll also get similar errors if you try to import from a library that isn't a module at all.
Lodash also has named exports, so you can import specific functions from Lodash like so:
import { random } from 'lodash-es';
console.log(random(1, 999)); // 934
In this case, you're importing the random() function from Lodash and using it directly.
Often it takes a small amount of experimentation to figure out how to import a library. Sometimes they don't support named exports, other times they don't have a default export. Trial and error is typically the fastest way to figure it out.
Using Import Maps
You can also use an import map to import modules. An import map is a JSON file that maps module names to URLs. For example, instead of importing Lodash directly, you could create an import map like so:
<script type="importmap">
{
"imports": {
"lodash": "https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js"
}
}
</script>
Then, in your JavaScript file, you can import Lodash like so:
import _ from 'lodash';
console.log(_.random(1, 999)); // 194
In this case, you're importing the default export from the lodash module. The import map is a nice way to organize your imports, especially if you're using the same module in multiple files.
Some libraries will require you import them with an import map, though. For example, Three.js, in their installation documentation recommend that you use an import map to import their modules:
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/[email protected]/build/three.module.js",
"three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
}
}
</script>
Then you can use these imports in your JavaScript files:
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
As you can see, you've created two paths in the import map which you can then use to reference the modules. See the GitHub repository for Three.js to explore the folder structure that you're referencing.
While you can import the main Three.js module like you did for Lodash, the addons require the import map. This is because the addons expect to be able to import other modules from the the three path which is defined in the import map. If you're importing three from "<cdn_url>" addons will still be searching for the three path, which won't exist.
Working with Modules and Regular Scripts
If you're going to be using modules, you should go all in and use modules for all your JavaScript files, it makes things much clearer and easier to manage. It also gives you all the added benefits of editor support and error checking.
For example, say you had a regular script that you wanted to use in your module:
// non-es-lib.js
function drawTriangle() {
console.log('Drawing triangle');
}
No export keywords here, so this is a regular script. You can't import this directly into your module, but you can include it in a regular script tag in the HTML, like so:
<head>
<script defer src="non-es-lib.js"></script>
<script type="module" src="index.js"></script>
</head>
Here, you've imported the regular script before the module script, so the regular script will be available to the module script. You've used the defer keyword to make sure that the regular script is executed after the HTML has loaded, this prevents visible delays in loading of the page.
Then, in your module, you can just use the functions from the regular script as if you had imported them:
// index.js
if (!window.drawTriangle) {
throw new Error('drawTriangle is not defined');
}
drawTriangle(); // Drawing triangle
As you can see, since the regular script is loaded before the module script, the functions from the regular script are available to the module script. It's as if all the scripts were in the same file. Your editor may complain that this function isn't defined, but it will still work when you load the HTML.
You've also added a check to ensure that the function is available before you use it. This is a good idea because if you forget to include the regular script, you'll get a descriptive error when you try to use the function.
This is a workaround, and isn't ideal, but sometimes you have to work with what you have. If you're using a library that doesn't support modules, you can use these techniques to use it in your module.
Using the Console When Debugging ES Modules
When you run module-based code, you won't be able to access the variables and functions within the modules directly from the console, as you might have done for regular JavaScript. This is because modules are evaluated in their own scope, and the console is in the global scope. This is a good thing in terms of keeping your modules isolated with their own "global" scope, but it does make debugging a little less direct.
You can still set breakpoints in your code by inserting the debugger keyword where you want to start the debugger. Once the debugger is active, on the console, you'll have temporary access to the variables and functions that are active in that current scope.
Normal console.log() calls will work if it's in your script, it will show up on the console as usual. Though if your debugging technique is to run the script and then mess around in the console by referencing things defined in the global scope, you'll have to change your approach.
Node.js Workflows and Modules
In a later course you'll get to use Node.js. Node.js is a JavaScript runtime that allows you to run JavaScript outside of the browser. It's often used for server-side JavaScript, but it can also be used for other things, such as building front-end JavaScript applications.
You will probably have seen installation instructions for JavaScript libraries that use Node.js that look like this:
npm install <package_name>
This indicates that this library can be installed with the the Node Package Manager (NPM).
When using Node.js, you can install modules into a local project folder using NPM. This allows you to use modules in your project without having to worry about downloading them manually or fetching them from a CDN.
You can then take advantage of module imports in your JavaScript files, and use the modules that you've installed locally. This is a very common workflow for JavaScript projects.
With this kind of setup, a whole host of tools become available, one of the most important being bundlers. Bundlers allow you to take your JavaScript modules and bundle them into a single file that can be loaded into the browser. Many bundlers use tree shaking to remove unused code from your modules, which can significantly reduce the size of your JavaScript files, leading to snappier front-end experiences.
Essentially, you use Node.js, and NPM, and any library, framework or utility you can imagine, to build a JavaScript application that can be loaded into the browser. This opens up a whole host of tools and workflows that you'll explore in later courses.
Summary: ES Modules - How to Import a JavaScript File
In this lesson you've:
- Recognized the benefits of ES Modules, such as easier code organization, better tooling support, and more predictable script loading order due to automatic deferral.
- Grasped how to import modules in your HTML with a single script tag using
type="module". - Understood
exportstatements to share functions and values across different modules. - Learned the distinction between default and named exports.
- Explored how to import modules from CDNs and using import maps.
- Addressed how to integrate non-module scripts with modules.