1
0
mirror of https://github.com/apachecn/eloquent-js-3e-zh.git synced 2025-05-23 20:02:20 +00:00
eloquent-js-3e-zh/diff-en/2ech20-3ech20.diff
wizardforcel 9faf107133 diff
2018-04-28 15:20:25 +08:00

905 lines
78 KiB
Diff

diff --git a/2ech20.md b/3ech20.md
index 2d2405b..5629452 100644
--- a/2ech20.md
+++ b/3ech20.md
@@ -4,48 +4,28 @@
>
> <footer>Master Yuan-Ma, <cite>The Book of Programming</cite></footer>
-So far, you have learned the JavaScript language and used it within a single environment: the browser. This chapter and the [next one](21_skillsharing.html#skillsharing) will briefly introduce you to Node.js, a program that allows you to apply your JavaScript skills outside of the browser. With it, you can build anything from simple command-line tools to dynamic HTTP servers.
+So far, we have used the JavaScript language in a single environment: the browser. This chapter and the [next one](21_skillsharing.html) will briefly introduce Node.js, a program that allows you to apply your JavaScript skills outside of the browser. With it, you can build anything from small command-line tools to HTTP servers that power dynamic websites.
-These chapters aim to teach you the important ideas that Node.js builds on and to give you enough information to write some useful programs for it. They do not try to be a complete, or even a thorough, treatment of Node.
+These chapters aim to teach you the main concepts that Node.js uses and to give you enough information to write useful programs for it. They do not try to be a complete, or even a thorough, treatment of the platform.
-Whereas you could run the code in previous chapters directly on these pages, since it was either raw JavaScript or written for the browser, the code samples in this chapter are written for Node and won't run in the browser.
+Whereas you could run the code in previous chapters directly on these pages, because it was either raw JavaScript or written for the browser, the code samples in this chapter are written for Node and often won't run in the browser.
-If you want to follow along and run the code in this chapter, start by going to [_nodejs.org_](http://nodejs.org) and following the installation instructions for your operating system. Also refer to that website for further documentation about Node and its built-in modules.
+If you want to follow along and run the code in this chapter, you'll need to install Node.js version 10 or higher. To do so, go to [_nodejs.org_](https://nodejs.org) and follow the installation instructions for your operating system. You can also find further documentation for Node.js there.
## Background
-One of the more difficult problems with writing systems that communicate over the network is managing input and output—that is, the reading and writing of data to and from the network, the hard drive, and other such devices. Moving data around takes time, and scheduling it cleverly can make a big difference in how quickly a system responds to the user or to network requests.
+One of the more difficult problems with writing systems that communicate over the network is managing input and output—that is, the reading and writing of data to and from the network and hard drive. Moving data around takes time, and scheduling it cleverly can make a big difference in how quickly a system responds to the user or to network requests.
-The traditional way to handle input and output is to have a function, such as `readFile`, start reading a file and return only when the file has been fully read. This is called _synchronous I/O_ (I/O stands for input/output).
+In such programs, asynchronous programming is often helpful. It allows the program to send and receive data from and to multiple devices at the same time without complicated thread management and synchronization.
-Node was initially conceived for the purpose of making _asynchronous_ I/O easy and convenient. We have seen asynchronous interfaces before, such as a browser's `XMLHttpRequest` object, discussed in [Chapter 17](17_http.html#xmlhttprequest). An asynchronous interface allows the script to continue running while it does its work and calls a callback function when it's done. This is the way Node does all its I/O.
-
-JavaScript lends itself well to a system like Node. It is one of the few programming languages that does not have a built-in way to do I/O. Thus, JavaScript could be fit onto Node's rather eccentric approach to I/O without ending up with two inconsistent interfaces. In 2009, when Node was being designed, people were already doing callback-based I/O in the browser, so the community around the language was used to an asynchronous programming style.
-
-## Asynchronicity
-
-I'll try to illustrate synchronous versus asynchronous I/O with a small example, where a program needs to fetch two resources from the Internet and then do some simple processing with the result.
-
-In a synchronous environment, the obvious way to perform this task is to make the requests one after the other. This method has the drawback that the second request will be started only when the first has finished. The total time taken will be at least the sum of the two response times. This is not an effective use of the machine, which will be mostly idle when it is transmitting and receiving data over the network.
-
-The solution to this problem, in a synchronous system, is to start additional threads of control. (Refer to [Chapter 14](14_event.html#timeline) for a previous discussion of threads.) A second thread could start the second request, and then both threads wait for their results to come back, after which they resynchronize to combine their results.
-
-In the following diagram, the thick lines represent time the program spends running normally, and the thin lines represent time spent waiting for I/O. In the synchronous model, the time taken by I/O is _part_ of the timeline for a given thread of control. In the asynchronous model, starting an I/O action conceptually causes a _split_ in the timeline. The thread that initiated the I/O continues running, and the I/O itself is done alongside it, finally calling a callback function when it is finished.
-
-![Control flow for synchronous and asynchronous I/O](img/control-io.svg)
-
-Another way to express this difference is that waiting for I/O to finish is _implicit_ in the synchronous model, while it is _explicit_, directly under our control, in the asynchronous one. But asynchronicity cuts both ways. It makes expressing programs that do not fit the straight-line model of control easier, but it also makes expressing programs that do follow a straight line more awkward.
-
-In [Chapter 17](17_http.html#promises), I already touched on the fact that all those callbacks add quite a lot of noise and indirection to a program. Whether this style of asynchronicity is a good idea in general can be debated. In any case, it takes some getting used to.
-
-But for a JavaScript-based system, I would argue that callback-style asynchronicity is a sensible choice. One of the strengths of JavaScript is its simplicity, and trying to add multiple threads of control to it would add a lot of complexity. Though callbacks don't tend to lead to simple _code_, as a _concept_, they're pleasantly simple yet powerful enough to write high-performance web servers.
+Node was initially conceived for the purpose of making asynchronous programming easy and convenient. JavaScript lends itself well to a system like Node. It is one of the few programming languages that does not have a built-in way to do in- and output. Thus, JavaScript could be fit onto Node's rather eccentric approach to in- and output without ending up with two inconsistent interfaces. In 2009, when Node was being designed, people were already doing callback-based programming in the browser, so the community around the language was used to an asynchronous programming style.
## The node command
When Node.js is installed on a system, it provides a program called `node`, which is used to run JavaScript files. Say you have a file `hello.js`, containing this code:
```
-var message = "Hello world";
+let message = "Hello world";
console.log(message);
```
@@ -56,7 +36,7 @@ $ node hello.js
Hello world
```
-The `console.log` method in Node does something similar to what it does in the browser. It prints out a piece of text. But in Node, the text will go to the process' standard output stream, rather than to a browser's JavaScript console.
+The `console.log` method in Node does something similar to what it does in the browser. It prints out a piece of text. But in Node, the text will go to the process' standard output stream, rather than to a browser's JavaScript console. When running `node` from the command line, that means you see the logged values in your terminal.
If you run `node` without giving it a file, it provides you with a prompt at which you can type JavaScript code and immediately see the result.
@@ -70,121 +50,137 @@ $ node
$
```
-The `process` variable, just like the `console` variable, is available globally in Node. It provides various ways to inspect and manipulate the current program. The `exit` method ends the process and can be given an exit status code, which tells the program that started `node` (in this case, the command-line shell) whether the program completed successfully (code zero) or encountered an error (any other code).
+The `process` binding, just like the `console` binding, is available globally in Node. It provides various ways to inspect and manipulate the current program. The `exit` method ends the process and can be given an exit status code, which tells the program that started `node` (in this case, the command-line shell) whether the program completed successfully (code zero) or encountered an error (any other code).
-To find the command-line arguments given to your script, you can read `process.argv`, which is an array of strings. Note that it also includes the name of the `node` command and your script name, so the actual arguments start at index 2\. If `showargv.js` simply contains the statement `console.log(process.argv)`, you could run it like this:
+To find the command-line arguments given to your script, you can read `process.argv`, which is an array of strings. Note that it also includes the name of the `node` command and your script name, so the actual arguments start at index 2\. If `showargv.js` contains the statement `console.<wbr>log(process.<wbr>argv)`, you could run it like this:
```
$ node showargv.js one --and two
-["node", "/home/marijn/showargv.js", "one", "--and", "two"]
+["node", "/tmp/showargv.js", "one", "--and", "two"]
```
-All the standard JavaScript global variables, such as `Array`, `Math`, and `JSON`, are also present in Node's environment. Browser-related functionality, such as `document` and `alert`, is absent.
-
-The global scope object, which is called `window` in the browser, has the more sensible name `global` in Node.
+All the standard JavaScript global bindings, such as `Array`, `Math`, and `JSON`, are also present in Node's environment. Browser-related functionality, such as `document` or `prompt`, is not.
## Modules
-Beyond the few variables I mentioned, such as `console` and `process`, Node puts little functionality in the global scope. If you want to access other built-in functionality, you have to ask the module system for it.
+Beyond the few bindings I mentioned, such as `console` and `process`, Node puts few bindings in the global scope. If you want to access built-in functionality, you have to ask the module system for it.
-The CommonJS module system, based on the `require` function, was described in [Chapter 10](10_modules.html#commonjs). This system is built into Node and is used to load anything from built-in modules to downloaded libraries to files that are part of your own program.
+The CommonJS module system, based on the `require` function, was described in [Chapter 10](10_modules.html#commonjs). This system is built into Node and is used to load anything from built-in modules to downloaded packages to files that are part of your own program.
-When `require` is called, Node has to resolve the given string to an actual file to load. Pathnames that start with `"/"`, `"./"`, or `"../"` are resolved relative to the current module's path, where `"./"` stands for the current directory, `"../"` for one directory up, and `"/"` for the root of the file system. So if you ask for `"./world/world"` from the file `/home/marijn/elife/run.js`, Node will try to load the file `/home/marijn/elife/world/world.js`. The `.js` extension may be omitted.
+When `require` is called, Node has to resolve the given string to an actual file that it can load. Pathnames that start with `"/"`, `"./"`, or `"../"` are resolved relative to the current module's path, where `"./"` stands for the current directory, `"../"` for one directory up, and `"/"` for the root of the file system. So if you ask for `"./<wbr>graph"` from the file `/<wbr>tmp/<wbr>robot/<wbr>robot.<wbr>js`, Node will try to load the file `/<wbr>tmp/<wbr>robot/<wbr>graph.<wbr>js`.
-When a string that does not look like a relative or absolute path is given to `require`, it is assumed to refer to either a built-in module or a module installed in a `node_modules` directory. For example, `require("fs")` will give you Node's built-in file system module, and `require("elife")` will try to load the library found in `node_modules/elife/`. A common way to install such libraries is by using NPM, which I will discuss in a moment.
+The `.js` extension may be omitted, and Node will add it if such a file exists. If the required path refers to a directory, Node will try to load the file named `index.js` in that directory.
-To illustrate the use of `require`, let's set up a simple project consisting of two files. The first one is called `main.js`, which defines a script that can be called from the command line to garble a string.
+When a string that does not look like a relative or absolute path is given to `require`, it is assumed to refer to either a built-in module or a module installed in a `node_modules` directory. For example, `require("fs")` will give you Node's built-in file system module. And `require("robot")` might try to load the library found in `node_modules/<wbr>robot/<wbr>`. A common way to install such libraries is by using NPM, which we'll come back to in a moment.
+
+Let's set up a small project consisting of two files. The first one is called `main.js`, and defines a script that can be called from the command line to reverse a string.
```
-var garble = require("./garble");
+const {reverse} = require("./reverse");
// Index 2 holds the first actual command-line argument
-var argument = process.argv[2];
+let argument = process.argv[2];
-console.log(garble(argument));
+console.log(reverse(argument));
```
-The file `garble.js` defines a library for garbling strings, which can be used both by the command-line tool defined earlier and by other scripts that need direct access to a garbling function.
+The file `reverse.js` defines a library for reversing strings, which can be used both by this command-line tool and by other scripts that need direct access to a string-reversing function.
```
-module.exports = function(string) {
- return string.split("").map(function(ch) {
- return String.fromCharCode(ch.charCodeAt(0) + 5);
- }).join("");
+exports.reverse = function(string) {
+ return Array.from(string).reverse().join("");
};
```
-Remember that replacing `module.exports`, rather than adding properties to it, allows us to export a specific value from a module. In this case, we make the result of requiring our `garble` file the garbling function itself.
-
-The function splits the string it is given into single characters by splitting on the empty string and then replaces each character with the character whose code is five points higher. Finally, it joins the result back into a string.
+Remember that adding properties to `exports` adds them to the interface of the module. Since Node.js treats files as CommonJS modules, `main.js` can take the exported `reverse` function from `reverse.js`.
We can now call our tool like this:
```
$ node main.js JavaScript
-Of{fXhwnuy
+tpircSavaJ
```
## Installing with NPM
-NPM, which was briefly discussed in [Chapter 10](10_modules.html#modules_npm), is an online repository of JavaScript modules, many of which are specifically written for Node. When you install Node on your computer, you also get a program called `npm`, which provides a convenient interface to this repository.
+NPM, which was introduced in [Chapter 10](10_modules.html#modules_npm), is an online repository of JavaScript modules, many of which are specifically written for Node. When you install Node on your computer, you also get the `npm`, which you can use to interact with this repository.
-For example, one module you will find on NPM is `figlet`, which can convert text into _ASCII art_—drawings made out of text characters. The following transcript shows how to install and use it:
+Its main use is downloading packages. We saw the `ini` package in [Chapter 10](10_modules.html#modules_ini). We can use NPM to fetch and install that package on our computer.
```
-$ npm install figlet
-npm GET https://registry.npmjs.org/figlet
-npm 200 https://registry.npmjs.org/figlet
-npm GET https://registry.npmjs.org/figlet/-/figlet-1.0.9.tgz
-npm 200 https://registry.npmjs.org/figlet/-/figlet-1.0.9.tgz
-figlet@1.0.9 node_modules/figlet
+$ npm install ini
+npm WARN enoent ENOENT: no such file or directory,
+ open '/tmp/package.json'
++ ini@1.3.5
+added 1 package in 0.552s
+
$ node
-> var figlet = require("figlet");
-> figlet.text("Hello world!", function(error, data) {
- if (error)
- console.error(error);
- else
- console.log(data);
- });
- _ _ _ _ _ _ _
- | | | | ___| | | ___ __ _____ _ __| | __| | |
- | |_| |/ _ \ | |/ _ \ \ \ /\ / / _ \| '__| |/ _` | |
- | _ | __/ | | (_) | \ V V / (_) | | | | (_| |_|
- |_| |_|\___|_|_|\___/ \_/\_/ \___/|_| |_|\__,_(_)
+> const {parse} = require("ini");
+> parse("x = 1\ny = 2");
+{ x: '1', y: '2' }
+```
+
+After running `npm install`, NPM will have created a directory called `node_modules`. Inside that directory will be an `ini` directory which contains the library. You can open it and look at the code. When we call `require("ini")`, this library is loaded, and we can call its `parse` property to parse a configuration file.
+
+By default NPM installs packages under the current directory, rather than in a central place. If you are used to other package managers, this may seem unusual, but it has advantages—it puts each application in full control of the packages it installs, and makes it easier to manage versions and clean up when removing an application.
+
+### Package files
+
+In the `npm install` example, you could see a warning about the fact that the `package.json` file did not exist. It is recommended to create such a file for each project, either manually or by running `npm init`. It contains some information about the project, such as its name and version, and lists its dependencies.
+
+The robot simulation from [Chapter 7](07_robot.html), as modularized in [Exercise 10.1](10_modules.html#modular_robot), might have a `package.json` file like this:
+
+```
+{
+ "author": "Marijn Haverbeke",
+ "name": "eloquent-javascript-robot",
+ "description": "Simulation of a package-delivery robot",
+ "version": "1.0.0",
+ "main": "run.js",
+ "dependencies": {
+ "dijkstrajs": "^1.0.1",
+ "random-item": "^1.0.0"
+ },
+ "license": "ISC"
+}
```
-After running `npm install`, NPM will have created a directory called `node_modules`. Inside that directory will be a `figlet` directory, which contains the library. When we run `node` and call `require("figlet")`, this library is loaded, and we can call its `text` method to draw some big letters.
+When you run `npm install` without naming a package to install, NPM will install the dependencies listed in `package.json`. When you install a specific package that is not already listed as a dependency, NPM will add it to `package.json`.
+
+### Versions
-Somewhat unexpectedly perhaps, instead of simply returning the string that makes up the big letters, `figlet.text` takes a callback function that it passes its result to. It also passes the callback another argument, `error`, which will hold an error object when something goes wrong or null when everything is all right.
+A `package.json` file lists both the program's own version and versions for its dependencies. Versions are a way to deal with the fact that packages evolve separately, and code written to work with a package as it existed at one point may not work with a later, modified version of the package.
-This is a common pattern in Node code. Rendering something with `figlet` requires the library to read a file that contains the letter shapes. Reading that file from disk is an asynchronous operation in Node, so `figlet.text` can't immediately return its result. Asynchronicity is infectious, in a way—every function that calls an asynchronous function must itself become asynchronous.
+NPM demands that its packages follow a schema called _semantic versioning_, which encodes some information about which versions are _compatible_ (don't break the old interface) in the version number. A semantic version consists of three numbers, separated by periods, such as `2.3.0`. Every time new functionality is added, the middle number has to be incremented. Every time compatibility is broken, so that existing code that uses the package might not work with the new version, the first number has to be incremented.
-There is much more to NPM than `npm install`. It reads `package.json` files, which contain JSON-encoded information about a program or library, such as which other libraries it depends on. Doing `npm install` in a directory that contains such a file will automatically install all dependencies, as well as _their_ dependencies. The `npm` tool is also used to publish libraries to NPM's online repository of packages so that other people can find, download, and use them.
+A caret character (`^`) in front of the version number for a dependency in `package.json` indicates that any version compatible with the given number may be installed. So for example `"^2.<wbr>3.<wbr>0"` would mean that any version greater than or equal to 2.3.0 and less than 3.0.0 is allowed.
-This book won't delve further into the details of NPM usage. Refer to [_npmjs.org_](http://npmjs.org) for further documentation and for an easy way to search for libraries.
+The `npm` command is also used to publish new packages or new versions of packages. If you `npm publish` in a directory that has a `package.json` file, it will publish a package with the name and version listed in the JSON file to the registry. Anyone can publish packages to NPM—though only under a new name, since it would be somewhat scary if random people could update existing packages.
+
+Since the `npm` program is a piece of software that talks to an open system—the package registry—there is nothing unique about what it does. Another program, `yarn`, which can be installed from the NPM registry, fills the same role as `npm` using a somewhat different interface and installation strategy.
+
+This book won't delve further into the details of NPM usage. Refer to [_npmjs.org_](https://npmjs.org) for further documentation and a way to search for packages.
## The file system module
-One of the most commonly used built-in modules that comes with Node is the `"fs"` module, which stands for _file system_. This module provides functions for working with files and directories.
+One of the most commonly used built-in modules in Node is the `fs` module, which stands for _file system_. It exports functions for working with files and directories.
-For example, there is a function called `readFile`, which reads a file and then calls a callback with the file's contents.
+For example, there is a function called `readFile` which reads a file and then calls a callback with the file's contents.
```
-var fs = require("fs");
-fs.readFile("file.txt", "utf8", function(error, text) {
- if (error)
- throw error;
- console.log("The file contained:", text);
+let {readFile} = require("fs");
+readFile("file.txt", "utf8", (error, text) => {
+ if (error) throw error;
+ console.log("The file contains:", text);
});
```
-The second argument to `readFile` indicates the _character encoding_ used to decode the file into a string. There are several ways in which text can be encoded to binary data, but most modern systems use UTF-8 to encode text, so unless you have reasons to believe another encoding is used, passing `"utf8"` when reading a text file is a safe bet. If you do not pass an encoding, Node will assume you are interested in the binary data and will give you a `Buffer` object instead of a string. This is an array-like object that contains numbers representing the bytes in the files.
+The second argument to `readFile` indicates the _character encoding_ used to decode the file into a string. There are several ways in which text can be encoded to binary data, but most modern systems use UTF-8\. So unless you have reasons to believe another encoding is used, pass `"utf8"` when reading a text file. If you do not pass an encoding, Node will assume you are interested in the binary data and will give you a `Buffer` object instead of a string. This is an array-like object that contains numbers representing the bytes (8-bit chunks of data) in the files.
```
-var fs = require("fs");
-fs.readFile("file.txt", function(error, buffer) {
- if (error)
- throw error;
+const {readFile} = require("fs");
+readFile("file.txt", (error, buffer) => {
+ if (error) throw error;
console.log("The file contained", buffer.length, "bytes.",
"The first byte is:", buffer[0]);
});
@@ -193,40 +189,50 @@ fs.readFile("file.txt", function(error, buffer) {
A similar function, `writeFile`, is used to write a file to disk.
```
-var fs = require("fs");
-fs.writeFile("graffiti.txt", "Node was here", function(err) {
- if (err)
- console.log("Failed to write file:", err);
- else
- console.log("File written.");
+const {writeFile} = require("fs");
+writeFile("graffiti.txt", "Node was here", err => {
+ if (err) console.log(`Failed to write file: ${err}`);
+ else console.log("File written.");
});
```
-Here, it was not necessary to specify the encoding since `writeFile` will assume that if it is given a string to write, rather than a `Buffer` object, it should write it out as text using its default character encoding, which is UTF-8.
+Here it was not necessary to specify the encoding—`writeFile` will assume that when it is given a string to write, rather than a `Buffer` object, it should write it out as text using its default character encoding, which is UTF-8.
+
+The `fs` module contains many other useful functions: `readdir` will return the files in a directory as an array of strings, `stat` will retrieve information about a file, `rename` will rename a file, `unlink` will remove one, and so on. See the documentation at [_nodejs.org_](https://nodejs.org) for specifics.
-The `"fs"` module contains many other useful functions: `readdir` will return the files in a directory as an array of strings, `stat` will retrieve information about a file, `rename` will rename a file, `unlink` will remove one, and so on. See the documentation at [_nodejs.org_](http://nodejs.org) for specifics.
+And most of these take a callback function as last parameter, which they call either with an error (the first argument), or a successful result (the second). As we saw in [Chapter 11](11_async.html), there are downsides to this style of programming—the biggest one being that error handling becomes verbose and error-prone.
-Many of the functions in `"fs"` come in both synchronous and asynchronous variants. For example, there is a synchronous version of `readFile` called `readFileSync`.
+Though promises have been part of JavaScript for a while, at the time of writing their integration into Node.js is still a work in progress. There is a package called `fs/promises` in the standard library since version 10, which exports most of the same functions as `fs`, but using promises rather than callback functions.
```
-var fs = require("fs");
-console.log(fs.readFileSync("file.txt", "utf8"));
+const {readFile} = require("fs/promises");
+readFile("file.txt", "utf8")
+ .then(text => console.log("The file contains:", text));
```
-Synchronous functions require less ceremony to use and can be useful in simple scripts, where the extra speed provided by asynchronous I/O is irrelevant. But note that while such a synchronous operation is being performed, your program will be stopped entirely. If it should be responding to the user or to other machines on the network, being stuck on synchronous I/O might produce annoying delays.
+Sometimes you don't need asynchronicity, and it just gets in the way. Many of the functions in `fs` also have a synchronous variant, which has the same name with `Sync` added to the end. For example, the synchronous version of `readFile` is called `readFileSync`.
+
+```
+const {readFileSync} = require("fs");
+console.log("The file contains:",
+ readFileSync("file.txt", "utf8"));
+```
+
+Do note that while such a synchronous operation is being performed, your program is stopped entirely. If it should be responding to the user or to other machines on the network, being stuck on a synchronous action might produce annoying delays.
## The HTTP module
-Another central module is called `"http"`. It provides functionality for running HTTP servers and making HTTP requests.
+Another central module is called `http`. It provides functionality for running HTTP servers and making HTTP requests.
-This is all it takes to start a simple HTTP server:
+This is all it takes to start an HTTP server:
```
-var http = require("http");
-var server = http.createServer(function(request, response) {
+const {createServer} = require("http");
+let server = createServer((request, response) => {
response.writeHead(200, {"Content-Type": "text/html"});
- response.write("<h1>Hello!</h1><p>You asked for <code>" +
- request.url + "</code></p>");
+ response.write(`
+ <h1>Hello!</h1>
+ <p>You asked for <code>${request.url}</code></p>`);
response.end();
});
server.listen(8000);
@@ -234,233 +240,254 @@ server.listen(8000);
If you run this script on your own machine, you can point your web browser at [_http://localhost:8000/hello_](http://localhost:8000/hello) to make a request to your server. It will respond with a small HTML page.
-The function passed as an argument to `createServer` is called every time a client tries to connect to the server. The `request` and `response` variables are objects representing the incoming and outgoing data. The first contains information about the request, such as its `url` property, which tells us to what URL the request was made.
+The function passed as argument to `createServer` is called every time a client connects to the server. The `request` and `response` bindings are objects representing the incoming and outgoing data. The first contains information about the request, such as its `url` property which tells us to what URL the request was made.
-To send something back, you call methods on the `response` object. The first, `writeHead`, will write out the response headers (see [Chapter 17](17_http.html#headers)). You give it the status code (200 for “OK” in this case) and an object that contains header values. Here we tell the client that we will be sending back an HTML document.
+So when you open that page in your browser, it sends a request to your own computer. This causes the server function to run and send back a response, which you can then see in the browser.
-Next, the actual response body (the document itself) is sent with `response.write`. You are allowed to call this method multiple times if you want to send the response piece by piece, possibly streaming data to the client as it becomes available. Finally, `response.end` signals the end of the response.
+To send something back, you call methods on the `response` object. The first, `writeHead`, will write out the response headers (see [Chapter 18](18_http.html#headers)). You give it the status code (200 for “OK” in this case) and an object that contains header values. The example sets the `Content-Type` header to inform the client that we'll be sending back an HTML document.
-The call to `server.listen` causes the server to start waiting for connections on port 8000\. This is the reason you have to connect to _localhost:8000_, rather than just _localhost_ (which would use the default port, 80), to speak to this server.
+Next, the actual response body (the document itself) is sent with `response.write`. You are allowed to call this method multiple times if you want to send the response piece by piece, for example to stream data to the client as it becomes available. Finally, `response.end` signals the end of the response.
-To stop running a Node script like this, which doesn't finish automatically because it is waiting for further events (in this case, network connections), press Ctrl-C.
+The call to `server.listen` causes the server to start waiting for connections on port 8000\. This is the reason you have to connect to _localhost:8000_ to speak to this server, rather than just _localhost_, which would use the default port 80.
-A real web server usually does more than the one in the previous example—it looks at the request's method (the `method` property) to see what action the client is trying to perform and at the request's URL to find out which resource this action is being performed on. You'll see a more advanced server [later in this chapter](20_node.html#file_server).
+When you run this script, the process just sits there and waits. When a script is listening for events—in this case, network connections—`node` will not automatically exit when it reaches the end of the script. To close it, press Ctrl-C.
-To act as an HTTP _client_, we can use the `request` function in the `"http"` module.
+A real web server usually does more than the one in the example—it looks at the request's method (the `method` property) to see what action the client is trying to perform and at the request's URL to find out which resource this action is being performed on. We'll see a more advanced server [later in this chapter](20_node.html#file_server).
+
+To act as an HTTP _client_, we can use the `request` function in the `http` module.
```
-var http = require("http");
-var request = http.request({
+const {request} = require("http");
+let requestStream = request({
hostname: "eloquentjavascript.net",
path: "/20_node.html",
method: "GET",
headers: {Accept: "text/html"}
-}, function(response) {
+}, response => {
console.log("Server responded with status code",
response.statusCode);
});
-request.end();
+requestStream.end();
```
The first argument to `request` configures the request, telling Node what server to talk to, what path to request from that server, which method to use, and so on. The second argument is the function that should be called when a response comes in. It is given an object that allows us to inspect the response, for example to find out its status code.
Just like the `response` object we saw in the server, the object returned by `request` allows us to stream data into the request with the `write` method and finish the request with the `end` method. The example does not use `write` because `GET` requests should not contain data in their request body.
-To make requests to secure HTTP (HTTPS) URLs, Node provides a package called `https`, which contains its own `request` function, similar to `http.request`.
+There's a similar `request` function in the `https` module, which can be used to make requests to `https:` URLs.
+
+But making request with Node's raw functionality is rather verbose. There are much more convenient wrapper packages available on NPM. For example `node-fetch` provides the promise-based `fetch` interface that we know from the browser.
## Streams
-We have seen two examples of writable streams in the HTTP examples—namely, the response object that the server could write to and the request object that was returned from `http.request`.
+We have seen two instances of writable streams in the HTTP examples—namely, the response object that the server could write to and the request object that was returned from `request`.
-Writable streams are a widely used concept in Node interfaces. All writable streams have a `write` method, which can be passed a string or a `Buffer` object. Their `end` method closes the stream and, if given an argument, will also write out a piece of data before it does so. Both of these methods can also be given a callback as an additional argument, which they will call when the writing to or closing of the stream has finished.
+Writable streams are a widely used concept in Node. Such objects have a `write` method, which can be passed a string or a `Buffer` object to write something to the stream. Their `end` method closes the stream, and also optionally takes a value to write to the stream before closing. Both of these methods can also be given a callback as an additional argument, which they will call when the writing or closing has finished.
-It is possible to create a writable stream that points at a file with the `fs.createWriteStream` function. Then you can use the `write` method on the resulting object to write the file one piece at a time, rather than in one shot as with `fs.writeFile`.
+It is possible to create a writable stream that points at a file with the `createWriteStream` function from the `fs` module. Then you can use the `write` method on the resulting object to write the file one piece at a time, rather than in one shot as with `writeFile`.
-Readable streams are a little more involved. Both the `request` variable that was passed to the HTTP server's callback function and the `response` variable passed to the HTTP client are readable streams. (A server reads requests and then writes responses, whereas a client first writes a request and then reads a response.) Reading from a stream is done using event handlers, rather than methods.
+Readable streams are a little more involved. Both the `request` binding that was passed to the HTTP server's callback and the `response` binding passed to the HTTP client callback are readable streams—a server reads requests and then writes responses, whereas a client first writes a request and then reads a response. Reading from a stream is done using event handlers, rather than methods.
Objects that emit events in Node have a method called `on` that is similar to the `addEventListener` method in the browser. You give it an event name and then a function, and it will register that function to be called whenever the given event occurs.
-Readable streams have `"data"` and `"end"` events. The first is fired every time some data comes in, and the second is called whenever the stream is at its end. This model is most suited for “streaming” data, which can be immediately processed, even when the whole document isn't available yet. A file can be read as a readable stream by using the `fs.createReadStream` function.
+Readable streams have `"data"` and `"end"` events. The first is fired every time data comes in, and the second is called whenever the stream is at its end. This model is most suited for _streaming_ data, which can be immediately processed, even when the whole document isn't available yet. A file can be read as a readable stream by using the `createReadStream` function from `fs`.
-The following code creates a server that reads request bodies and streams them back to the client as all-uppercase text:
+This code creates a server that reads request bodies and streams them back to the client as all-uppercase text:
```
-var http = require("http");
-http.createServer(function(request, response) {
+const {createServer} = require("http");
+createServer((request, response) => {
response.writeHead(200, {"Content-Type": "text/plain"});
- request.on("data", function(chunk) {
- response.write(chunk.toString().toUpperCase());
- });
- request.on("end", function() {
- response.end();
- });
+ request.on("data", chunk =>
+ response.write(chunk.toString().toUpperCase()));
+ request.on("end", () => response.end());
}).listen(8000);
```
-The `chunk` variable passed to the data handler will be a binary `Buffer`, which we can convert to a string by calling `toString` on it, which will decode it using the default encoding (UTF-8).
+The `chunk` value passed to the data handler will be a binary `Buffer`. We can convert this to string by decoding it as UTF-8 encoded characters with its `toString` method..
-The following piece of code, if run while the uppercasing server is running, will send a request to that server and write out the response it gets:
+The following piece of code, when run with the uppercasing server active, will send a request to that server and write out the response it gets:
```
-var http = require("http");
-var request = http.request({
+const {request} = require("http");
+request({
hostname: "localhost",
port: 8000,
method: "POST"
-}, function(response) {
- response.on("data", function(chunk) {
- process.stdout.write(chunk.toString());
- });
-});
-request.end("Hello server");
+}, response => {
+ response.on("data", chunk =>
+ process.stdout.write(chunk.toString()));
+}).end("Hello server");
+// → HELLO SERVER
```
-The example writes to `process.stdout` (the process' standard output, as a writable stream) instead of using `console.log`. We can't use `console.log` because it adds an extra newline character after each piece of text that it writes, which isn't appropriate here.
+The example writes to `process.stdout` (the process' standard output, which is a writable stream) instead of using `console.log`. We can't use `console.log` because it adds an extra newline character after each piece of text that it writes, which isn't appropriate here since the response may come in as multiple chunks.
-## A simple file server
+## A file server
-Let's combine our newfound knowledge about HTTP servers and talking to the file system and create a bridge between them: an HTTP server that allows remote access to a file system. Such a server has many uses. It allows web applications to store and share data or give a group of people shared access to a bunch of files.
+Let's combine our newfound knowledge about HTTP servers and working with the file system to create a bridge between the two: an HTTP server that allows remote access to a file system. Such a server has all kinds of uses—it allows web applications to store and share data or can give a group of people shared access to a bunch of files.
When we treat files as HTTP resources, the HTTP methods `GET`, `PUT`, and `DELETE` can be used to read, write, and delete the files, respectively. We will interpret the path in the request as the path of the file that the request refers to.
-We probably don't want to share our whole file system, so we'll interpret these paths as starting in the server's working directory, which is the directory in which it was started. If I ran the server from `/home/marijn/public/` (or `C:\Users\marijn\public\` on Windows), then a request for `/file.txt` should refer to `/home/marijn/public/file.txt` (or `C:\Users\marijn\public\file.txt`).
+We probably don't want to share our whole file system, so we'll interpret these paths as starting in the server's working directory, which is the directory in which it was started. If I ran the server from `/tmp/public/` (or `C:\tmp\public\` on Windows), then a request for `/file.txt` should refer to `/&lt;wbr&gt;tmp/&lt;wbr&gt;public/&lt;wbr&gt;file.&lt;wbr&gt;txt` (or `C:\tmp\public\file.&lt;wbr&gt;txt`).
-We'll build the program piece by piece, using an object called `methods` to store the functions that handle the various HTTP methods.
+We'll build the program piece by piece, using an object called `methods` to store the functions that handle the various HTTP methods. Method handlers are `async` functions that get the request object as argument and return a promise that resolves to an object that describes the response.
```
-var http = require("http"), fs = require("fs");
+const {createServer} = require("http");
-var methods = Object.create(null);
+const methods = Object.create(null);
-http.createServer(function(request, response) {
- function respond(code, body, type) {
- if (!type) type = "text/plain";
- response.writeHead(code, {"Content-Type": type});
- if (body && body.pipe)
- body.pipe(response);
- else
- response.end(body);
- }
- if (request.method in methods)
- methods[request.method](urlToPath(request.url),
- respond, request);
- else
- respond(405, "Method " + request.method +
- " not allowed.");
+createServer((request, response) => {
+ let handler = methods[request.method] || notAllowed;
+ handler(request)
+ .catch(error => {
+ if (error.status != null) return error;
+ return {body: String(error), status: 500};
+ })
+ .then(({body, status = 200, type = "text/plain"}) => {
+ response.writeHead(status, {"Content-Type": type});
+ if (body && body.pipe) body.pipe(response);
+ else response.end(body);
+ });
}).listen(8000);
+
+async function notAllowed(request) {
+ return {
+ status: 405,
+ body: `Method ${request.method} not allowed.`
+ };
+}
```
-This starts a server that just returns 405 error responses, which is the code used to indicate that a given method isn't handled by the server.
+This starts a server that just returns 405 error responses, which is the code used to indicate that the server refuses to handle a given method.
-The `respond` function is passed to the functions that handle the various methods and acts as a callback to finish the request. It takes an HTTP status code, a body, and optionally a content type as arguments. If the value passed as the body is a readable stream, it will have a `pipe` method, which is used to forward a readable stream to a writable stream. If not, it is assumed to be either `null` (no body) or a string and is passed directly to the response's `end` method.
+When a request handler's promise is rejected, the `catch` call translates the error into a response object, if it isn't already, so that the server can send back an error response to inform the client that it failed to handle the request.
-To get a path from the URL in the request, the `urlToPath` function uses Node's built-in `"url"` module to parse the URL. It takes its pathname, which will be something like `/file.txt`, decodes that to get rid of the `%20`-style escape codes, and prefixes a single dot to produce a path relative to the current directory.
+The `status` field of the response description may be omitted, in which case it defaults to 200 (OK). The content type, in the `type` property, can also be left off, in which case the response is assumed to be plain text.
+
+When the value of `body` is a readable stream, it will have a `pipe` method, which is used to forward all content from a readable stream to a writable stream. If not, it is assumed to be either `null` (no body), a string, or a buffer, and is passed directly to the response's `end` method.
+
+To figure out which file path corresponds to a request URL, the `urlPath` function uses Node's built-in `url` module to parse the URL. It takes its pathname, which will be something like `"/&lt;wbr&gt;file.&lt;wbr&gt;txt"`, decodes that to get rid of the `%20`-style escape codes, and resolves it relative to the program's working directory.
```
-function urlToPath(url) {
- var path = require("url").parse(url).pathname;
- return "." + decodeURIComponent(path);
+const {parse} = require("url");
+const {resolve} = require("path");
+
+const baseDirectory = process.cwd();
+
+function urlPath(url) {
+ let {pathname} = parse(url);
+ let path = resolve(decodeURIComponent(pathname).slice(1));
+ if (path != baseDirectory &&
+ !path.startsWith(baseDirectory + "/")) {
+ throw {status: 403, body: "Forbidden"};
+ }
+ return path;
}
```
-If you are worried about the security of the `urlToPath` function, you are right. We will return to that in the exercises.
+As soon as you set up a program to accept network requests, you have to start worrying about security. In this case, if we aren't careful, it is likely that we'll accidentally expose our whole file system to the network.
+
+File paths are strings in Node. To map such a string to an actual file, there is a nontrivial amount of interpretation going on. Paths may, for example, include `"../"` to refer to a parent directory. So one obvious source of problems would be requests for paths like `/../secret_file`.
+
+To avoid such problems, `urlPath` uses the `resolve` function from the `path` module, which resolves relative paths. It then verifies that the result is _below_ the working directory. The `process.cwd` function (where “cwd” stands for “current working directory”) can be used to find this working directory. When the path doesn't start with the base directory, the function throws an error response object, using the HTTP status code that indicates that access to the resource is forbidden.
-We will set up the `GET` method to return a list of files when reading a directory and to return the file's content when reading a regular file.
+We'll set up the `GET` method to return a list of files when reading a directory and to return the file's content when reading a regular file.
-One tricky question is what kind of `Content-Type` header we should add when returning a file's content. Since these files could be anything, our server can't simply return the same type for all of them. But NPM can help with that. The `mime` package (content type indicators like `text/plain` are also called _MIME types_) knows the correct type for a huge number of file extensions.
+One tricky question is what kind of `Content-Type` header we should set when returning a file's content. Since these files could be anything, our server can't simply return the same content type for all of them. NPM can help us again here. The `mime` package (content type indicators like `text/plain` are also called _MIME types_) knows the correct type for a large number of file extensions.
-If you run the following `npm` command in the directory where the server script lives, you'll be able to use `require("mime")` to get access to the library:
+The following `npm` command, in the directory where the server script lives, installs a specific version of `mime`.
```
-$ npm install mime@1.4.0
-npm http GET https://registry.npmjs.org/mime
-npm http 304 https://registry.npmjs.org/mime
-mime@1.4.0 node_modules/mime
+$ npm install mime@2.2.0
```
-When a requested file does not exist, the correct HTTP error code to return is 404\. We will use `fs.stat`, which looks up information on a file, to find out both whether the file exists and whether it is a directory.
+When a requested file does not exist, the correct HTTP status code to return is 404\. We'll use the `stat` function, which looks up information about a file, to find out both whether the file exists and whether it is a directory.
```
-methods.GET = function(path, respond) {
- fs.stat(path, function(error, stats) {
- if (error && error.code == "ENOENT")
- respond(404, "File not found");
- else if (error)
- respond(500, error.toString());
- else if (stats.isDirectory())
- fs.readdir(path, function(error, files) {
- if (error)
- respond(500, error.toString());
- else
- respond(200, files.join("\n"));
- });
- else
- respond(200, fs.createReadStream(path),
- require("mime").lookup(path));
- });
+const {createReadStream} = require("fs");
+const {stat, readdir} = require("fs/promises");
+const mime = require("mime");
+
+methods.GET = async function(request) {
+ let path = urlPath(request.url);
+ let stats;
+ try {
+ stats = await stat(path);
+ } catch (error) {
+ if (error.code != "ENOENT") throw error;
+ else return {status: 404, body: "File not found"};
+ }
+ if (stats.isDirectory()) {
+ return {body: (await readdir(path)).join("\n")};
+ } else {
+ return {body: createReadStream(path),
+ type: mime.getType(path)};
+ }
};
```
-Because it has to touch the disk and thus might take a while, `fs.stat` is asynchronous. When the file does not exist, `fs.stat` will pass an error object with a `code` property of `"ENOENT"` to its callback. It would be nice if Node defined different subtypes of `Error` for different types of error, but it doesn't. Instead, it just puts obscure, Unix-inspired codes in there.
+Because it has to touch the disk and thus might take a while, `stat` is asynchronous. Since we're using promises rather than callback style, it has to be imported from `fs/promises` instead of `fs`.
-We are going to report any errors we didn't expect with status code 500, which indicates that the problem exists in the server, as opposed to codes starting with 4 (such as 404), which refer to bad requests. There are some situations in which this is not entirely accurate, but for a small example program like this, it will have to be good enough.
+When the file does not exist `stat` will throw an error object with a `code` property of `"ENOENT"`. These somewhat obscure, Unix-inspired codes are how you recognize error types in Node.
-The `stats` object returned by `fs.stat` tells us a number of things about a file, such as its size (`size` property) and its modification date (`mtime` property). Here we are interested in the question of whether it is a directory or a regular file, which the `isDirectory` method tells us.
+The `stats` object returned by `stat` tells us a number of things about a file, such as its size (`size` property) and its modification date (`mtime` property). Here we are interested in the question of whether it is a directory or a regular file, which the `isDirectory` method tells us.
-We use `fs.readdir` to read the list of files in a directory and, in yet another callback, return it to the user. For normal files, we create a readable stream with `fs.createReadStream` and pass it to `respond`, along with the content type that the `"mime"` module gives us for the file's name.
+We use `readdir` to read the array of files in a directory and return it to the client. For normal files, we create a readable stream with `createReadStream` and return that as the body, along with the content type that the `mime` package gives us for the file's name.
The code to handle `DELETE` requests is slightly simpler.
```
-methods.DELETE = function(path, respond) {
- fs.stat(path, function(error, stats) {
- if (error && error.code == "ENOENT")
- respond(204);
- else if (error)
- respond(500, error.toString());
- else if (stats.isDirectory())
- fs.rmdir(path, respondErrorOrNothing(respond));
- else
- fs.unlink(path, respondErrorOrNothing(respond));
- });
+const {rmdir, unlink} = require("fs/promises");
+
+methods.DELETE = async function(request) {
+ let path = urlPath(request.url);
+ let stats;
+ try {
+ stats = await stat(path);
+ } catch (error) {
+ if (error.code != "ENOENT") throw error;
+ else return {status: 204};
+ }
+ if (stats.isDirectory()) await rmdir(path);
+ else await unlink(path);
+ return {status: 204};
};
```
-You may be wondering why trying to delete a nonexistent file returns a 204 status, rather than an error. When the file that is being deleted is not there, you could say that the request's objective is already fulfilled. The HTTP standard encourages people to make requests _idempotent_, which means that applying them multiple times does not produce a different result.
+When an HTTP response does not contain any data, the status code 204 (“no content”) can be used to indicate this. Since the response to deletion doesn't need to transmit any information beyond whether the operation succeeded, that is a sensible thing to return here.
-```
-function respondErrorOrNothing(respond) {
- return function(error) {
- if (error)
- respond(500, error.toString());
- else
- respond(204);
- };
-}
-```
-
-When an HTTP response does not contain any data, the status code 204 (“no content”) can be used to indicate this. Since we need to provide callbacks that either report an error or return a 204 response in a few different situations, I wrote a `respondErrorOrNothing` function that creates such a callback.
+You may be wondering why trying to delete a nonexistent file returns a success status code, rather than an error. When the file that is being deleted is not there, you could say that the request's objective is already fulfilled. The HTTP standard encourages us to make requests _idempotent_, which means that making the same request multiple times produces the same result as making it once. In a way, if you try to delete something that's already gone, the effect you were trying to do has been achieved—the thing is no longer there.
This is the handler for `PUT` requests:
```
-methods.PUT = function(path, respond, request) {
- var outStream = fs.createWriteStream(path);
- outStream.on("error", function(error) {
- respond(500, error.toString());
- });
- outStream.on("finish", function() {
- respond(204);
+const {createWriteStream} = require("fs");
+
+function pipeStream(from, to) {
+ return new Promise((resolve, reject) => {
+ from.on("error", reject);
+ to.on("error", reject);
+ to.on("finish", resolve);
+ from.pipe(to);
});
- request.pipe(outStream);
+}
+
+methods.PUT = async function(request) {
+ let path = urlPath(request.url);
+ await pipeStream(request, createWriteStream(path));
+ return {status: 204};
};
```
-Here, we don't need to check whether the file exists—if it does, we'll just overwrite it. We again use `pipe` to move data from a readable stream to a writable one, in this case from the request to the file. If creating the stream fails, an `"error"` event is raised for it, which we report in our response. When the data is transferred successfully, `pipe` will close both streams, which will cause a `"finish"` event to fire on the writable stream. When that happens, we can report success to the client with a 204 response.
+We don't need to check whether the file exists this time—if it does, we'll just overwrite it. We again use `pipe` to move data from a readable stream to a writable one, in this case from the request to the file. But since `pipe` isn't written to return a promise, we have to write a wrapper, `pipeStream` that creates a promise around the outcome of calling `pipe`.
+
+When something goes wrong when opening the file `createWriteStream` will still return a stream, but that stream will fire an `"error"` event. The output stream to the request may also fail, for example if the network goes down. So we wire up both streams' `"error"` events to reject the promise. When `pipe` is done, it will close the output stream, which causes it to fire a `"finish"` event. That's the point where we can successfully resolve the promise (returning nothing).
-The full script for the server is available at [_eloquentjavascript.net/2nd_edition/code/file_server.js_](http://eloquentjavascript.net/2nd_edition/code/file_server.js). You can download that and run it with Node to start your own file server. And of course, you can modify and extend it to solve this chapter's exercises or to experiment.
+The full script for the server is available at [_eloquentjavascript.net/code/file_server.js_](https://eloquentjavascript.net/code/file_server.js). You can download that and, after installing its dependencies, run it with Node to start your own file server. And of course, you can modify and extend it to solve this chapter's exercises or to experiment.
-The command-line tool `curl`, widely available on Unix-like systems, can be used to make HTTP requests. The following session briefly tests our server. Note that `-X` is used to set the request's method and `-d` is used to include a request body.
+The command-line tool `curl`, widely available on Unix-like systems (such as OS X and Linux), can be used to make HTTP requests. The following session briefly tests our server. The `-X` option is used to set the request's method and `-d` is used to include a request body.
```
$ curl http://localhost:8000/file.txt
@@ -475,158 +502,60 @@ File not found
The first request for `file.txt` fails since the file does not exist yet. The `PUT` request creates the file, and behold, the next request successfully retrieves it. After deleting it with a `DELETE` request, the file is again missing.
-## Error handling
-
-In the code for the file server, there are _six_ places where we are explicitly routing exceptions that we don't know how to handle into error responses. Because exceptions aren't automatically propagated to callbacks but rather passed to them as arguments, they have to be handled explicitly every time. This completely defeats the advantage of exception handling, namely, the ability to centralize the handling of failure conditions.
-
-What happens when something actually _throws_ an exception in this system? Since we are not using any `try` blocks, the exception will propagate to the top of the call stack. In Node, that aborts the program and writes information about the exception (including a stack trace) to the program's standard error stream.
-
-This means that our server will crash whenever a problem is encountered in the server's code itself, as opposed to asynchronous problems, which will be passed as arguments to the callbacks. If we wanted to handle all exceptions raised during the handling of a request, to make sure we send a response, we would have to add `try/catch` blocks to _every_ callback.
-
-This is not workable. Many Node programs are written to make as little use of exceptions as possible, with the assumption that if an exception is raised, it is not something the program can handle, and crashing is the right response.
-
-Another approach is to use promises, which were introduced in [Chapter 17](17_http.html#promises). Those catch exceptions raised by callback functions and propagate them as failures. It is possible to load a promise library in Node and use that to manage your asynchronous control. Few Node libraries integrate promises, but it is often trivial to wrap them. The excellent `"promise"` module from NPM contains a function called `denodeify`, which takes an asynchronous function like `fs.readFile` and converts it to a promise-returning function.
-
-```
-var Promise = require("promise");
-var fs = require("fs");
-
-var readFile = Promise.denodeify(fs.readFile);
-readFile("file.txt", "utf8").then(function(content) {
- console.log("The file contained: " + content);
-}, function(error) {
- console.log("Failed to read file: " + error);
-});
-```
-
-For comparison, I've written another version of the file server based on promises, which you can find at [_eloquentjavascript.net/2nd_edition/code/file_server_promises.js_](http://eloquentjavascript.net/2nd_edition/code/file_server_promises.js). It is slightly cleaner because functions can now _return_ their results, rather than having to call callbacks, and the routing of exceptions is implicit, rather than explicit.
-
-I'll list a few lines from the promise-based file server to illustrate the difference in the style of programming.
-
-The `fsp` object that is used by this code contains promise-style variants of a number of `fs` functions, wrapped by `Promise.denodeify`. The object returned from the method handler, with `code` and `body` properties, will become the final result of the chain of promises, and it will be used to determine what kind of response to send to the client.
-
-```
-methods.GET = function(path) {
- return inspectPath(path).then(function(stats) {
- if (!stats) // Does not exist
- return {code: 404, body: "File not found"};
- else if (stats.isDirectory())
- return fsp.readdir(path).then(function(files) {
- return {code: 200, body: files.join("\n")};
- });
- else
- return {code: 200,
- type: require("mime").lookup(path),
- body: fs.createReadStream(path)};
- });
-};
-
-function inspectPath(path) {
- return fsp.stat(path).then(null, function(error) {
- if (error.code == "ENOENT") return null;
- else throw error;
- });
-}
-```
-
-The `inspectPath` function is a simple wrapper around `fs.stat`, which handles the case where the file is not found. In that case, we replace the failure with a success that yields `null`. All other errors are allowed to propagate. When the promise that is returned from these handlers fails, the HTTP server responds with a 500 status code.
-
## Summary
-Node is a nice, straightforward system that lets us run JavaScript in a nonbrowser context. It was originally designed for network tasks to play the role of a _node_ in a network. But it lends itself to all kinds of scripting tasks, and if writing JavaScript is something you enjoy, automating everyday tasks with Node works wonderfully.
+Node is a nice, small system that lets us run JavaScript in a nonbrowser context. It was originally designed for network tasks to play the role of a _node_ in a network. But it lends itself to all kinds of scripting tasks, and if writing JavaScript is something you enjoy, automating tasks with Node works very well.
-NPM provides libraries for everything you can think of (and quite a few things you'd probably never think of), and it allows you to fetch and install those libraries by running a simple command. Node also comes with a number of built-in modules, including the `"fs"` module, for working with the file system, and the `"http"` module, for running HTTP servers and making HTTP requests.
+NPM provides packages for everything you can think of (and quite a few things you'd probably never think of), and it allows you to fetch and install those packages with the `npm` program. Node comes with a number of built-in modules, including the `fs` module for working with the file system and the `http` module for running HTTP servers and making HTTP requests.
-All input and output in Node is done asynchronously, unless you explicitly use a synchronous variant of a function, such as `fs.readFileSync`. You provide callback functions, and Node will call them at the appropriate time, when the I/O you asked for has finished.
+All input and output in Node is done asynchronously, unless you explicitly use a synchronous variant of a function, such as `readFileSync`. When calling such asynchronous functions you provide callback functions, and Node will call them with an error value and (if available) a result when it is ready.
## Exercises
-### Content negotiation, again
+### Search tool
-In [Chapter 17](17_http.html#exercise_accept), the first exercise was to make several requests to [_eloquentjavascript.net/author_](http://eloquentjavascript.net/author), asking for different types of content by passing different `Accept` headers.
+On Unix systems, there is a command-line tool called `grep` that can be used to quickly search files for a regular expression.
-Do this again, using Node's `http.request` function. Ask for at least the media types `text/plain`, `text/html`, and `application/json`. Remember that headers to a request can be given as an object, in the `headers` property of `http.request`'s first argument.
+Write a Node script that can be run from the command line and acts somewhat like `grep`. It treats its first command-line argument as a regular expression, and any further arguments as files to search. It should output the names of any file whose content matches the regular expression.
-Write out the content of the responses to each request.
+When that works, extend it so that when one of the arguments is a directory, it searches through all files in that directory and its subdirectories.
-Don't forget to call the `end` method on the object returned by `http.request` in order to actually fire off the request.
+Use asynchronous or synchronous file system functions as you see fit. Setting things up so that multiple asynchronous actions are requested at the same time might speed things up a little, but not a huge amount, since most file systems can only read one thing at a time.
-The response object passed to `http.request`'s callback is a readable stream. This means that it is not entirely trivial to get the whole response body from it. The following utility function reads a whole stream and calls a callback function with the result, using the usual pattern of passing any errors it encounters as the first argument to the callback:
+Your first command-line argument, the regular expression, can be found in `process.argv[2]`. The input files come after that. You can use the `RegExp` constructor to go from a string to a regular expression object.
-```
-function readStreamAsString(stream, callback) {
- var data = "";
- stream.on("data", function(chunk) {
- data += chunk.toString();
- });
- stream.on("end", function() {
- callback(null, data);
- });
- stream.on("error", function(error) {
- callback(error);
- });
-}
-```
+Doing this synchronously, with `readFileSync`, is more straightforward, but if you use `fs/promises` again to get promise-returning functions and write an `async` function, the code looks similar.
-### Fixing a leak
+To figure out whether something is a directory, you can again use `stat` (or `statSync`) and the stats object's `isDirectory` method.
-For easy remote access to some files, I might get into the habit of having the [file server](20_node.html#file_server) defined in this chapter running on my machine, in the `/home/marijn/public` directory. Then, one day, I find that someone has gained access to all the passwords I stored in my browser.
+Exploring a directory is a branching process. You can do it either with a recursive function or by keeping an array of work (files that still need to be explored). To find the files in a directory, you can call `readdir` or `readdirSync` (note the strange capitalization—Node's file system function naming is loosely based on standard Unix functions, such as `readdir`, which are all lowercase, but then it adds `Sync` with a capital letter).
-What happened?
+To go from a filename read with `readdir` to a full path name, you have to combine it with the name of the directory, putting a slash character (`/`) between them.
-If it isn't clear to you yet, think back to the `urlToPath` function, defined like this:
+### Directory creation
-```
-function urlToPath(url) {
- var path = require("url").parse(url).pathname;
- return "." + decodeURIComponent(path);
-}
-```
-
-Now consider the fact that paths passed to the `"fs"` functions can be relative—they may contain `"../"` to go up a directory. What happens when a client sends requests to URLs like the ones shown here?
-
-```
-http://myhostname:8000/../.config/config/google-chrome/Default/Web%20Data
-http://myhostname:8000/../.ssh/id_dsa
-http://myhostname:8000/../../../etc/passwd
-```
-
-Change `urlToPath` to fix this problem. Take into account the fact that Node on Windows allows both forward slashes and backslashes to separate directories.
-
-Also, meditate on the fact that as soon as you expose some half-baked system on the Internet, the bugs in that system might be used to do bad things to your machine.
+Though the `DELETE` method in our file server is able to delete directories (using `rmdir`), the server currently does not provide any way to _create_ a directory.
-It is enough to strip out all occurrences of two dots that have a slash, a backslash, or the end of the string on both sides. Using the `replace` method with a regular expression is the easiest way to do this. But since such instances may overlap (as in `"/../../f"`), you may have to apply `replace` multiple times, until the string no longer changes. Also make sure you do the replace _after_ decoding the string, or it would be possible to foil the check by encoding a dot or a slash.
+Add support for a `MKCOL` method (“make column”), which should create a directory by calling `mkdir` from the `fs` module. `MKCOL` is not a widely used HTTP method, but it does exist for this same purpose in the _WebDAV_ standard, which specifies a set of conventions on top of HTTP that make it suitable for creating documents.
-Another potentially worrying case is when paths start with a slash, which are interpreted as absolute paths. But because `urlToPath` puts a dot character in front of the path, it is impossible to create requests that result in such a path. Multiple slashes in a row, inside the path, are odd but will be treated as a single slash by the file system.
-
-### Creating directories
-
-Though the `DELETE` method is wired up to delete directories (using `fs.rmdir`), the file server currently does not provide any way to _create_ a directory.
-
-Add support for a method `MKCOL`, which should create a directory by calling `fs.mkdir`. `MKCOL` is not one of the basic HTTP methods, but it does exist, for this same purpose, in the _WebDAV_ standard, which specifies a set of extensions to HTTP, making it suitable for writing resources, not just reading them.
-
-You can use the function that implements the `DELETE` method as a blueprint for the `MKCOL` method. When no file is found, try to create a directory with `fs.mkdir`. When a directory exists at that path, you can return a 204 response so that directory creation requests are idempotent. If a nondirectory file exists here, return an error code. The code 400 (“bad request”) would be appropriate here.
+You can use the function that implements the `DELETE` method as a blueprint for the `MKCOL` method. When no file is found, try to create a directory with `mkdir`. When a directory exists at that path, you can return a 204 response so that directory creation requests are idempotent. If a nondirectory file exists here, return an error code. Code 400 (“bad request”) would be appropriate.
### A public space on the web
-Since the file server serves up any kind of file and even includes the right `Content-Type` header, you can use it to serve a website. Since it allows everybody to delete and replace files, it would be an interesting kind of website: one that can be modified, vandalized, and destroyed by everybody who takes the time to create the right HTTP request. Still, it would be a website.
+Since the file server serves up any kind of file and even includes the right `Content-Type` header, you can use it to serve a website. Since it allows everybody to delete and replace files, it would be an interesting kind of website: one that can be modified, improved, and vandalized by everybody who takes the time to create the right HTTP request.
Write a basic HTML page that includes a simple JavaScript file. Put the files in a directory served by the file server and open them in your browser.
-Next, as an advanced exercise or even a weekend project, combine all the knowledge you gained from this book to build a more user-friendly interface for modifying the website from _inside_ the website.
+Next, as an advanced exercise or even a weekend project, combine all the knowledge you gained from this book to build a more user-friendly interface for modifying the website—from _inside_ the website.
-Use an HTML form ([Chapter 18](18_forms.html#forms)) to edit the content of the files that make up the website, allowing the user to update them on the server by using HTTP requests as described in [Chapter 17](17_http.html#http).
+Use an HTML form to edit the content of the files that make up the website, allowing the user to update them on the server by using HTTP requests as described in [Chapter 18](18_http.html).
Start by making only a single file editable. Then make it so that the user can select which file to edit. Use the fact that our file server returns lists of files when reading a directory.
-Don't work directly in the code on the file server, since if you make a mistake you are likely to damage the files there. Instead, keep your work outside of the publicly accessible directory and copy it there when testing.
-
-If your computer is directly connected to the Internet, without a firewall, router, or other interfering device in between, you might be able to invite a friend to use your website. To check, go to [_whatismyip.com_](http://www.whatismyip.com/), copy the IP address it gives you into the address bar of your browser, and add `:8000` after it to select the right port. If that brings you to your site, it is online for everybody to see.
-
-You can create a `&lt;textarea&gt;` element to hold the content of the file that is being edited. A `GET` request, using `XMLHttpRequest`, can be used to get the current content of the file. You can use relative URLs like _index.html_, instead of [_http://localhost:8000/index.html_](http://localhost:8000/index.html), to refer to files on the same server as the running script.
+Don't work directly in the code exposed by the file server, since if you make a mistake you are likely to damage the files there. Instead, keep your work outside of the publicly accessible directory and copy it there when testing.
-Then, when the user clicks a button (you can use a `&lt;form&gt;` element and `"submit"` event or simply a `"click"` handler), make a `PUT` request to the same URL, with the content of the `&lt;textarea&gt;` as request body, to save the file.
+You can create a `&lt;textarea&gt;` element to hold the content of the file that is being edited. A `GET` request, using `fetch`, can retrieve the current content of the file. You can use relative URLs like _index.html_, instead of [_http://localhost:8000/index.html_](http://localhost:8000/index.html), to refer to files on the same server as the running script.
-You can then add a `&lt;select&gt;` element that contains all the files in the server's root directory by adding `&lt;option&gt;` elements containing the lines returned by a `GET` request to the URL `/`. When the user selects another file (a `"change"` event on the field), the script must fetch and display that file. Also make sure that when saving a file, you use the currently selected filename.
+Then, when the user clicks a button (you can use a `&lt;form&gt;` element and `"submit"` event), make a `PUT` request to the same URL, with the content of the `&lt;textarea&gt;` as request body, to save the file.
-Unfortunately, the server is too simplistic to be able to reliably read files from subdirectories since it does not tell us whether the thing we fetched with a `GET` request is a regular file or a directory. Can you think of a way to extend the server to address this?
+You can then add a `&lt;select&gt;` element that contains all the files in the server's top directory by adding `&lt;option&gt;` elements containing the lines returned by a `GET` request to the URL `/`. When the user selects another file (a `"change"` event on the field), the script must fetch and display that file. When saving a file, use the currently selected filename.