Let’s get started by developing a couple of simple programs that watch files for changes and read arguments from the command line. Even though they’re short, these applications offer insights into Node.js’s event-based architecture.
Watching files for changes is a convenient problem to start with because it demands asynchronous coding while demonstrating important Node.js concepts. Taking action whenever a file changes is just plain useful in a number of cases, ranging from automated deployments to running unit tests.
Open a terminal to begin. Create a new directory called filesystem and navigate down into it.
| | $ mkdir filesystem |
| | $ cd filesystem |
You’ll use this directory for all of the code examples in this chapter. Once there, use the touch command to create a file called target.txt.
| | $ touch target.txt |
If you’re in an environment that doesn’t have the touch command (like Windows), you can alternatively echo something to write the file.
| | $ echo, > target.txt |
This file will be the target for our watcher program. Now open your favorite text editor and enter the following:
| | 'use strict'; |
| | const fs = require('fs'); |
| | fs.watch('target.txt', () => console.log('File changed!')); |
| | console.log('Now watching target.txt for changes...'); |
Save this file as watcher.js in the filesystem directory alongside the target.txt file. Although this is a short program, it deserves scrutiny since it takes advantage of a number of JavaScript and Node.js features. Let’s step through it.
The program begins with the string ’use strict’ at the top. This causes the program to be executed in strict mode, a feature introduced in ECMAScript version 5. Strict mode disables certain problematic JavaScript language features and makes others throw exceptions. It’s always a good idea to use strict mode, and we’ll use it throughout the book.
Next, notice the const keyword; this sets up fs to be a local variable with a constant value. A variable declared with const must be assigned a value when declared, and can never have anything assigned to it again (which would cause a runtime error).
It might surprise you, but it turns out that most of the time, in most code, variables don’t need to be reassigned, making const a good default choice for declaring variables. The alternative to const is let, which we’ll discuss shortly.
The require() function pulls in a Node.js module and returns it. In our case, we’re calling require(’fs’) to incorporate Node.js’s built-in filesystem module.[17]
In Node.js, a module is a self-contained bit of JavaScript that provides functionality to be used elsewhere. The output of require() is usually a plain old JavaScript object, but may also be a function. Modules can depend on other modules, much like libraries in other programming environments, which import or #include other libraries.
Next we call the fs module’s watch() method, which takes a path to a file and a callback function to invoke whenever the file changes. In JavaScript, functions are first-class citizens. This means they can be assigned to variables and passed as parameters to other functions. Take a close look at our callback function:
| | () => console.log('File changed!') |
This is an arrow-function expression, sometimes called a fat arrow function or just an arrow function. The empty pair of parentheses () at the beginning means this function expects no arguments. Then the body of the function uses console.log to echo a message to standard output.
Arrow functions are new in ECMAScript 2015 and you’ll be writing many such functions throughout this book. Prior to the introduction of arrow functions, you’d have supplied a callback using the more verbose function(){} construction:
| | function() { |
| | console.log('File changed!'); |
| | } |
Aside from having a terser syntax than older function expressions, arrow functions have another big advantage over their ancestral counterparts: they do not create a new scope for this. Dealing with this has been a thorn in the side of many JavaScript developers over the years, but thanks to arrow functions, it’s no longer a major source of consternation. Just like const should be your go-to means of declaring variables, arrow functions should be your first choice in declaring function expressions (such as callbacks).
The last line of the program just informs you that everything is ready. Let’s try it out! Return to the command line and launch the watcher program using node, like so:
| | $ node watcher.js |
| | Now watching target.txt for changes... |
After the program starts, Node.js will patiently wait until the target file is changed. To trigger a change, open another terminal to the same directory and touch the file again. The terminal running watcher.js will output the string File changed!, and then the program will go back to waiting.
If you see duplicate messages, particularly on Mac OS X or Windows, this is not a bug in your code! There are a number of known issues around this, and many have to do with how the operating system surfaces changes.
Since you’ll be touching the target file a lot this chapter to trigger changes, you might want to use the watch command to do this automatically:
| | $ watch -n 1 touch target.txt |
This command will touch the target file once every second until you stop it. If you’re on a system that doesn’t have the watch command, don’t worry. Any means of writing to target.txt is fine.
The program we wrote in the last section is a good example of the Node.js event loop at work. Recall the event-loop figure from How Node.js Applications Work. Our simple file-watcher program causes Node.js to go through each of these steps, one by one.
To run the program, Node.js does the following:
It loads the script, running all the way through to the last line, which produces the Now watching message in the console.
It sees that there’s more to do because of the call to fs.watch.
It waits for something to happen—namely, for the fs module to observe a change to the file.
It executes our callback function when the change is detected.
It determines that the program still has not finished, and resumes waiting.
In Node.js the event loop spins until there’s nothing left to do, there’s nothing left to wait for, or the program exits by some other means. For example, if an exception is thrown and not caught, the process will exit. We’ll look at how this works next.
Now let’s make our program more useful by taking in the file to watch as a command-line argument. This will introduce the process global object and how Node.js deals with exceptions.
Open your editor and enter this:
| | const fs = require('fs'); |
| | const filename = process.argv[2]; |
| | if (!filename) { |
| | throw Error('A file to watch must be specified!'); |
| | } |
| | fs.watch(filename, () => console.log(`File ${filename} changed!`)); |
| | console.log(`Now watching ${filename} for changes...`); |
Save the file as watcher-argv.js and run it like so:
| | $ node watcher-argv.js target.txt |
| | Now watching target.txt for changes... |
You should see output and behavior that’s nearly identical to that of the first watcher.js program. After outputting Now watching target.txt for changes... the script will diligently wait for changes to the target file.
This program uses process.argv to access the incoming command-line arguments. argv stands for argument vector; it’s an array containing node and the full path to the watcher-argv.js as its first two elements. The third element (that is, at index 2) is target.txt, the name of our target file.
Note the use of backtick characters (‘) to mark the strings logged in this program:
| | `File ${filename} changed!` |
These are called template strings. They can span multiple lines and they support expression interpolation, meaning you can place an expression inside of ${} and it will insert the stringified result.
If a target filename is not provided to watcher-argv.js, the program will throw an exception. You see try that by simply omitting the target.txt parameter:
| | $ node watcher-argv.js |
| | /full/path/to/script/watcher-argv.js:4 |
| | throw Error('A file to watch must be specified!'); |
| | ^ |
| | |
| | Error: A file to watch must be specified! |
Any unhandled exception thrown in Node.js will halt the process. The exception output shows the offending file and the line number and position of the exception.
Processes are important in Node. It’s pretty common in Node.js development to spawn separate processes as a way of breaking up work, rather than putting everything into one big Node.js program. In the next section, you’ll learn how to spawn a process in Node.