JavaScript - tutorial - 17 - modules

Revision:


Content

definition and usage core module features examples illustrating the use of modules


definition and usage

top

Modules are small units of independent, reusable code that can be used as the building blocks in creating a Javascript application. Modules let the developer define private and public members separately, making it one of the more desired design patterns in JavaScript.

A module is just a file. One script is one module. Modules can load each other and use special directives "export" and "import" to interchange functionality, call functions of one module from another one:

- the "export keyword" labels variables and functions that should be accessible from outside the current module;
- the "import keyword" enables the import of functionality from other modules;
- the "import directive" loads the module by the path relative to the current file, and assigns exported function to the corresponding variable.

As modules support special keywords and features, we must tell the browser that a script should be treated as a module, by using the attribute <script type="module">. The browser automatically fetches and evaluates the imported module (and its imports if needed), and then runs the script.


core module features

top

always "use strict"

modules always work in strict mode. E.g. assigning to an undeclared variable will give an error.

module-level scope

each module has its own top-level scope, what means that top-level variables and functions from a module are not seen in other scripts. Modules should export what they want to be accessible from outside and import what they need. A module code is evaluated only the first time when imported.

If the same module is imported into multiple other modules, its code is executed only once, upon the first import. Then its exports are given to all further importers. The one-time evaluation has important consequences:

If executing a module code brings side-effects - like showing a message - then importing it multiple times will trigger it only once – the first time.
There's a rule: top-level module code should be used for initialization, creation of module-specific internal data structures.
If we need to make something callable multiple times, we should export it as a function.

If a module is imported from multiple files, the module is only evaluated the first time and then passed to all further importers.

the object "import.meta"

This contains the information about the current module. Its content depends on the environment. In the browser, it contains the URL of the script, or a current webpage URL if inside HTML.

this

In a module, β€œthis” is undefined.


examples illustrating the use of modules

top

Example: create a rectangle.

code:
                    <div class="spec">
                        <p id="mod-01"></p>
                        <p id="mod-02"></p>
                    </div>
                    <script>
                    // This is a Rectangle Module. 
                        function Rectangle() { 
                            var length, width; 
                            function create(l, w) { 
                                length = l; 
                                width = w; 
                            } 
                            function getArea() { 
                                return (length * width); 
                            } 
                            function getPerimeter(){ 
                                return (2 * (length + width)); 
                            } 
                            // This is the object to consist public members.  The rest are private members. 
                            var publicAPI = { 
                                create : create, 
                                getArea : getArea, 
                                getPerimeter : getPerimeter 
                            }; 
                            // To be returned upon creation of a new instance. 
                            return publicAPI; 
                        } 
                        // create a Rectangle module instance 
                        var myRect = Rectangle(); 
                        myRect.create(5, 4); 
                        document.getElementById("mod-01").innerHTML = "Area: " + myRect.getArea(); 
                        document.getElementById("mod-02").innerHTML = "Perimeter: " + myRect.getPerimeter();               
                </script>
                

Example: explaning the module approach - main.js

The module on the right (i.e. main.js) has three functions defined in it: 1/ getPower: this function gets the power of a number; 2/ capitalize: this function capitalizes the first letter in a word; 3/ roundToDecimalPlace: this function rounds a given number to a specified number of decimal places.

At the end of the file, two of the three functions were exported. In other words, they became public functions which could be used by any other script. To export two functions out of the three, you use the export keyword, followed by an object containing the functions you want to make accessible. Once you do this, the functions can be accessed by any program within that codebase which require them.

code:
                    <script>
                        function getPower(decimalPlaces) {
                            return 10 ** decimalPlaces;
                        }
                        function capitalize(word) {
                            return word[0].toUpperCase() + word.slice(1);
                        }
                        function roundToDecimalPlace(number, decimalPlaces = 2) {
                            const round = getPower(decimalPlaces);
                            return Math.round(number * round) / round;
                        }
                        export { capitalize, roundToDecimalPlace };
                    </script>
                

Example: explaining the module approach - displayTotal.js

The displayTotal.js module - on the right side - does not have capitalize() and roundToDecimalPlace() but wants to use the capitalize and round-to-decimal-place functionality. By using the import keyword followed by the name of the functions - i.e. capitalize and roundToDecimalPlace - we want to import from the module.

code:
                    <script>
                        import { capitalize, roundToDecimalPlace } from './main';
                        function displayTotal(name, total) {
                            return `${capitalize(name)}, your total cost is: ${roundToDecimalPlace(total)}`;
                        }
                        displayTotal('kingsley', 20.4444444);
                        // "Kingsley, your total cost is: 20.44"
                        export { displayTotal };
                    </script>
                

Example: explaining the module approach - warn.js

If you want to import every public function from another module, use the asterisk * keyword, which is used to import the public functions into a new object - i.e. mainfunctions.

If you're importing everything from a module, you should use the asterisk instead of explicitly spelling out all the functions one-by-one. We then access and call the functions we want to use in our program.

code:
                    <script>
                        import * as mainfunctions from './main';
                        function warn(name) {
                        return `I am warning you, ${mainfunctions.capitalize(name)}!`;
                        }
                        warn('kingsley');
                        // I am warning you, Kingsley!
                        export { warn };
                    </script>
                

Example: explaining the module approach - anothermain.js

A function, variable, or class can also be exported by registering the export keyword just in front of its definition.

In the module on the right - anothermain.js - the export keyword is attached to both functions when they are being defined.

code:
                    <script>
                        function getPower(decimalPlaces) {
                            return 10 ** decimalPlaces;
                        }
                        export function capitalize(word) {
                            return word[0].toUpperCase() + word.slice(1);
                        }
                        export function roundToDecimalPlace(number, decimalPlaces = 2) {
                            const round = getPower(decimalPlaces);
                            return Math.round(number * round) / round;
                        }
                    </script>
                

JavaScript.info - examples

examples
what is a module:
                        // πŸ“ sayHi.js
                        export function sayHi(user) {
                            alert(`Hello, ${user}!`);
                        }
                        // πŸ“ main.js
                        import {sayHi} from './sayHi.js';
                        alert(sayHi); // function...
                        sayHi('John'); // Hello, John!

                        <!doctype html>
                        <script type="module">
                        import {sayHi} from './say.js';
                        document.body.innerHTML = sayHi('John');
                        </script>
                    
module scripts are deferred:
                        <script type="module">
                            alert(typeof button); // object: the script can 'see' the button below
                            // as modules are deferred, the script runs after the whole page is loaded
                        </script>
                        Compare to regular script below:
                        <script>
                            alert(typeof button); // button is undefined, the script can't see elements below
                            // regular scripts run immediately, before the rest of the page is processed
                        </script>
                        <button id="button">Button</button>