Book Image

Modern Frontend Development with Node.js

By : Florian Rappl
5 (1)
Book Image

Modern Frontend Development with Node.js

5 (1)
By: Florian Rappl

Overview of this book

Almost a decade after the release of Node.js, the tooling used by frontend developers is fully embracing this cross-platform JavaScript runtime, which is sadly often limited to server-side web development. This is where this Node.js book comes in, showing you what this popular runtime has to offer and how you can unlock its full potential to create frontend-focused web apps. You’ll begin by learning the basics and internals of Node.js, before discovering how to divide your code into modules and packages. Next, you’ll get to grips with the most popular package managers and their uses and find out how to use TypeScript and other JavaScript variants with Node.js. Knowing which tool to use when is crucial, so this book helps you understand all the available state-of-the-art tools in Node.js. You’ll interact with linters such as ESLint and formatters such as Prettier. As you advance, you’ll become well-versed with the Swiss Army Knife for frontend developers – the bundler. You’ll also explore various testing utilities, such as Jest, for code quality verification. Finally, you’ll be able to publish your code in reusable packages with ease. By the end of this web development book, you’ll have gained the knowledge to confidently choose the right code structure for your repositories with all that you’ve learned about monorepos.
Table of Contents (17 chapters)
1
Part 1: Node.js Fundamentals
5
Part 2: Tooling
10
Part 3: Advanced Topics

CommonJS

One thing that Node.js got right from the beginning was to introduce an explicit way of obtaining and using functionality. JavaScript in the browser suffered from the global scope problem, which caused many headaches for developers.

Global scope

In JavaScript, the global scope refers to functionality that is accessible from every script running in the same application. On a website, the global scope is usually the same as the window variable. Attaching variables to the global scope may be convenient and sometimes even necessary, but it may also lead to conflicts. For instance, two independent functions could both try to write and read from the same variable. The resulting behavior can then be hard to debug and very tricky to resolve. The standard recommendation is to avoid using the global scope as much as possible.

The idea that other functionalities are explicitly imported was certainly not new when Node.js was introduced. While an import mechanism existed in other programming languages or frameworks for quite some time, similar options have also been available for JavaScript in the browser – via third-party libraries such as RequireJS.

Node.js introduced its module system with the name CommonJS. The basis for Node.js’s implementation was actually a project developed at Mozilla. In that project, Mozilla worked on a range of proposals that started with non-browser use but later on expanded to a generic set of JavaScript specifications for a module system.

CommonJS implementations

Besides the implementation in Node.js, many other runtimes or frameworks use CommonJS. As an example, the JavaScript that can be used in the MongoDB database leverages a module system using the CommonJS specifications. The implementation in Node.js is actually only partially fulfilling the full specification.

A module system is crucial for allowing the inclusion of more functionality in a very transparent and explicit manner. In addition to a set of more advanced functionality, a module system gives us the following:

  • A way of including more functionality (in CommonJS, via the global require function)
  • A way of exposing functionality, which can then be included somewhere else (in CommonJS, via the module-specific module or exports variables)

At its core, the way CommonJS works is quite simple. Imagine you have a file called a.js, which contains the following code:

const b = require('./b.js');
console.log('The value of b is:', b.myValue);

Now the job of Node.js would be to actually make this work, that is, give the b variable a value that represents the so-called exports of the module. Right now, the script would error out saying that a b.js file is missing.

The b.js file, which should be adjacent to a.js, reads as follows:

exports.myValue = 42;

When Node.js evaluates the file, it will remember the defined exports. In this case, Node.js will know that b.js is actually just an object with a myValue key with a value of 42.

From the perspective of a.js, the code can therefore be read like this:

const b = {
  myValue: 42,
};
console.log('The value of b is:', b.myValue);

The advantage of using the module system is that there is no need to write the outputs of the module again. The call to require does that for us.

Side effects

Replacing the call to require with the module’s outputs is only meant for illustrative purposes. In general, this cannot be done as the module evaluation can have some so-called side effects. A side effect happens when implicit or explicit global variables are manipulated. For instance, already writing something to the console or outputting a file in the module evaluation is a side effect. If we’d only replace the require call with the imported module’s exports, we would not run the side effects, which would miss a crucial aspect of the module.

In the given example, we used the name of the file directly, but importing a module can be more subtle than that. Let’s see a refined version of the code:

a.js

const b = require('./b');
console.log('The value of b is:', b.myValue);

The call to./b.js has been replaced by ./b. This will still work, as Node.js will try various combinations for the given import. Not only will it append certain known extensions (such as .js) but it will also look at whether b is actually a directory with an index.js file.

Therefore, with the preceding code, we could actually move b.js from a file adjacent to a.js to an index.js file in the adjacent directory, b.

The greatest advantage, however, is that this syntax also allows us to import functionality from third-party packages. As we will explore later in Chapter 2, Dividing Code into Modules and Packages, our code has to be divided into different modules and packages. A package contains a set of reusable modules.

Node.js already comes with a set of packages that don’t even need to be installed. Let’s see a simple example:

host.js

const os = require('os');
console.log('The current hostname is:', os.hostname());

The preceding example uses the integrated os package to obtain the current computer’s network name.

We can run this script with node in the command line:

$ node host.js

The current hostname is: DESKTOP-3JMIDHE

This script works on every computer that has Node.js installed.