Frontier Software

Modules

Modules

Many things confused me about JavaScript’s modules, and there appear to be many quirks, especially regarding scoping.

Declaration

The modern way is to load a main.js file in the head of an HTML document with the attribute type="module":

<head>
...
  <script type="module" src="/js/main.js"></script>
</head>

This replaces the old way of loading the file before the closing </body> to ensure the DOM objects used by the JavaScript code were known to it. Something that tripped me up is the type="module" attribute means main.js is a module client, aka importer, not an exporter, ie what’s generally thought of as a module.

Namespace import

Of the many styles of importing modules available, the one I favour is termed module object.

The example main.js looks like this.

import * as Canvas from './modules/canvas.js';

import * as Square from './modules/square.js';
import * as Circle from './modules/circle.js';
import * as Triangle from './modules/triangle.js';

// create the canvas and reporting list
let myCanvas = Canvas.create('myCanvas', document.body, 480, 320);
let reportList = Canvas.createReportList(myCanvas.id);

// draw a square
let square1 = Square.draw(myCanvas.ctx, 50, 50, 100, 'blue');
Square.reportArea(square1.length, reportList);
Square.reportPerimeter(square1.length, reportList);

// draw a circle
let circle1 = Circle.draw(myCanvas.ctx, 75, 200, 100, 'green');
Circle.reportArea(circle1.radius, reportList);
Circle.reportPerimeter(circle1.radius, reportList);

// draw a triangle
let triangle1 = Triangle.draw(myCanvas.ctx, 100, 75, 190, 'yellow');
Triangle.reportArea(triangle1.length, reportList);
Triangle.reportPerimeter(triangle1.length, reportList);

MDN’s illustrates a snag I hit that modules can’t see any outside variables except those prefixed by window.whatever.

The canvas module has a function, create, which returns {ctx: ctx, id: id};

In the examples, the modules contain functions which require all the required information to be passed to them as arguments, and return values. There are no environment variables.

Sharing data between modules

That modules don’t share scope in some ways encourages low coupling which is considered good design.

The best form of coupling is by passing only data. One way to do this in bulk is to pass a global object as a parameter. According to jslint, modules should freeze their objects to avoid mutation.

JavaScript modules can be both importers and exporters, remembering imported values can only be modified by the exporter.

But I think having modules import other modules encourages bad (ie high) coupling. Furthermore with browsers there’s the snag filenames need to be changed once edited to so old, cached versions aren’t used. Having to update multiple files is bound to lead to problems.

Here are some hacks to share data when required between modules:

Global window namespace

window.canvas = document.querySelector("canvas");
window.ctx = window.canvas.getContext("2d");

Making such global variables is frowned upon. Please try to avoid them. — javascript.info

My view is DOM elements such as canvas, ctx etc are pinned to the browser’s glogal window object anyhow, so that’s the easiest way to share them between modules.

Web Storage API

localStorage.myKey = "myValue";

or

localStorage["myKey"] = "myValue";

Apparently it’s better to use localStorage.setItem("myKey", "myValue"); and localStorage.getItem("myKey");. getItem has the advantage of being testable:

if (!localStorage.getItem("bgcolor")) {
  populateStorage();
} else {
  setStyles();
}

Other that when a test is needed, simply treating localStorage like any other namespace object seems better to me, with one important caveat:

Storage only supports storing and retrieving strings. If you want to save other data types, you have to convert them to strings. For plain objects and arrays, you can use JSON.stringify().