ponyfoo.com

Practical Modern JavaScript

Dive into ES6 and the future of JavaScript — Modular JavaScript Book Series
O’Reilly Media334 PagesISBN 978-1-4919-4353-3

“I am delighted to support Nicolás’ endeavor because his book looks exactly like what people who are coming to JavaScript with fresh eyes need.”

– Brendan Eich

Ideal for professional software developers with a basic understanding of JavaScript, this practical book shows you how to build small, interconnected ES6 JavaScript modules that emphasize reusability. You’ll learn how to face a project with a modular mindset, and how to organize your applications into simple pieces that work well in isolation and can be combined to create a large, robust application.

This book focuses on two aspects of JavaScript development: modularity and ES6 features. You’ll learn how to tackle application development by following a scale-out approach. As pieces of your codebase grow too big, you can break them up into smaller modules.

The book can be read online for free or purchased through Amazon.

This book is part of the Modular JavaScript series.

🗞 Start with the book series launch announcement on Pony Foo
💳 Participate in the crowdfunding campaign on Indiegogo
🌩 Amplify the announcement on social media via Thunderclap
🐤 Share a message on Twitter or within your social circles
👏 Contribute to the source code repository on GitHub
🦄 Read the free HTML version of the book on Pony Foo
📓 Purchase the book from O’Reilly on Amazon

Chapter 8

JavaScript Modules

Over the years, we’ve seen multiple different ways in which to split code into more manageable units. For the longest time we’ve had the module pattern, where you simply wrapped pieces of code in self-invoking function expressions. You had to be careful to sort your scripts so that each script came after all of its dependencies.

A while later, the RequireJS library was born. It provided a way of defining the dependencies of each module programmatically, so that a dependency graph is created and you wouldn’t have to worry about sorting your scripts anymore. RequireJS demands that you provide an array of strings used to identify your dependencies and also wrap modules in a function call, which would then receive those dependencies as parameters. Many other libraries provide similar functionality but offer a slightly different API.

Other complexity management mechanisms exist, such as the dependency injection mechanism in AngularJS, where you define named components using functions where you can, in turn, specify other named component dependencies. AngularJS carries the load of dependency injection on your behalf, so you only have to name components and specify dependencies.

CommonJS (CJS) surfaced as an alternative to RequireJS, and it was swiftly popularized by Node.js soon afterwards. In this chapter we’ll take a look at CommonJS, which is still heavily in use today. We’ll then cover the module system introduced to native JavaScript in ES6, and lastly we’ll explore interoperability between CommonJS and native JavaScript modules—also known as ECMAScript modules (ESM).

CommonJS

Unlike other module formats where modules are declared programmatically, in CommonJS every file is a module. CommonJS modules have an implicit local scope, while the global scope needs to be accessed explicitly. CommonJS modules can dynamically export a public interface consumers can interact with. CommonJS modules import their dependencies dynamically as well, resolving dependencies through require function calls. These require function calls are synchronous and return the interface exposed by required modules.

Interpreting the definition of a module format without looking at some code can be confusing. The following code snippet shows what a reusable CommonJS module file may look like. Both the has and union functions are local to our module’s scope. Given that we’ve assigned union to module.exports, that’ll be the public API for our module.

function has(list, item) {
  return list.includes(item)
}
function union(list, item) {
  if (has(list, item)) {
    return list
  }
  return [...list, item]
}
module.exports = union

Suppose we take that snippet of code and save it as union.js. We can now consume union.js in another CommonJS module. Let’s call that one app.js. In order to consume union.js, we call require passing in a relative path to the union.js file.

const union = require('./union.js')
console.log(union([1, 2], 3))
// <- [1, 2, 3]
console.log(union([1, 2], 2))
// <- [1, 2]
Warning

We can omit the file extension as long as it’s .js or .json, but this is discouraged.

While the file extension is optional for require statements and when using the node CLI, we should strongly consider getting into the habit of including it nevertheless. Browser implementations of ESM won’t have this luxury, since that’d entail extra roundtrips to figure out the correct endpoint for a JavaScript module HTTP resource.

We could run _app.js_ in its current state through the CLI for Node.js, node, as seen in the next snippet.

» node app.js
# [1, 2, 3]
# [1, 2]
Note

After installing Node.js, you’ll be able to use the node program in your terminal.

The require function in CJS can be treated dynamically, just like any other JavaScript function. This aspect of require is sometimes leveraged to dynamically require different modules that conform to one interface. As an example, let’s conjure up a templates directory with a number of view template functions. Our templates will take a model and return an HTML string.

The template found in the following code snippet renders an item of a grocery shopping list by reading its attributes from a model object.

// views/item.js
module.exports = model => `<li>
  <span>${ model.amount }</span>
  <span>x </span>
  <span>${ model.name }</span>
</li>`

Our application could print a <li> by leveraging the item.js view template.

// app.js
const renderItem = require('./views/item.js')
const html = renderItem({
  name: 'Banana bread',
  amount: 3
})
console.log(html)

Figure 8-1 shows our tiny application in action.

Printing an item for our grocery shopping list
Figure 8-1. Rendering a model as HTML is as easy as saying template literal expression interpolation!

The next template we’ll make renders the grocery list itself. It receives an array of items, and renders each of them by reusing the item.js template from the previous code snippet.

// views/list.js
const renderItem = require('./item.js')

module.exports = model => `<ul>
  ${ model.map(renderItem).join('\n') }
</ul>`

We can consume the list.js template in a very similar way to what we did before, but we’ll need to adjust the model passed into the template so that we provide a collection of items instead of a single one.

// app.js
const renderList = require('./views/list.js')
const html = renderList([{
  name: 'Banana bread',
  amount: 3
}, {
  name: 'Chocolate chip muffin',
  amount: 2
}])
console.log(html)

Figure 8-2 shows our updated application in all its glory.

Printing a grocery shopping list
Figure 8-2. Composing components made with template literals can be as simple as we choose to make them

In the examples so far, we’ve written short modules that are only concerned with producing an HTML view after matching a model object with the corresponding view template. A simple API encourages reusability, which is why we’re easily able to render the items for a list by mapping their models to the item.js templating function, and joining their HTML representations with newlines.

Given that the views all have a similar API where they take a model and return an HTML string, we can treat them uniformly. If we wanted a render function that could render any template, we could easily do that, thanks to the dynamic nature of require. The next example shows how we can construct the path to a template module. An important distinction is how require calls don’t necessarily need to be on the top level of a module. Calls to require can be anywhere, even embedded within other functions.

// render.js
module.exports = function render(template, model) {
  return require(`./views/${ template }`.js)(model)
}

Once we had such an API, we wouldn’t have to worry about carefully constructing require statements that match the directory structure of our view templates, because the render.js module could take care of that. Rendering any template becomes a matter of calling the render function with the template’s name and the model for that template, as demonstrated in the following code and Figure 8-3.

// app.js
const render = require('./render.js')
console.log(render('item', {
  name: 'Banana bread',
  amount: 1
}))
console.log(render('list', [{
  name: 'Apple pie',
  amount: 2
}, {
  name: 'Roasted almond',
  amount: 25
}]))
Printing different views through a normalized render function.
Figure 8-3. Creating a bare bones HTML rendering application is made easy by template literals

Moving on, you’ll notice that ES6 modules are somewhat influenced by CommonJS. In the next few sections we’ll look at export and import statements, and learn how ESM is compatible with CJS.

1
Read this comprehensive article about strict mode on Mozilla’s MDN.
Unlock with one Tweet!
Grants you full online access to Practical Modern JavaScript!
You can also read the book on the public git repository, but it won’t be as pretty! 😅