NodeJS basis

What is a NodeJS

JS is a scripting language, and scripting languages require a parser to run. For JS written in HTML pages, the browser acts as a parser. For JS that need to run independently, NodeJS is a parser.

Each parser is a runtime environment that not only allows JS to define various data structures and perform various calculations, but also allows JS to do something with built-in objects and methods provided by the runtime. For example, JS running in a browser is used to manipulate the DOM, and browsers provide built-in objects like Document. The purpose of JS running in NodeJS is to operate disk files or build HTTP servers. NodeJS provides built-in objects such as FS and HTTP accordingly.

With use of

While there are some students who think it’s cool to be able to run JS files directly, the majority of students are primarily interested in what is useful and what value they bring to something new.

The author of NodeJS says that he created NodeJS in order to achieve high performance Web servers, and he prioritized the advantages of the event mechanism and asynchronous IO model over JS. But he needed to choose a programming language to implement his idea, one that didn’t come with IO, and one that supported events well. JS has no built-in IO, is designed to handle DOM events in the browser, and has a large group of programmers, making it a natural choice.

As he hoped, NodeJS became active on the server side, and a number of Web services based on NodeJS emerged. NodeJS, on the other hand, has made the front end so popular that it can finally extend its reach beyond the browser window, and more front-end tools are springing up.

Therefore, for the front end, although not everyone needs to write a server program with NodeJS, it can be as simple as debugging JS code fragments using command interaction mode, and as complex as writing tools to improve productivity.

The NodeJS ecosystem is thriving.

How to install

The installation program

NodeJS provides several installers, which can be downloaded and installed at nodejs.org.

In Windows, select the installation file with the. Msi suffix that matches the system version. In Mac OS X, select the installation file with the suffix. PKG.

Compile the installation

No installer is available for Linux, and while some distributions can be installed using methods like apt-get, they may not be able to install to the latest version. Therefore, NodeJS is generally installed in the following compilation mode on Linux.

  1. Make sure g++ is 4.6 or above and python is 2.6 or above.

  2. Download the latest NodeJS source package with the tar.gz suffix from nodejs.org and unzip it to a location.

  3. Go to the decompressed directory and run the following command to compile and install.

     $ ./configure
     $ make
     $ sudo make installCopy the code

How to run

Open the terminal and type Node to enter the command interactive mode. You can enter a code statement and execute it immediately and display the result, for example:

$ node > console.log('Hello World! '); Hello World!Copy the code

If you want to run a large section of code, you can write a JS file and then run it. For example, there is hello.js.

function hello() { console.log('Hello World! '); } hello();Copy the code

Enter node hello.js in terminal and run. The result is as follows:

$ node hello.js
Hello World!Copy the code

Permission problems

When using NodeJS to listen on port 80 or 443 to provide HTTP(S) services on Linux, root permission is required. There are two ways to do this.

One way is to run NodeJS with the sudo command. For example, server.js, run with the following command, has permission to use ports 80 and 443. This method is recommended to ensure that only necessary JS scripts are provided with root permission.

$ sudo node server.jsCopy the code

The alternative is to use the chmod +s command to make NodeJS always run as root, as follows. Because this method allows any JS script to have root permission, it is not very secure, so it is not recommended for security systems.

$ sudo chown root /usr/local/bin/node
$ sudo chmod +s /usr/local/bin/nodeCopy the code

The module

When writing larger programs, it’s common to modularize your code. In NodeJS, the code is normally split into different JS files, each of which is a module, and the file path is the module name.

Each module is written with three predefined variables: require, exports, and Module.

require

The require function is used to load and use other modules in the current module, passing in a module name and returning a module export object. Module names can be relative paths (starting with./) or absolute paths (starting with a drive letter such as/or C:). In addition, the.js extension in the module name can be omitted. Here’s an example.

var foo1 = require('./foo'); var foo2 = require('./foo.js'); var foo3 = require('/home/user/foo'); var foo4 = require('/home/user/foo.js'); // Foo1 through foo4 are exported objects from the same module.Copy the code

Alternatively, you can load and use a JSON file in the following manner.

var data = require('./data.json');Copy the code

exports

Exports objects are exports of the current module and are used to export module public methods and properties. Other modules that use the current module through the require function get the exports object of the current module. The following example exports a public method.

exports.hello = function () { console.log('Hello World! '); };Copy the code

module

The Module object provides access to some information about the current module, but is mostly used to replace the exported object of the current module. For example, the module export object is a normal object by default. If you want to change it to a function, you can use the following method.

module.exports = function () { console.log('Hello World! '); };Copy the code

In the above code, the module’s default export object is replaced with a function.

Module initialization

The JS code in a module executes only once, the first time the module is used, and initializes the module’s exported object during execution. The cached exported objects are then reused.

The main module

Modules that are passed to NodeJS with command-line arguments to start the program are called master modules. The main module is responsible for scheduling the other modules that make up the entire program to complete their work. For example, main.js is the main module when the program is launched with the following command.

$ node main.jsCopy the code

Complete sample

For example, there are the following directories.

- /home/user/hello/
    - util/
        counter.js
    main.jsCopy the code

The counter. Js content is as follows:

var i = 0;

function count() {
    return ++i;
}

exports.count = count;Copy the code

The module internally defines a private variable I and exports a public method count on exports objects.

The main module main.js contains the following contents:

var counter1 = require('./util/counter');
var    counter2 = require('./util/counter');

console.log(counter1.count());
console.log(counter2.count());
console.log(counter2.count());Copy the code

The results of running the program are as follows:

$ node main.js
1
2
3Copy the code

As you can see, counter.js is not initialized twice because it is required twice.

Binary module

Although we typically write modules in JS, NodeJS also supports writing binary modules in C/C++. Compiled binary modules are used in the same way as JS modules, except that the file extension is.node. Although binary modules can use all the functions provided by the operating system and have unlimited potential, they are too difficult for front-end students to write and are difficult to use across platforms, so they are not covered by this tutorial.

summary

This chapter introduces the basic concepts and usage of NodeJS. The following points are summarized:

  • NodeJS is a JAVASCRIPT script parser. Installing NodeJS on any operating system essentially copies the NodeJS executable to a directory, and then ensures that this directory is under the system PATH environment variable so that the terminal can use node commands.

  • Enter the node command directly from the terminal to enter command interactive mode, which is ideal for testing JS code snippets, such as regular expressions.

  • NodeJS uses a CMD module system, with the main module as the entry point of the program and all modules initialized only once during execution.

  • Don’t use binary modules unless JS modules don’t meet your needs, or your users will cry.

Organization and deployment of code

Experienced C programmers start by writing a make file when writing a new program. Similarly, to get off to a good start with NodeJS, you need to have your code’s directory structure and deployment in place, much like building scaffolding. This chapter will introduce all kinds of knowledge related to it.

Module path resolution rules

We already know that the require function supports absolute paths starting with a slash (/) or drive (C :), as well as relative paths starting with a./. However, these two paths establish a strong coupling relationship between modules. Once the location of a module file needs to be changed, the code of other modules using this module also needs to be adjusted to become a whole system. Therefore, the require function supports a third form of path, written similarly to foo/bar, and parses the path in turn until the module location is found.

  1. Built-in module

    If the require function is passed the NodeJS built-in module name, no path resolution is done and the exported object of the internal module is returned, such as require(‘fs’).

  2. Node_modules directory

    NodeJS defines a special node_modules directory for storing modules. For example, when a module whose absolute path is /home/user/hello.js is loaded with require(‘foo/bar’), NodeJS tries to use the following paths in sequence.

     /home/user/node_modules/foo/bar
     /home/node_modules/foo/bar
     /node_modules/foo/barCopy the code
  3. NODE_PATH Environment variable

    Like the PATH environment variable, NodeJS allows you to specify additional module search paths via the NODE_PATH environment variable. The NODE_PATH environment variable contains one to more directory paths, which are used on Linux: separated, used on Windows; Space. For example, the following NODE_PATH environment variable is defined:

     NODE_PATH=/home/user/lib:/home/libCopy the code

    When a module is loaded using require(‘foo/bar’), NodeJS tries the following paths in turn.

     /home/user/lib/foo/bar
     /home/lib/foo/barCopy the code

Package (package)

We already know that the basic unit of a JS module is a single JS file, but more complex modules tend to consist of multiple submodules. For ease of management and use, we can call a large module consisting of multiple submodules a package and put all the submodules in the same directory.

Among all the submodules that make up a package, there needs to be an entry module whose export objects are used as the export objects of the package. For example, there is the following directory structure.

- /home/user/lib/
    - cat/
        head.js
        body.js
        main.jsCopy the code

The CAT directory defines a package that contains three submodules. As an entry module, main.js has the following contents:

var head = require('./head');
var body = require('./body');

exports.create = function (name) {
    return {
        name: name,
        head: head.create(),
        body: body.create()
    };
};Copy the code

When you use a package in another module, you need to load the package entry module. Following the previous example, require(‘/home/user/lib/cat/main’) would do the trick, but it doesn’t seem like a good idea to have the entry module name in the path. So we need to do a little extra work to make packages work more like individual modules.

index.js

When the file name of a module is index.js, the module can be loaded using the directory of the module instead of the module file path. Therefore, following the example above, the following two statements are equivalent.

var cat = require('/home/user/lib/cat');
var cat = require('/home/user/lib/cat/index');Copy the code

After doing this, you just pass the package directory path to the require function, and it feels like the entire directory is being used as a single module, giving it a more holistic feel.

package.json

If you want to customize the name and location of the entry module, you need to include a package.json file in the package directory and specify the path to the entry module. The CAT module in the above example can be refactored as follows.

- /home/user/lib/
    - cat/
        + doc/
        - lib/
            head.js
            body.js
            main.js
        + tests/
        package.jsonCopy the code

The package.json content is as follows.

{
    "name": "cat",
    "main": "./lib/main.js"
}Copy the code

In this way, the module can also be loaded using require(‘/home/user/lib/cat’). NodeJS will locate the entry module based on package.json in the package directory.

Command line program

Anything written in NodeJS is either a package or a command-line program, and the former will eventually be used to develop the latter. So we need some tricks when we deploy the code to make the user feel like they’re using a command-line program.

For example, we wrote a program with NodeJS that can print command line arguments as they are. The program is very simple, in the main module to achieve all the functions. After that, we deployed the program in /home/user/bin/node-echo.js. To run the program in any directory, we need to use the following terminal command.

$ node /home/user/bin/node-echo.js Hello World
Hello WorldCopy the code

This doesn’t look much like a command line program, the following is what we expect.

$ node-echo Hello WorldCopy the code

Linux

In Linux, JS files can be used as shell scripts to achieve the above objectives. The specific steps are as follows:

  1. In shell scripts, you can use #! Comment to specify the parser used by the current script. So we’ll start by adding the following line comment at the top of the node-echo.js file to indicate that the current script is parsed using NodeJS.

     #! /usr/bin/env nodeCopy the code

    NodeJS ignores #! At the beginning of the JS module. Comment, do not worry that this line of comment is an illegal statement.

  2. We then grant execution permission to the Node-echo.js file using the following command.

     $ chmod +x /home/user/bin/node-echo.jsCopy the code
  3. Finally, in a directory specified in the PATH environment variable, such as /usr/local/bin, we create a soft link file with the same name as the terminal command we want to use:

     $ sudo ln -s /home/user/bin/node-echo.js /usr/local/bin/node-echoCopy the code

After this processing, we can use the Node-echo command in any directory.

Windows

On Windows it is completely different and we rely on the.cmd file to solve the problem. Suppose node-echo.js is stored in the C:\Users\user\bin directory that has been added to the PATH environment variable. Then create a new file named node-echo. CMD in this directory. The content of the file is as follows:

@node "C:\User\user\bin\node-echo.js" %*Copy the code

After this processing, we can use the Node-echo command in any directory.

Project directory

With this knowledge in mind, we can now plan a complete project catalog. To write a command line program, for example, we usually provide both command line mode and API mode, and we use three-party packages to write the code. In addition to code, a complete program should also have its own documentation and test cases. Thus, a standard project catalog would look something like this.

- / home/user/workspace/node - echo / # # project directory - bin/store command line code node - echo + doc / # # documentation - lib/storage API code echo. Js - Node_modules/argv/ + tests/Copy the code

Some of the documents are as follows:

/* bin/node-echo */ var argv = require('argv'), echo = require('.. /lib/echo'); console.log(echo(argv.join(' '))); /* lib/echo.js */ module.exports = function (message) { return message; }; /* package.json */ { "name": "node-echo", "main": "./lib/echo.js" }Copy the code

The above example stores different types of files and loads the module directly using the tripartite package name through the node_moudles directory. In addition, after package.json is defined, the Node-echo directory can also be used as a package.

NPM

NPM is a package management tool installed with NodeJS, which can solve many problems in NodeJS code deployment. The common usage scenarios are as follows:

  • Allows users to download third-party packages written by others from the NPM server for local use.

  • Allows users to download and install command line programs written by others from the NPM server for local use.

  • Allows users to upload their own packages or command-line programs to the NPM server for others to use.

As you can see, NPM creates a NodeJS ecosystem where NodeJS developers and users can interact with each other. The following describes how to use NPM in each of these scenarios.

Download tripartite packages

When you need to use three-party packages, you first need to know what packages are available. Npmjs.org provides a search box by package name, but if you’re unsure of the name of the tripartite package you want to use, do a search. Once you know the package name, such as argv in the previous example, you can open the terminal in the project directory and use the following command to download the three-way package.

$ npm install argv ... [email protected] node_modules \ argvCopy the code

Once downloaded, the argv package is stored in the node_modules directory of the project directory, so you can simply require(‘argv’) in your code without specifying a tripartite package path.

The above command downloads the latest version of the tripartite package by default. If you want to download the specified version of the package, you can add @

after the package name. For example, argv 0.0.1 can be downloaded by using the following command.

$ npm install [email protected]
...
[email protected] node_modules\argvCopy the code

If you use a lot of three – party packages, in the terminal next package a command installation is too human. So NPM extends the package.json field to allow tripartite package dependencies to be declared there. Therefore, the package.json in the previous example could be rewritten as follows:

{" name ":" node - echo ", "main" : ". / lib/echo. Js ", "dependencies" : {" argv ":" hundreds "}}Copy the code

After this process, you can use the NPM install command in the project directory to batch install tripartite packages. More importantly, when Node-Echo is also uploaded to the NPM server in the future, NPM will automatically download further dependent third-party packages according to the third-party dependencies declared in the package. For example, when you run the NPM install node-echo command, NPM automatically creates the following directory structure.

- project/ - node_modules/ - node-echo/ - node_modules/ + argv/ ... .Copy the code

This way, users only need to care about the three-party packages they use directly, rather than having to resolve all the package dependencies themselves.

Install the command line program

Downloading and installing a command line program from the NPM service is similar to a three-way package. For example, node-echo in the preceding example provides the command line. As long as Node-echo configures the package.json field, users only need to use the following command to install the program.

$ npm install node-echo -gCopy the code

-g indicates global installation. Therefore, node-echo is installed in the following location by default, and NPM automatically creates a soft link file for Linux or a. CMD file for Windows.

- /usr/local/ # Linux -lib /node_modules/ + node-echo/... - bin/ node-echo ... . -% APPDATA%\ NPM \ # Windows -node_modules \ + node-echo\... node-echo.cmd ...Copy the code

Post code

You need to register an account before publishing code with NPM for the first time. Run NPM adduser on the terminal and follow the instructions. With the account done, we then need to edit the package.json file to add the required fields for NPM. Following the node-echo example above, the necessary fields in package.json are as follows.

{"name": "node-echo", "node-echo", "node-echo", "node-echo" "Hundreds"}, "main" : ". / lib/echo. Js ", # entry module position "bin" : {" node - echo ":". / bin/node - echo "# command line program name and position of the main module}}Copy the code

After that, we can run the NPM publish publish code in the package.json directory.

The version number

When you download and distribute code using NPM, you are exposed to the version number. NPM uses semantic version numbers to manage code, which are briefly described here.

The semantic version number is X.Y.Z, representing the major version number, minor version number, and patch version number respectively. When code changes, the version number is updated according to the following principles.

+ If it is just a bug fix, you need to update the Z bit. + If it is a new function, but backward compatibility, need to update the Y bit. + If there is a big change, the X bit needs to be updated for downward incompatibility.Copy the code

Version numbers With this guarantee, you can rely on a range of version numbers in addition to a fixed version number when claiming third-party package dependencies. For example, “argv”: “0.0.x” depends on argv, the latest version of the 0.0.x series. All version number ranges supported by NPM can be viewed in official documentation.

A little sense

NPM provides a lot of functionality beyond what is covered in this chapter, and there are many other useful fields in package.json. In addition to the official documentation available at npmjs.org/doc/, here are some common NPM commands.

  • NPM provides many commands, such as install and publish, and you can view all of them using NPM help.

  • Use NPM help to view detailed help for a command, such as NPM help install.

  • Use NPM install. -g in package.json to install the current command line program locally, which can be used for local testing before release.

  • Use NPM update to update the modules in the node_modules subdirectory in the current directory to the latest version.

  • Use NPM update -g to update the corresponding command line program of the global installation to the latest version.

  • Using NPM cache clear clears the NPM local cache for anyone who releases a new version of code with the same version number.

  • Use NPM unpublish @

    to undo a version of your code.

summary

This chapter describes the preparation you need to do before writing code using NodeJS. It summarizes the following points:

  • Plan out the directory structure before writing code to keep things organized.

  • Larger programs can split code into modules to manage, and larger programs can use packages to organize modules.

  • Node_modules and NODE_PATH are used to decouple the package usage mode from the physical path.

  • Use NPM to join the NodeJS ecosystem.

  • Please register on NPM in advance when you think of a desired package name.

File operations

What makes the front-end feel magic is not that NodeJS can do network programming, but that NodeJS can manipulate files. From file lookup to code compilation, there is hardly a front-end tool that doesn’t manipulate files. On the other hand, most front-end tools can be written with almost no more than some data-processing logic, plus some file manipulation. This chapter introduces the NodeJS built-in modules associated with it.

Good start

NodeJS provides basic file manipulation apis, but not advanced functions like file copying, so let’s get started with file copying programs. Like copy, our program needs to be able to accept both source and destination file paths.

Small file copy

We use NodeJS built-in FS module to implement this program simply as follows.

var fs = require('fs');

function copy(src, dst) {
    fs.writeFileSync(dst, fs.readFileSync(src));
}

function main(argv) {
    copy(argv[0], argv[1]);
}

main(process.argv.slice(2));Copy the code

The above program uses fs.readfilesync to read the file contents from the source path and fs.writefilesync to write the file contents to the destination path.

Bean tip: Process is a global variable, and command line arguments are available through process.argv. Since argv[0] is fixed to the absolute path of the NodeJS execution program and argv[1] is fixed to the absolute path of the main module, the first command-line argument starts at argv[2].

Large file Copy

Copying small files is fine, but loading all files into memory and then writing them to disk at once is not a good way to copy large files. Memory will run out. For large files, we can only read and write until we are done copying. Therefore, the above program needs to be modified as follows.

var fs = require('fs');

function copy(src, dst) {
    fs.createReadStream(src).pipe(fs.createWriteStream(dst));
}

function main(argv) {
    copy(argv[0], argv[1]);
}

main(process.argv.slice(2));Copy the code

CreateReadStream creates a read-only stream from the source file and a write only stream from the destination file using fs.createWritestream, and uses the PIPE method to connect the two streams. And what happens when they’re connected, to put it more abstractly, is that the water runs down the pipe from one bucket to the other.

API takes a quick look

Let’s take a look at some of the apis NodeJS provides for file manipulation. I’m not going to go through each and every API here, but the official documentation is pretty good.

Buffer (block of data)

Official document: nodejs.org/api/buffer….

NodeJS has only String data types, not binary data types, so NodeJS provides a global Buffer equivalent to String to operate on binary data. In addition to the real exception of the Buffer that can be read from the file, it can also be constructed directly, for example:

var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]);Copy the code

A Buffer is similar to a string. In addition to obtaining the length of bytes with the. Length attribute, you can also use [index] to read bytes at a specified position, for example:

bin[0]; // => 0x68;Copy the code

Buffers and strings can be converted to each other. For example, binary data can be converted to a string using the specified encoding:

var str = bin.toString('utf-8'); // => "hello"Copy the code

Alternatively, convert the string to binary data in the specified encoding:

var bin = new Buffer('hello', 'utf-8'); // => <Buffer 68 65 6c 6c 6f>Copy the code

Buffers differ from strings in one important way. The string is read-only, and any modification to the string results in a new string, leaving the original string unchanged. As for buffers, they are more like C arrays that can do Pointers. For example, you can modify a byte at a location directly with [index].

bin[0] = 0x48;Copy the code

Instead of returning a new Buffer, the. Slice method is more like returning a pointer to somewhere in the middle of the original Buffer, as shown below.

[ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]
    ^           ^
    |           |
   bin     bin.slice(2)Copy the code

So modifications to the Buffer returned by the.slice method will apply to the original Buffer, for example:

var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]);
var sub = bin.slice(2);

sub[0] = 0x65;
console.log(bin); // => <Buffer 68 65 65 6c 6f>Copy the code

Therefore, if you want to copy a Buffer, you must first create a new Buffer and copy the data from the original Buffer using the. Copy method. This is similar to applying for a new piece of memory and copying the data from the existing memory. Here’s an example.

var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]);
var dup = new Buffer(bin.length);

bin.copy(dup);
dup[0] = 0x48;
console.log(bin); // => <Buffer 68 65 6c 6c 6f>
console.log(dup); // => <Buffer 48 65 65 6c 6f>Copy the code

In short, Buffer extends THE data-processing power of JS from strings to arbitrary binary data.

Stream (data Stream)

Official document: nodejs.org/api/stream….

Data flow is used when memory cannot hold all the data that needs to be processed at once, or when it is more efficient to read and process as you go. NodeJS provides operations on data streams through various streams.

As an example of the large file copy program above, we can create a read-only data stream for the data source as shown in the following example:

var rs = fs.createReadStream(pathname);

rs.on('data', function (chunk) {
    doSomething(chunk);
});

rs.on('end', function () {
    cleanUp();
});Copy the code

Beans: Streams work based on an event mechanism, and all instances of streams inherit from EventEmitter provided by NodeJS.

In the code above, data events are constantly fired, whether the doSomething function can handle them or not. The code can be modified as follows to solve this problem.

var rs = fs.createReadStream(src);

rs.on('data', function (chunk) {
    rs.pause();
    doSomething(chunk, function () {
        rs.resume();
    });
});

rs.on('end', function () {
    cleanUp();
});Copy the code

The above code adds a callback to the doSomething function so that we can pause the data reading before processing it and continue reading it after processing it.

Alternatively, we can create a write-only data stream for the data target as shown in the following example:

var rs = fs.createReadStream(src);
var ws = fs.createWriteStream(dst);

rs.on('data', function (chunk) {
    ws.write(chunk);
});

rs.on('end', function () {
    ws.end();
});Copy the code

When we replace doSomething with writing data to a write-only data stream, the above code looks like a file copy program. However, the above code has the problem mentioned above. If the write speed cannot keep up with the read speed, the cache inside the write data stream will burst. We can use the return value of the.write method to determine whether incoming data has been written to the target or temporarily placed in the cache, and use the drain event to determine when the write-only stream has written to the target and is ready to pass in the next data to be written. So the code can be modified as follows:

var rs = fs.createReadStream(src); var ws = fs.createWriteStream(dst); rs.on('data', function (chunk) { if (ws.write(chunk) === false) { rs.pause(); }}); rs.on('end', function () { ws.end(); }); ws.on('drain', function () { rs.resume(); });Copy the code

The above code realizes the data transfer from read-only data stream to write data stream, and includes explosion-proof warehouse control. Because there are many scenarios for using this, such as the big file copy program above, NodeJS directly provides the.pipe method to do this, which is internally implemented in a similar way to the code above.

File System

Official document: nodejs.org/api/fs.html

NodeJS provides manipulation of files through the fs built-in module. The APIS provided by the FS module can be basically divided into the following three categories:

  • File attributes read and write.

    Fs. stat, fs.chmod, fs.chown, etc.

  • File contents read and write.

    Fs.readfile, fs.readdir, fs.writefile, fs.mkdir, etc.

  • Underlying file operations.

    Fs. open, fs.read, fs.write, fs.close, etc.

NodeJS’s best asynchronous IO model is fully embodied in fs modules, such as the apis mentioned above, which pass results through callback functions. Take fs.readfile as an example:

fs.readFile(pathname, function (err, data) {
    if (err) {
        // Deal with error.
    } else {
        // Deal with data.
    }
});Copy the code

As the code above shows, almost all FS module apis have two callback parameters. The first argument is equal to the exception object if an error occurs, and the second argument is always used to return the result of the API method execution.

In addition, all asynchronous apis of the FS module have synchronous versions for cases where asynchronous operations are not available or when synchronous operations are more convenient. In addition to the addition of Sync at the end of the method name, the synchronization API also changes the way exception objects and execution results are passed. Fs.readfilesync is also used as an example:

try {
    var data = fs.readFileSync(pathname);
    // Deal with data.
} catch (err) {
    // Deal with error.
}Copy the code

Fs module provides a lot of API, here is not an introduction, please refer to the official documentation when necessary.

Path (Path)

Official documentation: nodejs.org/api/path.ht…

When you operate a file, you cannot avoid dealing with the file path. NodeJS provides a built-in path module to simplify path-related operations and improve code readability. Here are a few commonly used apis.

  • path.normalize

    Convert the passed path to the standard path, specifically, except in the parse path. With.. In addition, it can also get rid of redundant slashes. If a program needs to use a path as an index for some data, but allows users to enter arbitrary paths, this method is required to ensure the uniqueness of paths. Here’s an example:

    var cache = {}; function store(key, value) { cache[path.normalize(key)] = value; } store('foo/bar', 1); store('foo//baz//.. /bar', 2); console.log(cache); // => { "foo/bar": 2 }Copy the code

    Note: The slash in the normalized path is \ on Windows and/on Linux. Replace (/\\/g, ‘/’) with the standard path if you want to ensure that all systems use/as the path separator.

  • path.join

    Concatenate multiple paths passed into a standard path. This method can avoid manual concatenation of path strings, and can correctly use the corresponding path separator in different systems. Here’s an example:

    path.join('foo/', 'baz/', '.. /bar'); // => "foo/bar"Copy the code
  • path.extname

    This is useful when we need to do different things with different file extensions. Here’s an example:

      path.extname('foo/bar.js'); // => ".js"Copy the code

The rest of the methods provided by the PATH module are also limited, and a quick look at the official documentation will tell you all about them.

Directory traversal

Traversing a directory is a common requirement when working with files. For example, to write a program that needs to find and process all JS files in a specified directory, you need to traverse the entire directory.

A recursive algorithm

Recursive algorithms are generally used when traversing directories, otherwise it is difficult to write clean code. Recursive algorithms, similar to mathematical induction, solve problems by reducing the size of the problem. The following example illustrates this approach.

function factorial(n) { if (n === 1) { return 1; } else { return n * factorial(n - 1); }}Copy the code

The top function is used to compute N factorial. . As you can see, when N is greater than 1, the problem simplifies to calculating N times N minus 1 factorial. When N is equal to 1, the problem reaches its minimum size and no further simplification is required, so it simply returns 1.

The pitfall: Code written using recursive algorithms is concise, but since each recursion produces a function call, you need to convert the recursive algorithm to a circular algorithm to reduce the number of function calls when performance needs to be prioritised.

Through the calendar calculation method

Directory is a tree structure, and depth-first + first-order traversal algorithm is generally used in traversal. Depth-first, which means that when a node is reached, child nodes are first traversed instead of neighbor nodes. Sequential traversal means that the traversal is complete when a node is reached for the first time, rather than when a node is returned for the last time. Therefore, the following tree is traversed in the order A > B > D > E > C > F.

          A
         / \
        B   C
       / \   \
      D   E   FCopy the code

Synchronization traverse

With the necessary algorithms in mind, we can simply implement the following directory traversal functions.

function travel(dir, callback) { fs.readdirSync(dir).forEach(function (file) { var pathname = path.join(dir, file); if (fs.statSync(pathname).isDirectory()) { travel(pathname, callback); } else { callback(pathname); }}); }Copy the code

As you can see, the function starts with a directory. When it encounters a subdirectory, it first iterates through the subdirectory. When it encounters a file, it passes the absolute path to the file to the callback function. Once the callback gets the file path, it can make all sorts of judgments and processes. So assume the following directories:

- /home/user/
    - foo/
        x.js
    - bar/
        y.js
    z.cssCopy the code

When you traverse the directory with the following code, you get the following input.

travel('/home/user', function (pathname) {
    console.log(pathname);
});

------------------------
/home/user/foo/x.js
/home/user/bar/y.js
/home/user/z.cssCopy the code

Asynchronous traversal

The directory traversal function is a bit more complicated to implement if you are reading directories or reading file state using an asynchronous API, but the principle is the same. The asynchronous version of the travel function is as follows.

function travel(dir, callback, finish) { fs.readdir(dir, function (err, files) { (function next(i) { if (i < files.length) { var pathname = path.join(dir, files[i]); fs.stat(pathname, function (err, stats) { if (stats.isDirectory()) { travel(pathname, callback, function () { next(i + 1); }); } else { callback(pathname, function () { next(i + 1); }); }}); } else { finish && finish(); }} (0)); }); }Copy the code

The technique of writing asynchronous traversal functions is not covered here, but will be covered in a later section. All in all, we can see that asynchronous programming is quite complex.

Text encoding

When using NodeJS to write front-end tools, most of the operations are text files, so file encoding is involved. Commonly used text encodings are UTF8 and GBK, and UTF8 files may also have BOM. When reading text files with different encoding, the file content needs to be converted to UTF8 encoding string used by JS before normal processing.

BOM to remove

BOM is used to mark a text file as Unicode encoded, and is itself a Unicode character (“\uFEFF”) at the head of the text file. Under different Unicode encodings, BOM characters correspond to the following binary bytes:

    Bytes      Encoding
----------------------------
    FE FF       UTF16BE
    FF FE       UTF16LE
    EF BB BF    UTF8Copy the code

Therefore, we can determine whether a text file contains a BOM and which Unicode encoding to use based on what the first few bytes of a text file are equal to. However, the BOM character, while serving as a marker for the encoding of the file, is not itself part of the content of the file, and it can be problematic in some usage scenarios if the BOM is not removed when reading a text file. For example, if several JS files are combined into one file, the BOM character in the middle of the file will cause the browser JS syntax error. Therefore, when using NodeJS to read text files, the BOM is usually removed. For example, the following code implements the ability to identify and remove UTF8 BOM.

function readText(pathname) {
    var bin = fs.readFileSync(pathname);

    if (bin[0] === 0xEF && bin[1] === 0xBB && bin[2] === 0xBF) {
        bin = bin.slice(3);
    }

    return bin.toString('utf-8');
}Copy the code

GBK turn UTF8

NodeJS supports specifying text encoding when reading text files or when Buffer is converted to a string, but unfortunately GBK encoding is not supported by NodeJS itself. Therefore, we usually use iconV-Lite, a three-party package, to convert the encoding. After downloading the package using NPM, we can write a function that reads the GBK text file as follows.

var iconv = require('iconv-lite');

function readGBKText(pathname) {
    var bin = fs.readFileSync(pathname);

    return iconv.decode(bin, 'gbk');
}Copy the code

Single-byte encoding

Sometimes, we can’t predict which encoding will be used for the file we want to read, so we can’t specify the correct encoding. For example, some CSS files we will deal with are encoded in GBK or UTF8. While it is possible to guess the text encoding to some extent from the byte content of a file, this is a much simpler technique that is somewhat limited.

First of all, if a text file contains only English characters, such as Hello World, it can be read with either GBK or UTF8 encoding. This is because characters in the ASCII0 to 128 range use the same single-byte encoding under these encodings.

On the other hand, even if a text file contains characters such as Chinese, if we need to deal with only characters in the ASCII0 to 128 range, such as JS code other than comments and strings, we can uniformly use single-byte encoding to read the file, regardless of whether the actual encoding is GBK or UTF8. The following example illustrates this approach.

Var foo = 'Chinese '; 2. Bytes: 76 61 72 20 66 6F 6F 20 3D 20 27 D6 D0 CE C4 27 3B 3. Var foo = '{garbled}{garbled}{garbled}{garbled}{garbled}'; Var bar = '{garbled}{garbled}{garbled}{garbled}'; 5. Use single-byte encoding to save the corresponding bytes: 76 61 72 20 62 61 72 20 3D 20 27 D6 D0 CE C4 27 3B 6. Var bar = 'Chinese ';Copy the code

The trick here is that no matter what garbled characters a single byte larger than 0xEF is parsed into in the single-byte encoding, the corresponding bytes behind the garbled characters remain the same when they are saved using the same single-byte encoding.

NodeJS has a binary code that can be used to implement this method, so in the following example, we will use this code to demonstrate how to write the corresponding code in the above example.

function replace(pathname) {
    var str = fs.readFileSync(pathname, 'binary');
    str = str.replace('foo', 'bar');
    fs.writeFileSync(pathname, str, 'binary');
}Copy the code

summary

This chapter introduces the apis and techniques needed to manipulate files using NodeJS. It summarizes the following points:

  • Learn file operation, write a variety of procedures are not afraid.

  • If you don’t care much about performance, the FS module’s synchronization API makes life better.

  • Use the fs module’s low-level file manipulation API when you need fine-grained control over file reads and writes at the byte level.

  • Instead of using concatenated strings for paths, use the path module.

  • Good command of directory traversal and file coding processing skills, very practical.

Network operating

Programmers who don’t know network programming don’t make a good front end, and NodeJS provides a window into that. Through NodeJS, in addition to writing some server-side programs to assist front-end development and testing, you can also learn some related knowledge of HTTP protocol and Socket protocol, which may come in handy when optimizing front-end performance and troubleshooting front-end faults. This chapter introduces the NodeJS built-in modules associated with it.

Good start

NodeJS was originally intended to write high-performance Web servers. We’ll start with a simple HTTP server implementation using the HTTP module built into NodeJS by repeating the example from the official documentation.

var http = require('http');

http.createServer(function (request, response) {
    response.writeHead(200, { 'Content-Type': 'text-plain' });
    response.end('Hello World\n');
}).listen(8124);Copy the code

The program creates an HTTP server and listens on port 8124. Open a browser and visit http://127.0.0.1:8124/ to see the effect.

Bean knowledge: In Linux, you need root permission to listen to ports less than 1024. Therefore, if you want to listen on port 80 or 443, you need to start the program with the sudo command.

API takes a quick look

Let’s take a look at some of the apis NodeJS provides for network operations. I’m not going to go through each and every API here, but the official documentation is pretty good.

HTTP

Official documentation: nodejs.org/api/http.ht…

The ‘HTTP’ module can be used in two ways:

  • When used as a server, an HTTP server is created that listens for HTTP client requests and returns a response.

  • When used as a client, an HTTP client request is made and a server response is obtained.

First let’s look at how it works in server mode. As shown in the example at the start, you first need to create a server using the.createserver method and then call the.Listen method to listen on the port. Later, the callback function passed in when the server was created is called each time a client request comes in. As you can see, this is an event mechanism.

An HTTP request is essentially a data stream consisting of headers and a body. For example, here is a complete HTTP request data content.

POST/HTTP/1.1 user-agent: curl/7.26.0 Host: localhost Accept: */* Content-length: 11 content-type: application/x-www-form-urlencoded Hello WorldCopy the code

As you can see, above the blank line is the request header and below is the request body. HTTP requests, when sent to the server, can be thought of as being streamed byte by byte in a start-to-end order. The HTTP server created by the HTTP module invokes the callback function when it receives the complete request header. In addition to using the Request object to access the request header data, the request object can also be used as a read-only data stream to access the request body data. Here’s an example.

http.createServer(function (request, response) { var body = []; console.log(request.method); console.log(request.headers); request.on('data', function (chunk) { body.push(chunk); }); request.on('end', function () { body = Buffer.concat(body); console.log(body.toString()); }); }).listen(80); -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- the POST {' the user-agent ':' curl / 7.26.0, host: 'localhost', accept: '*/*', 'content-length': '11', 'content-type': 'application/x-www-form-urlencoded' } Hello WorldCopy the code

An HTTP response is essentially a data flow, again consisting of headers and a body. For example, here is a complete HTTP request data content.

HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 11
Date: Tue, 05 Nov 2013 05:31:38 GMT
Connection: keep-alive

Hello WorldCopy the code

In the callback function, in addition to using the response object to write response header data, the response object can also be used as a write-only data stream to write response body data. For example, in the following example, the server returns the request body data requested by the client to the client as is.

http.createServer(function (request, response) {
    response.writeHead(200, { 'Content-Type': 'text/plain' });

    request.on('data', function (chunk) {
        response.write(chunk);
    });

    request.on('end', function () {
        response.end();
    });
}).listen(80);Copy the code

Let’s look at how this works in client mode. To make a client HTTP request, we need to specify the location of the target server and send the request header and body, as illustrated in the following example.

var options = {
        hostname: 'www.example.com',
        port: 80,
        path: '/upload',
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
        }
    };

var request = http.request(options, function (response) {});

request.write('Hello World');
request.end();Copy the code

As you can see, the.request method creates a client and specifies the request target and request header data. The Request object can then be treated as a write-only data stream to write request body data and terminate the request. In addition, because GET requests are the most common of HTTP requests and do not require a request body, the HTTP module also provides the following convenient apis.

http.get('http://www.example.com/', function (response) {});Copy the code

The callback function is invoked when the client sends the request and receives the complete server response header. In the callback function, in addition to using the Response object to access the response header data, you can also access the response body data as a read-only data stream. Here’s an example.

http.get('http://www.example.com/', function (response) { var body = []; console.log(response.statusCode); console.log(response.headers); response.on('data', function (chunk) { body.push(chunk); }); response.on('end', function () { body = Buffer.concat(body); console.log(body.toString()); }); }); ------------------------------------ 200 { 'content-type': 'text/html', server: 'Apache', 'content-length': '801', date: 'Tue, 05 Nov 2013 06:08:41 GMT', connection: 'keep-alive' } <! DOCTYPE html> ...Copy the code

HTTPS

Official documentation: nodejs.org/api/https.h…

The HTTPS module is similar to the HTTP module except that the HTTPS module requires additional processing of SSL certificates.

The following is an example of creating an HTTPS server in server mode.

var options = {
        key: fs.readFileSync('./ssl/default.key'),
        cert: fs.readFileSync('./ssl/default.cer')
    };

var server = https.createServer(options, function (request, response) {
        // ...
    });Copy the code

As you can see, instead of creating an HTTP server, there is an options object that specifies the private and public keys used by the HTTPS server through the key and cert fields.

In addition, NodeJS supports SNI technology and can dynamically use different certificates based on the domain names requested by HTTPS clients. Therefore, the same HTTPS server can use multiple domain names to provide services. Following the previous example, you can add multiple sets of certificates to an HTTPS server using the following method.

server.addContext('foo.com', {
    key: fs.readFileSync('./ssl/foo.com.key'),
    cert: fs.readFileSync('./ssl/foo.com.cer')
});

server.addContext('bar.com', {
    key: fs.readFileSync('./ssl/bar.com.key'),
    cert: fs.readFileSync('./ssl/bar.com.cer')
});Copy the code

In client mode, making an HTTPS client request is almost identical to an HTTP module, as shown in the following example.

var options = {
        hostname: 'www.example.com',
        port: 443,
        path: '/',
        method: 'GET'
    };

var request = https.request(options, function (response) {});

request.end();Copy the code

However, if the SSL certificate used by the target server is homemade and not purchased from an authority, by default the HTTPS module will reject the connection, indicating a certificate security problem. Add the rejectUnauthorized: false field to options to disable the certificate validity check and allow the HTTPS module to request the HTTPS server that uses the self-made certificate in the development environment.

URL

Official documentation: nodejs.org/api/url.htm…

The URL module is highly used when processing HTTP requests because it allows url parsing, GENERATION, and concatenation. First let’s look at the components of a complete URL.

href ----------------------------------------------------------------- host path --------------- ---------------------------- http: // user:pass @ host.com : 8080 /p/a/t/h ?query=string #hash ----- --------- -------- ---- -------- ------------- ----- protocol auth hostname port  pathname search hash ------------ queryCopy the code

We can convert a URL string to a URL object using the.parse method, as shown in the following example.

url.parse('http://user:[email protected]:8080/p/a/t/h?query=string#hash');
/* =>
{ protocol: 'http:',
  auth: 'user:pass',
  host: 'host.com:8080',
  port: '8080',
  hostname: 'host.com',
  hash: '#hash',
  search: '?query=string',
  query: 'query=string',
  pathname: '/p/a/t/h',
  path: '/p/a/t/h?query=string',
  href: 'http://user:[email protected]:8080/p/a/t/h?query=string#hash' }
*/Copy the code

The URL passed to the. Parse method does not have to be a complete URL. For example, in HTTP server callbacks, Request. URL does not contain a protocol header or domain name, but it can also be parsed using the. Parse method.

http.createServer(function (request, response) { var tmp = request.url; // => "/foo/bar? a=b" url.parse(tmp); /* => { protocol: null, slashes: null, auth: null, host: null, port: null, hostname: null, hash: null, search: '? a=b', query: 'a=b', pathname: '/foo/bar', path: '/foo/bar? a=b', href: '/foo/bar? a=b' } */ }).listen(80);Copy the code

The. Parse method also supports the second and third Boolean type optional arguments. When the second argument is true, the method returns a URL object in which the Query field is no longer a string, but a parameter object converted by the QueryString module. The third parameter is equal to the true, the method can correct parsing urls without agreement head, for example, / / www.example.com/foo/bar.

In turn, the format method allows you to convert a URL object to a URL string, as shown in the following example.

url.format({
    protocol: 'http:',
    host: 'www.example.com',
    pathname: '/p/a/t/h',
    search: 'query=string'
});
/* =>
'http://www.example.com/p/a/t/h?query=string'
*/Copy the code

Alternatively, the.resolve method can be used to concatenate urls, as shown in the following example.

url.resolve('http://www.example.com/foo/bar', '.. /baz'); /* => http://www.example.com/baz */Copy the code

Query String

Official documentation: nodejs.org/api/queryst…

The QueryString module is used to convert URL parameter strings to and from parameter objects, as shown in the following example.

querystring.parse('foo=bar&baz=qux&baz=quux&corge');
/* =>
{ foo: 'bar', baz: ['qux', 'quux'], corge: '' }
*/

querystring.stringify({ foo: 'bar', baz: ['qux', 'quux'], corge: '' });
/* =>
'foo=bar&baz=qux&baz=quux&corge='
*/Copy the code

Zlib

Official documentation: nodejs.org/api/zlib.ht…

The zlib module provides data compression and decompression functions. We may need this module when dealing with HTTP requests and responses.

First let’s look at an example of using the Zlib module to compress HTTP response body data. In this example, we determine whether the client supports GZIP and, if so, use the Zlib module to return the body data after Gzip.

http.createServer(function (request, response) { var i = 1024, data = ''; while (i--) { data += '.'; } if ((request.headers['accept-encoding'] || '').indexOf('gzip') ! == -1) { zlib.gzip(data, function (err, data) { response.writeHead(200, { 'Content-Type': 'text/plain', 'Content-Encoding': 'gzip' }); response.end(data); }); } else { response.writeHead(200, { 'Content-Type': 'text/plain' }); response.end(data); } }).listen(80);Copy the code

Next, let’s look at an example of extracting HTTP response body data using the Zlib module. In this example, the server response is determined to be gzip compressed and the zlib module is used to decompress the response body data in the compressed case.

var options = { hostname: 'www.example.com', port: 80, path: '/', method: 'GET', headers: { 'Accept-Encoding': 'gzip, deflate' } }; http.request(options, function (response) { var body = []; response.on('data', function (chunk) { body.push(chunk); }); response.on('end', function () { body = Buffer.concat(body); if (response.headers['content-encoding'] === 'gzip') { zlib.gunzip(body, function (err, data) { console.log(data.toString()); }); } else { console.log(data.toString()); }}); }).end();Copy the code

Net

Official documentation: nodejs.org/api/net.htm…

Net module can be used to create Socket server or Socket client. Since Socket is not widely used in the front-end domain, we will not introduce WebSocket here, but simply demonstrate how to implement HTTP requests and responses from the Socket level.

First let’s look at an example of using sockets to build a sloppy HTTP server. The HTTP server consistently returns the same response no matter what request it receives.

Net.createserver (function (conn) {conn.on('data', function (data) {conn.write(['HTTP/1.1 200 OK', 'content-type: text/plain', 'Content-Length: 11', '', 'Hello World' ].join('\n')); }); }).listen(80);Copy the code

Next, let’s look at an example of making an HTTP client request using a Socket. In this example, the Socket client sends an HTTP GET request after establishing a connection and uses the data event listener function to GET the server response.

var options = { port: 80, host: 'www.example.com' }; Var client = net.connect(options, function () {client.write(['GET/HTTP/1.1', 'user-agent: curl/7.26.0', 'Host: www.baidu.com', 'Accept: */*', '', '' ].join('\n')); }); client.on('data', function (data) { console.log(data.toString()); client.end(); });Copy the code

A little sense

Working with the web using NodeJS, especially with HTTP requests and responses, can come as a surprise. Here are some answers to common questions.

  • Q: Why are HTTP request or response header fields accessed through headers objects not humped?

    A: By specification, both HTTP request header and response header fields should be humped. Unfortunately, not every HTTP server or client program follows the specification strictly, so NodeJS converts header fields received from other clients or servers to lowercase letters so that developers can access header fields in a uniform way. Such as headers’ content – length.

  • Q: Why does the HTTP server created by the HTTP module return a chunked transport response?

    A: By default, the. Write method is allowed to write arbitrary length of response body data after using the. WriteHead method, and the. End method is allowed to end a response. NodeJS automatically adds a transfer-Encoding: chunked field to the response header because the length of the response body data is uncertain, and uses chunked Transfer mode. However, when the Length of the response body data is determined, the. WriteHead method can be used to add a Content-Length field to the response header, so NodeJS does not automatically add the Transfer-Encoding field and use chunked transport.

  • Q: Why does the SOCKET hang up error sometimes occur when THE HTTP module is used to initiate HTTP client requests?

    A: Create a client before sending an HTTP request to a client. The HTTP module provides a global client http.globalAgent that lets you use the.request or.get methods without having to manually create a client. However, the global client allows only five concurrent Socket connections by default. When the number of HTTP client requests exceeds this number, a Socket hang up error occurs. The solution is simple, via HTTP. GlobalAgent. MaxSockets properties change my this number can be larger. In addition, the same to HTTPS module meet the problem via HTTPS. GlobalAgent. MaxSockets attributes to deal with.

summary

This chapter introduces the apis and some pit avoidance techniques needed to operate the network using NodeJS, which are summarized as follows:

  • The HTTP and HTTPS modules support server mode and client mode.

  • In addition to reading and writing header data, both request and Response objects can be manipulated as data streams.

  • The url.parse method plus the request.url attribute is a fixture when handling HTTP requests.

  • Using the Zlib module reduces the amount of data transferred over HTTP.

  • Net module through the Socket server and the client can do the underlying HTTP protocol operations.

  • Tread carefully in the pit.

Process management

NodeJS can sense and control the running environment and state of its own processes, as well as create and work with child processes. This allows NodeJS to combine multiple programs to perform a task together, acting as glue and scheduler. In addition to the NodeJS built-in modules associated with it, this chapter focuses on typical usage scenarios.

Good start

We already know that NodeJS’s fs module is fairly basic and that copying all files and subdirectories from one directory to another requires quite a bit of code. A cp -r source/* target command can be used to copy directories. To simplify directory copying, use NodeJS to call terminal commands.

var child_process = require('child_process');
var util = require('util');

function copy(source, target, callback) {
    child_process.exec(
        util.format('cp -r %s/* %s', source, target), callback);
}

copy('a', 'b', function (err) {
    // ...
});Copy the code

As you can see from the above code, the child process runs asynchronously and returns the result of execution via a callback function.

API takes a quick look

Let’s take a look at some of the process management apis NodeJS provides. I’m not going to go through each and every API here, but the official documentation is pretty good.

Process

Official documentation: nodejs.org/api/process…

Any process has command line parameters to start the process, standard input standard output, run permissions, run environment, and run status. In NodeJS, all aspects of the NodeJS process can be sensed and controlled through the Process object. It is also important to note that Process is not a built-in module, but a global object, so it can be used directly anywhere.

Child Process

Official documentation: nodejs.org/api/child_p…

Child processes can be created and controlled using the child_process module. Spawn is the core of the API, and the rest of the API is a further encapsulation of it for specific usage scenarios, which is a kind of syntactic sugar.

Cluster

Official documentation: nodejs.org/api/cluster…

The Cluster module is a further encapsulation of the Child_process module and is dedicated to solving the problem that single-process NodeJS Web servers cannot take full advantage of multi-core cpus. Using this module can simplify the development of multi-process server programs, have a worker process running on each core, and listen for ports and distribute requests through the main process.

Application scenarios

It is boring to introduce apis related to process management alone. Therefore, this section describes how to use some important apis based on some typical application scenarios.

How do I get command line arguments

Command line arguments can be obtained from process.argv in NodeJS. Surprisingly, the node execution path and main module file path are fixed to argv[0] and argv[1], and the first command-line argument starts at argv[2]. To make argV more natural to use, you can do the following.

function main(argv) {
    // ...
}

main(process.argv.slice(2));Copy the code

How to Exit the program

Usually a program does all of its work and then exits normally, with an exit status code of 0. Or when a program runs, an exception occurs and it dies. In this case, the exit status code of the program is not equal to 0. If we catch an exception in our code, but feel that the program should not continue and need to exit immediately, and need to set the exit status code to the specified number, such as 1, we can do as follows:

try {
    // ...
} catch (err) {
    // ...
    process.exit(1);
}Copy the code

How to control the input and output

The standard input stream (stdin), standard output stream (stdout), and standard error stream (stderr) of the NodeJS program correspond to process.stdin, process.stdout, and process.stderr respectively. The latter two write data streams only, and operate on them in the same way as on data streams. For example, console.log can be implemented as follows.

function log() {
    process.stdout.write(
        util.format.apply(util, arguments) + '\n');
}Copy the code

How to drop weight

On Linux, we know that you need root permission to listen on ports below 1024. However, once port listening is complete, it is a security risk to continue to run the program under root permission, so it is best to reduce the permission. Here’s an example.

http.createServer(callback).listen(80, function () {
    var env = process.env,
        uid = parseInt(env['SUDO_UID'] || process.getuid(), 10),
        gid = parseInt(env['SUDO_GID'] || process.getgid(), 10);

    process.setgid(gid);
    process.setuid(uid);
});Copy the code

There are a few points to note in the above example:

  1. If the root permission is obtained through sudo, the UID and GID of the user running the program are stored in the environment variables SUDO_UID and SUDO_GID. If the root permission is obtained by chmod +s, the UID and GID of the user running the program can be obtained directly by using the process.getuid and process.getgid methods.

  2. The process.setuid and process.setgid methods only accept arguments of type number.

  3. The GID must be lowered first and then the UID must be lowered; otherwise, the GID cannot be changed.

How do I create a child process

Here is an example of creating a NodeJS child process.

var child = child_process.spawn('node', [ 'xxx.js' ]);

child.stdout.on('data', function (data) {
    console.log('stdout: ' + data);
});

child.stderr.on('data', function (data) {
    console.log('stderr: ' + data);
});

child.on('close', function (code) {
    console.log('child process exited with code ' + code);
});Copy the code

The spawn(exec, args, options) method is used in the above example, which takes three arguments. The first argument is the PATH to the execution file, which can be a relative or absolute PATH to the execution file, or the name of the execution file that can be found based on the PATH environment variable. In the second argument, each member of the array corresponds to a command line argument in order. The third optional parameter configures the execution environment and behavior of the child process.

In addition, although the output of the child process is accessed through the.stdout and.stderr of the child process object, different configurations of the options.stdio field can redirect the input and output of the child process to any data stream or share the standard input and output stream of the parent process. Or simply ignore the input and output of the child process.

How do processes communicate with each other

In Linux, processes can communicate with each other via signals. Here’s an example.

/* parent.js */
var child = child_process.spawn('node', [ 'child.js' ]);

child.kill('SIGTERM');

/* child.js */
process.on('SIGTERM', function () {
    cleanUp();
    process.exit(0);
});Copy the code

In the example above, the parent sends SIGTERM signals to the child process via the.kill method, and the child process listens for SIGTERM event responses from the Process object. Don’t be fooled by the name of the.kill method, which essentially sends a signal to a process, and what the process does when it receives a signal depends entirely on the type of signal and the process’s own code.

In addition, if both parent and child processes are NodeJS processes, data can be passed in both directions via IPC (inter-process communication). Here’s an example.

/* parent.js */
var child = child_process.spawn('node', [ 'child.js' ], {
        stdio: [ 0, 1, 2, 'ipc' ]
    });

child.on('message', function (msg) {
    console.log(msg);
});

child.send({ hello: 'hello' });

/* child.js */
process.on('message', function (msg) {
    msg.hello = msg.hello.toUpperCase();
    process.send(msg);
});Copy the code

As you can see, when the parent process creates the child process, it opens an IPC channel in the options.stdio field via IPC. After that, it can listen for the message event of the child process object to receive messages from the child process and send messages to the child process through the.send method. On the child side, you can listen for message events on the Process object to receive messages from the parent and send messages to the parent using the.send method. The data is serialized at the sending end using json.stringify and deserialized at the receiving end using json.parse.

How do I daemon child processes

The daemon is used to monitor the running status of a working process and restart it if it exits abnormally to ensure that the working process runs continuously. Here’s one way to do it.

/* daemon.js */
function spawn(mainModule) {
    var worker = child_process.spawn('node', [ mainModule ]);

    worker.on('exit', function (code) {
        if (code !== 0) {
            spawn(mainModule);
        }
    });
}

spawn('worker.js');Copy the code

As you can see, the daemon immediately restarts the worker process when it exits abnormally.

summary

This chapter describes the apis required for NodeJS process management and the main application scenarios. The following points are summarized:

  • Manage itself using process objects.

  • Create and manage child processes using the child_process module.

Asynchronous programming

NodeJS’s biggest selling points — the event mechanism and asynchronous IO — are not transparent to developers. Developers need to code asynchronously to take advantage of this, which has been criticized by some NodeJS detractors. However, asynchronous programming is the best thing about NodeJS, and you can’t really learn NodeJS without learning asynchronous programming. This chapter introduces various aspects of asynchronous programming.

The callback

In code, the immediate embodiment of asynchronous programming is callback. Asynchronous programming relies on callbacks, but you can’t say that callbacks make your program asynchronous. We can start by looking at the following code.

function heavyCompute(n, callback) {
    var count = 0,
        i, j;

    for (i = n; i > 0; --i) {
        for (j = n; j > 0; --j) {
            count += 1;
        }
    }

    callback(count);
}

heavyCompute(10000, function (count) {
    console.log(count);
});

console.log('hello');

-- Console ------------------------------
100000000
helloCopy the code

As you can see, the callback function in the above code still executes before the subsequent code. JS itself is a single thread running, it is not possible to run a code is not finished running other code, so there is no concept of asynchronous execution.

However, if what a function does is to create another thread or process, do something in parallel with the JS main thread, and notify the JS main thread when it’s done, that’s a different story. Let’s take a look at the following code.

setTimeout(function () {
    console.log('world');
}, 1000);

console.log('hello');

-- Console ------------------------------
hello
worldCopy the code

This time you can see that the callback function is executed after the subsequent code. As above said, JS itself is single-threaded, cannot be executed asynchronously, so we can think setTimeout to this kind of JS code provided by the operating environment of the special function of do is create a parallel threads returns immediately after, let JS main process can then perform subsequent code, and after receiving notification of parallel process to invoke the callback function. In addition to the usual setTimeout and setInterval functions, NodeJS provides asynchronous apis such as fs.readfile.

In addition, we still come back to the fact that JS is run single-threaded, which means that no other code, including callback functions, can be executed until one piece of code has been executed. That is, even if the parallel thread completes its work and tells the JS main thread to execute the callback function, the callback function will not start executing until the JS main thread is idle. Here’s an example.

function heavyCompute(n) {
    var count = 0,
        i, j;

    for (i = n; i > 0; --i) {
        for (j = n; j > 0; --j) {
            count += 1;
        }
    }
}

var t = new Date();

setTimeout(function () {
    console.log(new Date() - t);
}, 1000);

heavyCompute(50000);

-- Console ------------------------------
8520Copy the code

As you can see, the callback that should have been called after 1 second was delayed significantly because the main JS thread was busy running other code.

Code design pattern

Asynchronous programming has many unique code design patterns, and code written synchronous and asynchronous to achieve the same functionality can be quite different. Here are some common patterns.

Function return value

It is a common requirement to use the output of one function as the input of another, and in synchronous mode code is typically written as follows:

var output = fn1(fn2('input'));
// Do something.Copy the code

In asynchronous mode, since the result of function execution is not passed through the return value, but through the callback function, code is generally written as follows:

fn2('input', function (output2) {
    fn1(output2, function (output1) {
        // Do something.
    });
});Copy the code

As you can see, it’s easy to write code with a > shape if you have too many callback functions inside of one callback function.

Through the array

It is also a common requirement to do some processing with the data members in turn using a function when iterating through a set of numbers. If the function is executed synchronously, the following code is typically written:

var len = arr.length,
    i = 0;

for (; i < len; ++i) {
    arr[i] = sync(arr[i]);
}

// All array items have processed.Copy the code

If the function is executed asynchronously, the above code does not guarantee that all array members are processed after the loop ends. If the array members must be processed sequentially, asynchronous code is generally written as follows:

(function next(i, len, callback) {
    if (i < len) {
        async(arr[i], function (value) {
            arr[i] = value;
            next(i + 1, len, callback);
        });
    } else {
        callback();
    }
}(0, arr.length, function () {
    // All array items have processed.
}));Copy the code

As you can see, the above code does not pass in the next array member until the asynchronous function executes once and returns the result. It does not pass in the next array member until all the array members are processed and the subsequent code is triggered by a callback.

If the array members can be processed in parallel, but the subsequent code still needs to complete all the array members before executing, the asynchronous code adjusts to the following form:

(function (i, len, count, callback) { for (; i < len; ++i) { (function (i) { async(arr[i], function (value) { arr[i] = value; if (++count === len) { callback(); }}); }(i)); } }(0, arr.length, 0, function () { // All array items have processed. }));Copy the code

As you can see, compared to the asynchronous serial iteration, this code processes all the array members in parallel and uses the counter variable to determine when all the array members have been processed.

Exception handling

The exception catching and handling mechanism provided by JS itself — try.. catch.. Can only be used to synchronize executing code. Here’s an example.

function sync(fn) {
    return fn();
}

try {
    sync(null);
    // Do something.
} catch (err) {
    console.log('Error: %s', err.message);
}

-- Console ------------------------------
Error: object is not a functionCopy the code

As you can see, the exception bubbles along the code execution path until it is caught when the first try statement is encountered. However, because asynchronous functions interrupt the code execution path, exceptions generated during and after the execution of asynchronous functions bubble up to the point where the execution path is interrupted. If no try statement is encountered, they are thrown as a global exception. Here’s an example.

Function async(fn, callback) {// Code execution path breaks here.setTimeout (function (){callback(fn())); }, 0); } try { async(null, function (data) { // Do something. }); } catch (err) { console.log('Error: %s', err.message); } -- Console ------------------------------ /home/user/test.js:4 callback(fn()); ^ TypeError: object is not a function at null._onTimeout (/home/user/test.js:4:13) at Timer.listOnTimeout [as ontimeout] (timers.js:110:15)Copy the code

Because the code execution path is interrupted, we need to catch the exception with a try statement before it bubbles to a breakpoint and pass the caught exception through the callback function. So we can modify the above example as follows.

Function async(fn, callback) {// Code execution path breaks here.setTimeout (function (){try {callback(null, fn())); } catch (err) { callback(err); }}, 0); } async(null, function (err, data) { if (err) { console.log('Error: %s', err.message); } else { // Do something. } }); -- Console ------------------------------ Error: object is not a functionCopy the code

As you can see, the exception is caught again. In NodeJS, almost all asynchronous apis are designed this way, with the first argument in the callback function being err. So we can handle exceptions this way when writing our own asynchronous functions, in keeping with the NodeJS design style.

With exception handling in place, we can then think about how we normally write code. Basically, our code does something, then calls a function, then does something else, then calls another function, and so on. If we were writing synchronous code, we would just need to write a try statement at the code entry point to catch all the exceptions bubbling up, as shown in the following example.

function main() {
    // Do something.
    syncA();
    // Do something.
    syncB();
    // Do something.
    syncC();
}

try {
    main();
} catch (err) {
    // Deal with exception.
}Copy the code

However, if we are writing asynchronous code, there is only hehe. Since each asynchronous function call interrupts the code execution path, exceptions can only be passed through the callback function. Therefore, we need to determine whether there is an exception in each callback function, so only three asynchronous function calls can produce the following code.

function main(callback) { // Do something. asyncA(function (err, data) { if (err) { callback(err); } else { // Do something asyncB(function (err, data) { if (err) { callback(err); } else { // Do something asyncC(function (err, data) { if (err) { callback(err); } else { // Do something callback(null); }}); }}); }}); } main(function (err) { if (err) { // Deal with exception. } });Copy the code

As you can see, the callbacks already complicate the code, and the handling of exceptions in an asynchronous manner makes it even more complicated. If NodeJS ‘biggest selling point turns out to be this, no one will want to use NodeJS, so here are some of the solutions NodeJS offers.

Domain (Domain)

Official document: nodejs.org/api/domain….

NodeJS provides domain modules that simplify exception handling for asynchronous code. Before introducing this module, we need to understand the concept of “domain”. Simply put, a domain is a JS runtime in which an exception that is not caught is thrown as a global exception. NodeJS provides methods to catch global exceptions through the Process object, as shown in the following example code

process.on('uncaughtException', function (err) {
    console.log('Error: %s', err.message);
});

setTimeout(function (fn) {
    fn();
});

-- Console ------------------------------
Error: undefined is not a functionCopy the code

While there is a place to catch global exceptions, for most exceptions, we want to catch them early and determine the path of code execution based on the results. Let’s use the following HTTP server code as an example:

function async(request, callback) { // Do something. asyncA(request, function (err, data) { if (err) { callback(err); } else { // Do something asyncB(request, function (err, data) { if (err) { callback(err); } else { // Do something asyncC(request, function (err, data) { if (err) { callback(err); } else { // Do something callback(null, data); }}); }}); }}); } http.createServer(function (request, response) { async(request, function (err, data) { if (err) { response.writeHead(500); response.end(); } else { response.writeHead(200); response.end(data); }}); });Copy the code

The above code hands the request object to the asynchronous function for processing and then returns the response based on the processing result. Here we use a callback to pass the exception, so if more async functions are called inside the async function, the code will look like this. To make the code look nice, we can use the Domain module to create a sub-domain (JS sub-runtime) for each request we process. Code running in a subdomain can throw exceptions at will, and these exceptions can be uniformly caught by the error event of a subdomain object. So the above code can be modified as follows:

function async(request, callback) {
    // Do something.
    asyncA(request, function (data) {
        // Do something
        asyncB(request, function (data) {
            // Do something
            asyncC(request, function (data) {
                // Do something
                callback(data);
            });
        });
    });
}

http.createServer(function (request, response) {
    var d = domain.create();

    d.on('error', function () {
        response.writeHead(500);
        response.end();
    });

    d.run(function () {
        async(request, function (data) {
            response.writeHead(200);
            response.end(data);
        });
    });
});Copy the code

As you can see, we create a subdomain object using the.create method and use the.run method to get to the entry point for the code that needs to run in the subdomain. The asynchronous function callback function in the subdomain is much leaner because it no longer needs to catch exceptions.

trap

Whether a global exception is caught through uncaughtException on a Process object or an error event on a subdomain object, the NodeJS official documentation strongly recommends restarting the program immediately after handling the exception rather than letting it continue. According to the official documentation, the program after the exception is in an indeterminate state of operation, and if it does not exit immediately, the program may have a serious memory leak, or it may behave strangely.

But a few facts need to be clarified here. JS itself throws.. try.. The catch mechanism does not cause memory leaks or surprise program execution, but NodeJS is not an existing JS. A large number of APIS in NodeJS are internally implemented by C/C++. Therefore, during the running of NodeJS programs, the code execution paths shuttle between the inside and outside of the JS engine. However, the EXCEPTION throwing mechanism of JS may interrupt the normal code execution process, leading to the abnormal performance of C/C++ code, resulting in memory leakage and other problems.

Therefore, it is better to restart the program after handling uncaughtException or domain exception, if you are not sure whether the C/C++ part of the code execution path will cause memory leaks. However, when using the try statement to catch the exception, it usually catches the exception of JS itself, without worrying about the appeal problem.

summary

This chapter introduces the JS asynchronous programming related knowledge, summed up the following points:

  • You can’t learn NodeJS without learning asynchronous programming.

  • Asynchronous programming relies on callbacks, and using callbacks is not necessarily asynchronous programming.

  • Data passing between functions, array traversal, and exception handling are very different in asynchronous programming than in synchronous programming.

  • Use the Domain module to simplify exception handling of asynchronous code and beware of traps.

Large sample

Learning is about applying what you have learned and mastering it. Now that we’ve covered a lot of NodeJS separately, this final chapter will cover a complete example of developing a Web server using NodeJS.

demand

We are going to develop a simple static file merge server that needs to support JS or CSS file merge requests in a format similar to the following.

http://assets.example.com/foo/??bar.js,baz.jsCopy the code

In the above URL,?? Is a delimiter, preceded by the common part of the URL of multiple files that need to be merged, followed by the difference part that needs to be separated. So when the server processes the URL, it returns the contents of the following two files combined in order.

/foo/bar.js
/foo/baz.jsCopy the code

In addition, the server needs to be able to support normal JS or CSS file requests in formats like the following.

http://assets.example.com/foo/bar.jsCopy the code

So that’s the whole requirement.

First Iteration

Rapid iteration is a good development approach, so we implemented the basic functionality of the server in the first iteration.

design

After a brief analysis of the requirements, we can roughly get the following design scheme.

           +---------+   +-----------+   +----------+
request -->|  parse  |-->|  combine  |-->|  output  |--> response
           +---------+   +-----------+   +----------+Copy the code

That is, the server first parses the URL to get the path and type (MIME) of the requested file. The server then reads the requested files and merges their contents in order. Finally, the server returns a response to complete processing of a request.

In addition, the server needs to have a root directory when reading files, and the HTTP port that the server listens to is best not written in code, so the server needs to be configurable.

implementation

Based on the above design, we wrote the first version of the code as follows.

var fs = require('fs'), path = require('path'), http = require('http'); var MIME = { '.css': 'text/css', '.js': 'application/javascript' }; function combineFiles(pathnames, callback) { var output = []; (function next(i, len) { if (i < len) { fs.readFile(pathnames[i], function (err, data) { if (err) { callback(err); } else { output.push(data); next(i + 1, len); }}); } else { callback(null, Buffer.concat(output)); } }(0, pathnames.length)); } function main(argv) { var config = JSON.parse(fs.readFileSync(argv[0], 'utf-8')), root = config.root || '.', port = config.port || 80; http.createServer(function (request, response) { var urlInfo = parseURL(root, request.url); combineFiles(urlInfo.pathnames, function (err, data) { if (err) { response.writeHead(404); response.end(err.message); } else { response.writeHead(200, { 'Content-Type': urlInfo.mime }); response.end(data); }}); }).listen(port); } function parseURL(root, url) { var base, pathnames, parts; if (url.indexOf('?? ') === -1) { url = url.replace('/', '/?? '); } parts = url.split('?? '); base = parts[0]; pathnames = parts[1].split(',').map(function (value) { return path.join(root, base, value); }); return { mime: MIME[path.extname(pathnames[0])] || 'text/plain', pathnames: pathnames }; } main(process.argv.slice(2));Copy the code

The above code fully implements the required functions of the server, and the following points are worth noting:

  1. Command line arguments are used to pass the JSON configuration file path, and the entry function is responsible for reading the configuration and creating the server.

  2. The entry function completely describes the program’s execution logic, where the specific implementation of URL parsing and file merging is encapsulated in the other two functions.

  3. During URL parsing, the common URL is converted to a file merge URL so that the processing methods of the two urls can be consistent.

  4. Asynchronous APIS are used to read files during file merging to avoid server blocking while waiting for disk I/OS.

We can save the above code as server.js, and then we can start the program with the Node server.js config.json command, and we have our first static file merge server ready.

In addition, there is a less obvious logical flaw in the above code. For example, there are surprises when you request a server with the following URL.

    http://assets.example.com/foo/bar.js,foo/baz.jsCopy the code

After analysis, we will find that the problem is/is automatically replaced /?? This behavior, and this is a problem we can solve in the second iteration.

Second iteration

After the first iteration, we had a working version that met the functional requirements. Next, we need to look at the code from a performance perspective and see what improvements can be made.

design

It might have been faster to replace the map method with a for loop, but the biggest performance problem with the first version of the code was in the process from reading the file to output the response. We can deal with /? A. js, B. js, C. js Take an example of this request and see how long it takes to process it.

Sends a request waiting for server response receiving the response -- -- -- -- -- -- -- -- - + -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - + -- -- -- -- -- -- -- -- -- -- -- -- -- > analytical request -- -- -- -- -- -- - read a. s. -- -- -- -- -- - read b.j s -- -- -- -- -- -- - read SAN Antonio s Merge data - Output the responseCopy the code

As you can see, the first version of the code reads the requested files into memory in turn, then merges the data and outputs the response. This leads to two problems:

  1. When a large number of files are requested, it takes time to read the files in sequence, which lengthens the waiting time of the server.

  2. Because the data output from each response needs to be fully cached in memory first, when the server requests a large number of concurrent requests, there will be a large memory overhead.

For the first problem, it’s easy to think of changing the way files are read from serial to parallel. But don’t do this, because on a mechanical disk with only one head, trying to read files in parallel will cause the head to wobble a lot and reduce I/O efficiency. For solid-state drives, although there are indeed multiple parallel IO channels, the hard drive is already doing parallel IO for multiple requests that the server processes in parallel. Using parallel IO for a single request is like robbing Peter to pay Paul. Therefore, instead of switching to parallel IO, the correct approach is to output the response while reading the file, moving the response output forward to the time when the first file is read. After this adjustment, the entire request processing process looks like this.

Sends a request waiting for server response receiving the response -- -- -- -- -- -- -- -- - + - + -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - > - parse request - check file exists - output response head -- -- -- -- -- - read and output a. s -- -- -- -- -- - Read and output B. js ------ read and output C. jsCopy the code

Once the first problem is solved, the second problem is solved because the server does not need to fully cache the output data for each request.

implementation

Based on the above design, the second version of the code adjusts some functions in the following way.

function main(argv) { var config = JSON.parse(fs.readFileSync(argv[0], 'utf-8')), root = config.root || '.', port = config.port || 80; http.createServer(function (request, response) { var urlInfo = parseURL(root, request.url); validateFiles(urlInfo.pathnames, function (err, pathnames) { if (err) { response.writeHead(404); response.end(err.message); } else { response.writeHead(200, { 'Content-Type': urlInfo.mime }); outputFiles(pathnames, response); }}); }).listen(port); } function outputFiles(pathnames, writer) { (function next(i, len) { if (i < len) { var reader = fs.createReadStream(pathnames[i]); reader.pipe(writer, { end: false }); reader.on('end', function() { next(i + 1, len); }); } else { writer.end(); } }(0, pathnames.length)); } function validateFiles(pathnames, callback) { (function next(i, len) { if (i < len) { fs.stat(pathnames[i], function (err, stats) { if (err) { callback(err); } else if (! stats.isFile()) { callback(new Error()); } else { next(i + 1, len); }}); } else { callback(null, pathnames); } }(0, pathnames.length)); }Copy the code

As you can see, the second version of the code immediately prints out the response header after checking that all the requested files are valid, and then prints out the response contents as it reads the files in sequence. Also, the second version simplifies the code by directly using read-only data streams when reading files.

Third Iteration

After the second iteration, the function and performance of the server itself have been preliminarily satisfied. Next we need to take a fresh look at the code from a stability perspective and see what else needs to be done.

design

From an engineering standpoint, there is no infallible system. Even if the second iteration of the code was checked to make sure it was bug-free, it’s hard to say whether NodeJS itself, or the operating system itself, or even the hardware itself would cause our server applications to crash one day. Therefore, a typical server program in a production environment has a daemon that restarts the service immediately if it dies. The code for a typical daemon is much simpler than the code for a server, and it is probabilistically more difficult for the daemon to fail. With a little more rigor, the daemon itself can even restart itself when it dies, thus achieving double insurance.

Therefore, in this iteration, we first make use of NodeJS process management mechanism, taking the daemon as the parent process and the server program as the child process, and let the parent process monitor the running status of the child process and restart the child process in case of its abnormal exit.

implementation

Based on the above design, we wrote the code needed for the daemon.

var cp = require('child_process');

var worker;

function spawn(server, config) {
    worker = cp.spawn('node', [ server, config ]);
    worker.on('exit', function (code) {
        if (code !== 0) {
            spawn(server, config);
        }
    });
}

function main(argv) {
    spawn('server.js', argv[0]);
    process.on('SIGTERM', function () {
        worker.kill();
        process.exit(0);
    });
}

main(process.argv.slice(2));Copy the code

In addition, the entry functions of the server code itself need to be adjusted as follows.

function main(argv) {
    var config = JSON.parse(fs.readFileSync(argv[0], 'utf-8')),
        root = config.root || '.',
        port = config.port || 80,
        server;

    server = http.createServer(function (request, response) {
        ...
    }).listen(port);

    process.on('SIGTERM', function () {
        server.close(function () {
            process.exit(0);
        });
    });
}Copy the code

We can save the daemon code as daemon.js, and then we can start the service from Node daemon.js config.json, and the daemon further starts and monitors the server process. In addition, to enable normal service termination, we have the daemon terminate the server process when it receives the SIGTERM signal. On the other hand, the server process stops the HTTP service after receiving the SIGTERM signal. At this point, our server procedures on the spectrum of a lot of.

Fourth Iteration

After we have solved the problem of the server’s own functionality, performance, and reliability, then we need to consider the problem of code deployment, as well as the problem of server control.

design

Typically, an application has a fixed deployment directory on the server to which it is republished every time it is updated. Once deployed, services can generally be started and stopped using a fixed service control script. So our server application deployment directory can be designed as follows.

- deploy/
    - bin/
        startws.sh
        killws.sh
    + conf/
        config.json
    + lib/
        daemon.js
        server.jsCopy the code

In the directory structure above, we have sorted service control scripts, configuration files, and server code.

implementation

After storing the corresponding files according to the above directory structure, let’s take a look at how to write the control script. The first is start.sh.

#! /bin/sh if [ ! -f "pid" ] then node .. /lib/daemon.js .. /conf/config.json & echo $! > pid fiCopy the code

Then killws.sh.

#! /bin/sh if [ -f "pid" ] then kill $(tr -d '\r\n' < pid) rm pid fiCopy the code

So we have a simple code deployment directory and service control script, and our server application is up and running.

Subsequent iterations

Once we get the server up and running, we’ll probably find a lot of things to improve on. For example, when the server program merges JS files, it can automatically insert one between JS files. To avoid syntax problems such as server applications that need to provide logs to count traffic, server applications that need to be able to take full advantage of multicore cpus, and so on. At this point, after learning NodeJS for so long, you should already know how to do it.

summary

This chapter wraps up the tutorial by putting together a complete example of using NodeJS. Here are some suggestions for the new NodeJSer.

  • Be familiar with the official API documentation. This is not meant to be familiar enough to remember the name and usage of every API, but rather to be familiar with what NodeJS offers and where to query the API documentation if you need to.

  • Design before you implement. Before developing a program, you should first have a global design, not necessarily comprehensive, but enough to write some code.

  • Design after implementation. After you’ve written some code, you’ve got some concrete stuff, and you’re bound to find details that you missed. At this point, the previous design can be improved in turn in preparation for the second iteration.

  • Make full use of the tripartite package. NodeJS has a large ecosystem, and it saves a lot of time to see if there are any third-party packages available before writing code.

  • Don’t be superstitious about the three-way package. Anything overdone is bad, and so is the three-way package. Tripartite package is a black box, each use of a tripartite package, for the program to increase a potential risk. And it’s hard for a tripartite package to provide exactly what the program needs, so each additional tripartite package makes the program more bloated. So think twice before deciding to use a tripartite package.