Topic 05

Modules: Code in Several Files

JavaScriptHands-on

A script file only grows. The projects, the code that shows them, the theme switch, the contact form: put all of it in js/main.js and within a few chapters nobody, including you, can find anything in it. Modules let a program live in several files, each of which keeps its names to itself and shares only what it chooses. They also bring the first thing in this book that works only through the preview server, and this page shows why.

A kitchen with labelled drawers is organised the same way. Each drawer holds its own tools, and a recipe takes out only what it needs, by name. Nobody has to empty every drawer onto the counter to find the whisk.

export and import

A module is a JavaScript file loaded in a way that keeps its names private. Nothing made inside it can be used from another file unless the module says so. Putting the keyword export in front of a const or a function says so: it makes that one name available to other modules. Everything without export stays inside the file.

Create a new file, js/projects.js, next to main.js. Move the projects array into it from main.js, with export in front. The first part of the file is the array you already have, word for word, plus that one keyword.

js/projects.js, part 1
export const projects = [
  {
    title: "Budget Tracker",
    description: "Adds up a month of spending from a spreadsheet.",
    tags: ["javascript"],
    url: "https://example.com/budget"
  },
  {
    title: "Recipe Cards",
    description: "A responsive page of recipes that prints cleanly.",
    tags: ["html", "css"],
    url: "https://example.com/recipes"
  },
  {
    title: "Weather Now",
    description: "Shows today's forecast for any city.",
    tags: ["javascript", "api"],
    url: "https://example.com/weather"
  }
];

Below the array, the second part adds a function called renderProjects, also exported. To render something is to turn data into what the visitor sees. For now the function can only write each title in the Console, because putting elements on the page is the subject of Chapter 8. There, only the inside of this function changes; everything that calls it stays the same. Its parameter is also called projects: inside the function's braces, that name means whatever list the function was handed, following the scope rule from Chapter 6.

js/projects.js, part 2
export function renderProjects(projects) {
  for (const project of projects) {
    console.log(project.title);
  }
}

Now replace everything in main.js with the two lines below. The first is an import: it names what it wants in curly braces and says which file to take it from. The second calls the imported function with the imported array.

js/main.js
import { projects, renderProjects } from "./projects.js";

renderProjects(projects);

The path in quotes is worked out from the file doing the importing, not from the page. main.js and projects.js sit in the same folder, so the path starts with a dot and a slash, meaning "this folder". The extension is part of the path, too: in the browser, a path without the .js at the end points at a file that does not exist.

Loading a Module

The page has to be told that main.js is a module. In all three pages, replace the old script line from Chapter 6 with the one below: type="module" in place of defer. There is no script element for projects.js. The browser reads the import at the top of main.js and fetches that file by itself.

Fragment: index.html
<script type="module" src="js/main.js"></script>

A module does not need defer, because every module is deferred automatically: it downloads while the page is read and runs once the reading is done. Open the page through the preview server and look at the Console. It shows the three titles, Budget Tracker, Recipe Cards and Weather Now, now coming from projects.js. In the Network panel, you can see main.js requested first, then projects.js.

Console output
Budget Tracker
Recipe Cards
Weather Now

One difference from the earlier topics shows up at once. Type projects into the Console and you get a reference error: the name is not defined. Each module has its own scope, so the names at its top level are not shared with the whole page any more, and that includes the Console. That is the privacy modules promise, working as intended.

Why a Double-Clicked File Fails

Now try the other way of opening the page: double-click index.html in your file manager, so the address bar starts with file, colon and three slashes. The titles do not appear. Instead, the Console shows a red error, followed by a second red line saying the request for main.js failed.

Console output
Access to script at 'file:///.../my-site/js/main.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: chrome, chrome-extension, chrome-untrusted, data, http, https, isolated-app.

In words: access to the script main.js, from origin null, has been blocked by CORS policy, because such requests are only allowed for addresses starting with http, https and a few others. The dots in the middle of the file address stand for the folders on your own computer, which will differ. An origin is the part of an address that says which site a file comes from, and CORS is the set of rules browsers use to decide whether a page from one site may read files from another. Chapter 11 explains both properly.

For now the reason is enough. Browsers fetch every module with the same checks they use for files from other sites. A module from your own site, over an address starting with http, passes them without trouble. A page opened straight from your disk has no proper origin, which is why the message says null, and browsers deliberately treat such a page as a stranger to every other file, so that a page you downloaded cannot quietly read the rest of your computer. The fix is the preview server from Chapter 1: through it, the page has a real origin, and the module loads. The old defer script was not checked this way, which is why this is the first time the difference matters.

The Site's New Shape

After this topic the site's JavaScript has two files with separate jobs. projects.js holds the data and the code that shows it, and exports both. main.js is where the page's script starts: it imports what it needs and calls it. When a later chapter adds something new, it goes in a module of its own or in the one it belongs to, and main.js gains one import and one line.

That shape is about to be used. Chapter 8 rewrites the inside of renderProjects so that it builds a card for each project on the Projects page. Chapter 11 moves the array out into a data file and adds a function that loads it, in this same module. Each change lands in one place, which is what the split was for.

Two files, and the names that cross between them
module loaded by the page
js/main.js
imports from
./projects.js
exported name
projects: the array of three projects
exported name
renderProjects(projects)
Common Confusions
  • "Modules need a build tool." Browsers have run modules by themselves for years. The tools professionals add, named in Chapter 13, bundle modules together to load faster; they are not what makes modules work.
  • "The path can leave off .js." In the browser, the path must be exact, extension included, and it is worked out from the importing file.
  • "Everything in a module is shared." Only what is exported. Everything else stays private to its file, even from the Console.
Why It Matters
  • The site's code now has a shape that can grow without turning into one long file.
  • This is your first meeting with how browsers protect files by their origin, and Chapter 11 builds on it.

Knowledge Check

What does putting export in front of a function do?

  • It runs the function as soon as the file loads
  • It lets other modules import the function by name
  • It shows the function's code to anyone who visits
  • It copies the function into every file in the folder

Why does a page that uses modules fail when you double-click its HTML file?

  • Double-clicking opens an older copy of the page
  • Browsers cannot run modules without a build tool
  • The browser blocks modules from pages with no origin
  • The script element is missing its defer attribute

What changes when a script element gets type="module"?

  • The script runs before the page has been read
  • Every imported file needs its own script element
  • Its top-level names can be used from the Console
  • It is deferred, and it can import other files

js/main.js imports from js/projects.js. Which import path is right?

  • "./projects.js"
  • "../projects.js"
  • "./projects"
  • "projects.js"

You got correct