A brief history of JavaScript modules
Technically speaking, developers have been using modules in JavaScript for some time now. Solutions like CommonJS (for Node.js), Browserify, and Require.js have allowed developers to separate their code into reusable modules that make the code easier to maintain.
CommonJS was essentially the basis for Node’s package management system, npm. This allowed developers to create packages, or modules, that they can add to a remote registry, allowing others to use them. They, too, could use modules on npm’s registry for their own projects. But this was exclusive to Node.js (backend JavaScript), so browsers (i.e., client-side JavaScript) didn’t have a way to incorporate this modular package management.
That’s why JavaScript developers created tools like Browserify and Require.js to incorporate modules and bundle them for frontend projects. Later, the community introduced more powerful bundling solutions like Webpack and Parcel, allowing the process to be a regular part of modern JavaScript development.
If you want to delve deeper into the details of the history of JavaScript modules, see this article.
Native JavaScript modules
Solutions like CommonJS were necessary to make modules possible in JavaScript (particularly in Node.js). But JavaScript needed something better. In 2015, the ECMAScript standard officially added support for native modules (or ES Modules) that would no longer require third-party tooling for modular development in the browser.
Native modules are beneficial for several reasons, including:
- JavaScript modules allow developers to encapsulate and organize code into smaller, reusable parts. This modularity makes it easier to manage and maintain any codebase.
- You can reuse JS Modules, which avoids duplication in a project and helps keep code DRY (i.e., “Don’t Repeat Yourself”).
- Modules ensure that the browser loads only the necessary parts of the code at any given time, thus improving the performance of web apps, including PWAs.
- Modular code largely prevents naming conflicts since modularity isolates variables and other references, thus preventing conflict with other modules and not polluting the global scope.
- Modules make unit testing much easier, since you can run tests on isolated modules without affecting other pieces of code.
With that small history lesson and the benefits made clear, let’s now dig into the code so you can see how to use JS Modules in modern web apps.
Syntax for including JavaScript modules
You can insert any JavaScript module into a web page using almost the same syntax as any other script. But note one small difference:
body>
...
script src="index.js" type="module">script>
body>
Code language: HTML, XML (xml)
In the above code, I’ve added the element to the bottom of my HTML page.I’m referencing the script as I normally would using the
src
attribute.But,notice thetype
attribute.Instead of the customary value oftext/javascript
,I’ve used a value ofmodule
.This tells the browser to enable JavaScript module features in this script rather than treating it like a normal script.
How does a script differ when it’s included as a module?
- The browser automatically defers scripts with
type="module"
,so using thedefer
attribute would not effect this script. - Modules are subject to CORS rules,meaning you can access modules only on the same domain or those on a different domain that you’ve allowed permission to access via CORS.This is unlike regular scripts that you can load from anywhere.
- The browser interprets individual modules in strict mode.
In the example above,I can consider theindex.js
file as the entry point of my modular application.I could call this file whatever I want,but many developers customarily useindex.js
as their app entry point.
Now,I’m going to create some modules I’ll use insideindex.js
.Here’s a look at my folder structure so you can get an idea of how I’m organizing my modules:
index.html
index.js
modules/one.js
two.js
three.js
Your own app’s folder structure and file/folder names might differ,but the above should be easy enough to understand for demo purposes here.Theindex.html
file would be the one that includes theelement that inserts the initial module entry point(
index.js
).
Notice I’ve added a folder calledmodules
,which will store all of my modules in separate files.In this case,there are three modules.
Syntax for importing and exporting JavaScript modules
Now that I have a module entry point and I’ve set up my folder structure,I’ll show you how you can import any of the modules into the main script.
The one.js file is going to hold the following simple module script:
exportfunctionadd(a,b){returna+b}
Code language:JavaScript(javascript)
This is nothing but a simpleadd()
function.The key portion of the script that makes this a usable module is the use of theexport
statement.This syntax is only usable inside a module.With this in place,I can then import the module insidemain.js
like this:
import{add}from'./modules/one.js';console.log(add(5,8));
Code language:JavaScript(javascript)
The first line of code is where I import theadd()
function,wrapped in curly braces.I then define from where I want to import the module using the “from” keyword,followed by the module’s location in quotes.
Once I have the module imported,I can use theadd()
function as I would any function that I have access to.In this instance,I’m calling the function and passing in two numbers,which get added,and the function returns the value.
I don’t have to export a value at the same time that I define it.For example,earlier,I exported theadd()
function by putting theexport
keyword ahead of thefunction
keyword.I could alternatively do:
functionadd(a,b){returna+b}export{add};
Code language:JavaScript(javascript)
Here I’m exporting the reference toadd()
rather than the declaration ofadd()
.Note the use of curly braces around theadd
reference,which is required.The same principle would apply to anything I’m exporting – functions,variables,or classes.I can export the reference rather than the declaration.
That’s the simplest way to explain JavaScript modules syntax.There are quite a few other aspects to the code,so I’ll cover those next.
Syntax for multiple exports in a JavaScript module
In a JavaScript module file,I can export any number of values,including anything stored in a variable,a function,and so on.To demonstrate,I’ll add the following code to mytwo.js
module:
exportletname='Sally';exportletsalad=['lettuce','tomatoes','onions'];
Code language:JavaScript(javascript)
I’ve exported a simplename
variable followed by an array calledsalad
.Now I’ll import them,so myindex.js
file will look like this:
import{add}from'./modules/one.js';import{name,salad}from'./modules/two.js';console.log(add(5,8));console.log(name);console.log(salad[2]);
Code language:JavaScript(javascript)
Take note of the syntax required for importing multiple values.I’ve placed thename
andsalad
references inside the curly braces and separated them with a comma.This list of comma-separated imports could be any number of references,as long as I properly export them from thetwo.js
file.
Another way I can import multiple exports from a single file is by using the asterisk character during my import.Let’s say I have the following in my module:
exportfunctionadd(a,b){returna+b}exportletnum1=6,num2=13,num3=35;
Code language:JavaScript(javascript)
Notice I’m exporting a function along with three different variables.I can import all of these exports using the following syntax:
import*asmodfrom'./modules/one.js';console.log(mod.add(3,4));console.log(mod.add(mod.num1,mod.num2));console.log(mod.num3);
Code language:JavaScript(javascript)
Notice I’m exporting the entire module as an object calledmod
.From there,I can access all the functions,properties,or classes defined in themod
object using the familiar object dot notation.
In any of these examples,once I import the exports,I can then use them however I like in my application code.
Renaming JavaScript module exports
I can rename a module’s export before it’s exported,using theas
keyword,as in the following example:
functionadd(a,b){returna+b}export{addasaddFunc};
Code language:JavaScript(javascript)
With that code inside the module,importing would look like this:
import{addFunc}from'./modules/one.js';
Code language:JavaScript(javascript)
This allows me to use a different name for the export when it’s used inside the main application code compared to how it’s named in the module itself.I just have to make sure I reference the function asaddFunc()
when I use it.
I can also use the renaming syntax when doing the import.Assuming I’ve exported the function with its original name,add
,I can do the following:
import{addasaddFunc}from'./modules/one.js';
Code language:JavaScript(javascript)
This is the same basic idea as the previous example;I’m just doing the rename on import rather than on export.Of course,you’d have to be careful when renaming so as not to cause confusion in the code.You should generally have a good reason for renaming the export to make sure the code is still readable and maintainable.
Exporting defaults in JavaScript modules
The way I’ve exported and imported parts of my module code in earlier code examples is using what’s referred to as anamed export.The other kind of export is adefault export.Exporting a default value from a module is a pattern carried over from third-party module systems that I mentioned earlier.This became part of the ECMAScript standard to be interoperable with those older tools.
I can define a default export as follows:
exportdefaultfunctionadd(a,b){returna+b}
Code language:JavaScript(javascript)
This is the same function I exported earlier,except this time I’m using thedefault
keyword after theexport
keyword.I can also export a default value by reference:
functionadd(a,b){returna+b}exportdefaultadd;
Code language:JavaScript(javascript)
And I can use the renaming syntax:
functionadd(a,b){returna+b}export{addasdefault};
Code language:JavaScript(javascript)
In the case of the renaming syntax,I’m simply using the keyworddefault
in place of the export name.
To import any of the above default values,I can do the following:
importaddfrom'./modules/one.js';
Code language:JavaScript(javascript)
Notice I’m not using the curly braces around the import name.Non-default exports require curly braces,whereas a default export has no curly braces.Also,I can import any of the above exports using the following:
importaddFunctionfrom'./modules/one.js';
Code language:JavaScript(javascript)
In this case,I’ve renamed the import.Because this is a default export,I can alter the name as I import it;I’m not restricted to using the exported name.This is also clear from the fact that theas
syntax didn’t use a custom name when I exported the default.
The other thing that’s important to understand about default exports is that I can export only one value as the default.So,if I have multiple values I want to export in a module,I would use a similar syntax to the one earlier when I exported multiple items with an asterisk.
Here is my module:
exportfunctionadd(a,b){returna+b}exportletnum1=6,num2=13,num3=35;
Code language:JavaScript(javascript)
And here is myindex.js
file:
importmodfrom'./modules/one.js';console.log(mod.add(3,4));console.log(mod.add(mod.num1,mod.num2));console.log(mod.num3);
Code language:JavaScript(javascript)
The main difference here is that I’m not using the asterisk or theas
keyword;I’m simply importing the entire module and then working with it as I would any object.
More tips and facts on JavaScript modules
What I’ve discussed so far covers most of the basics to get you up and running with ES6 modules.Once you get past the basics,there are different subtleties you’ll want to keep in mind as you write your modules,which I’ll cover in this section.
Understanding encapsulation
Firstly,just because some code exists in a module doesn’t mean it’s going to be accessible in your primary script(the one that imports modules).For example,let’s saythree.js
has the following code:
functionsubtract(c,d){returnc-d}exportfunctionadd(a,b){returna+subtract(a,b)}
Code language:JavaScript(javascript)
I can then import and use theadd()
function,but notice what happens if I try to use thesubtract()
function:
import{add}from'./modules/three.js';import{subtract}from'./modules/three.js';console.log(add(23,16));console.log(subtract(30,34));
Code language:JavaScript(javascript)
Notice I can’t import thesubtract()
function,nor can I use it.Thesubtract()
function is part of my module’s logic and is necessary for the module to work.But it’s not an exported function,so I don’t have access to it outside of the module unless I explicitly export it.
Relative path names
As shown in multiple examples above,I can import JavaScript modules by referencing a JavaScript file.But notice what happens if I try to use the following syntax:
import{add}from'modules/three.js';
Code language:JavaScript(javascript)
I can use an absolute file path(i.e.,a full URL),and that would be no problem(as long as it passes CORS requirements).But if I’m using a relative file reference,the path needs to include a form that has a forward slash at the beginning of the path.This could be any one of the three formats shown in the error message above.
Strict mode in modules
Each encapsulated module works in the same way that code in strict mode works(that is,code in a block that has a'use strict'
statement at the top).So whatever rules apply to strict mode,the same rules apply to code in a module.For example,the value ofthis
in the top level of a module isundefined
,which isn’t the case when not in strict mode.
This means you can referencethis
at the top level inside the file doing the importing or inside any of the modules and the result will be the same:undefined
.
functionadd(a,b){returna+b}console.log(this);export{add};
Code language:JavaScript(javascript)
This is different from regular scripts in the browser,wherethis
at the top level is a reference to the Window object.
functionadd(a,b){returna+b}console.log(this);export{add};
Code language:JavaScript(javascript)
Impliedconst
in modules
Another point to keep in mind is that anything I import behaves as if I defined it usingconst
.If you’re familiar withconst
,this is a way to declare a variable that I can’t change(unless it’s an object,in which case I can change the properties).
To illustrate,suppose mytwo.js
file contains the following:
exportletname='Sally';
Code language:JavaScript(javascript)
I’m usinglet
to define the variable that’s exported.But notice what happens if I try to change it after import:
import{name}from'./modules/two.js';console.log(name);name="Jimmy";
Code language:JavaScript(javascript)
Even though I didn’t useconst
,the code import behaves as if I did.
Exporting and importing limitations
When doing imports or exports,I have to have them outside of other statements and functions.For example,the following code inside a module would throw an error:
if(name==='Sally'){export{name}}
Code language:JavaScript(javascript)
The same would result if trying to export inside of a function body.
Importing multiples using default and non-defaults
As mentioned,you can import only one default.But this doesn’t mean you’re limited to importing a single value from a module.For example,here’s myone.js
file:
functionadd(a,b){returna+b}exportletname="Sally";export{addasdefault};
Code language:JavaScript(javascript)
Notice I’m exporting theadd()
function as the default,but I’m also exporting a variable.
Now here’sindex.js
:
importadd,{name}from'./modules/one.js';console.log(name);console.log(add(10,8));
Code language:JavaScript(javascript)
I import the default with no curly braces and I export the variable with curly braces,and both are accessible as expected.When combining the default with non-default imports,I have to list the default first;otherwise,it will throw an error.
The exception would be if I were renaming the default,then I would put both inside the curly braces:
import{defaultasaddFunc,name}from'./modules/one.js';console.log(name);console.log(addFunc(10,8));
Code language:JavaScript(javascript)
Using the.mjs
file extension for JS Modules
One final thing I’ll mention here is that with JavaScript modules,you have the option to use a file extension of.mjs
instead of.js
for your module files.This can help with maintainability and how the modules work with some tools.
However,there are a few caveats,which you can read aboutin MDN’s reference.
Wrapping up this tutorial
That wraps up this tutorial on JavaScript modules.There’s more I could talk about,including how modules play an important role in yourbuild toolsprocess and how these tools willminifyyour modules.But this should be enough to give you a basic framework from which to get started.
Feel free to use the code examples from here to create your own testing ground to try out these features.This way,you can work with some live examples in your personal development environment and become more familiar with how this JavaScript standard works in practice.