Introduction to Node.js
-
Node.js is a JavaScript runtime environment designed to handle asynchronous events efficiently
-
It uses the standard I/O streams, stdin, stdout and stderr
process.stdin.read(); process.stdout.write(); process.stderr.write();
- console.log could be described as a wrapper around process.stdout.write that appends a newline
- console.error is also similar, but for process.stderr.write
- Preferably don't use process.stdin.read, it has more quirks compared to the output streams due to the interactive shell
- Use an already existing package for reading input
Command line scripts
-
Add the following line to the top of a script file:
#!/usr/bin/env node
- Locates node within the system and then uses node to interpret the rest of the program
-
Run the code in strict mode
"use strict";
-
Add executable permissions if there are none
chmod u+x {file}
-
Access arguments passed from the shell
-
process.argv contains all arguments passed to the script
-
process.argv[0] and process.argv[1] is the path to invoke node and the path to our file respectively
-
process.argv.slice(2) contains all other arguments after the second index
-
Minimist is a library for processing arguments
-
Returns an object with the following structure: { _: [], <parameter>:<value>, ... }
-
The number of <parameter>:<value> pairs in the object is arbitrary but of course, less than the number of arguments
-
_ is a key to an array which holds arguments that Minimist does not know how to parse
let args = require("minimist"); args(process.argv.slice(2)); // Configure arguments to data types args(process.argv.slice(2), { boolean: ["help"], string: ["file"] });
-
-
Working with files:
- __dirname is a variable that contains the absolute path of the currently executing script
- process.cwd() returns current working directory
- Synchronously read from file, expects a callback function now
let path = require("path"); let fs = require("fs"); let filePath = path.resolve(file); let contents = fs.readFileSync(filePath); function callback(error, contents) { if (error) { process.stderr.write(error); } else { process.stdout.write(contents); } } fs.readFile(filePath, callback);
- Asynchronously read from file
fs.readFile(filePath);
Streams
-
Simplex streams are unidirectional, meaning they are either readable or writable
-
Duplex streams are readable and writable
let path = require("path"); let fs = require("fs"); let readStream = fs.createReadStream(filePath); let writeStream = fs.createWriteStream(filePath); let outStream = readStream; outStream.pipe(process.stdout); // Writes input to stdout
-
Transform streams intercept the pipe to process items one at a time
- Chunk is a buffer from the stream
- The callback function notifies the transform is complete
let Transform = require("stream").Transform; let upperStream = new Transform({ transform(chunk, encoding, callback) { this.push(chunk.toString().toUpperCase()); callback(); } }); let outStream = outStream.pipe(upperStream); outStream.pipe(process.stdout);
-
GZIP compress with ZLIB
-
Unzipping, processing then rezipping files transferred over some protocol
- Create zipping and unzipping streams
- Can change file type of the compressed file to .gz
let zlib = require("zlib"); let gzipStream = zlib.createGzip(); let outStream = outStream.pipe(gzipStream); outputFile = `{outputFile}.gz`; let gunzipStream = zlib.createGunzip(); let outStream = outStream.pipe(gunzipStream);
-
Determine the end of a stream
function streamComplete(stream) { return new Promise(function callback(response) { stream.on("end", response); }); } outStream.pipe(process.stdout); await streamComplete(outStream);
Database
- SQLite3 can be maintained directly by the application and not externally
let path = require("path"); let fs = require("fs"); let sqlite3 = require("sqlite3"); const DB_PATH = path.join(__dirname, "my.db"); const DB_SQL_PATH = path.join(__dirname, "mydb.sql"); // Some helpers let myDb = new sqlite3.Database(DB_PATH); let SQL3 = { run(...args) { return new Promise(function callback(resolve, reject) { myDB.run(...args, function onResult(error) { if (error) { reject(error); } else { resolve(this); } }); }); }, // Return promise interface for each callback // Documentation: https://github.com/TryGhost/node-sqlite3/wiki/API get: util.promisify(myDB.get.bind(myDB)), all: util.promisify(myDB.all.bind(myDB)), exec: util.promisify(myDB.exec.bind(myDB)) };
Web Servers
-
Set up web server and handle requests to the server
let http = require("http"); const HTTP_PORT = 8039; async function handleRequest(request, respones) { if (request.url == "/hello") { // Write header and closes response stream // The "Hello World!" of HTTP servers response.writeHead(200, { "Content-Type": "text/plain" }); response.end("Hello World!"); } else { response.writeHead(404); response.end(); } } // Instantiate server let httpServer = http.createServer(handleRequest); httpServer.listen(HTTP_PORT);
-
Configure a server to only allow access to files under the web root, adds a file server that uses regex to match incoming requests
let path = require("path"); let http = require("http"); let staticAlias = require("node-static-alias"); const WEB_PATH = path.join(__dirname, "web"); const HTTP_PORT = 8039; let fileServer = new staticAlias.Server(WEB_PATH, { // Time to cache files in seconds cache: 100, serverInfo: "Server name", alias: [ { // If the URL contains index (excludes HTTP portion at the start) match: /^\/(?:index\/?)?(?:[?#].*$)?$/, serve: "index.html", force: true }, { // If the URL contains .js match: /^\/js\/.+$/, serve: "<% absPath %>", force: true }, { // If the URL contains words or numbers followed by separators match: /^\/(?:[\w\d]+)(?:[\/?#].*$)?$/, serve: function onMath(params) { return `${params.basename}.html`; } force: true }, { match: /[^]/, serve: "404.html" } ] }); async function handleRequest(request, respones) { fileServer.serve(request, response); } // Instantiate server let httpServer = http.createServer(handleRequest); httpServer.listen(HTTP_PORT);
-
Set up server to respond to API requests at a given endpoint
async function handleRequest(request, response) { // API request if (request.url == "/get-records") { // Get records from database let records = await getAllRecords(); response.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-cache" }); response.end(JSON.stringify(records)); } // Non-API requests else { fileServer.serve(request, response); } } // Instantiate server let httpServer = http.createServer(handleRequest); httpServer.listen(HTTP_PORT);
-
Modify server to use Express.js instead of manual routing
- Set up middleware, which is a function that gets called if the incoming request matches some criteria
- The order of middleware routes defined matters since they are executed in order
- Call next() within the middleware function if you want it to execute the next middleware functions other than the first
let path = require("path"); let http = require("http"); let express = require("express"); const WEB_PATH = path.join(__dirname, "web"); let app = express(); function defineRoutes() { // GET requests app.get("/get-records", async function(request, response) { let records = await getAllRecords(); response.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-cache" }); response.end(JSON.stringify(records)); }); //All incoming requests app.use(express.static(WEB_PATH, { maxAge: 100, setHeaders: function setHeaders(res) { res.setHeader("Server", "Server name"); } })); } defineRoutes(); let httpServer = http.createServer(app);
Child Processes
- Create child processes and check its status
let util = require("util"); let childProcess = require("child_process"); let child = childProcess.spawn("node", [ "file" ]); child.on("exit", function(code) { console.log("Child finished", code); }) async function main() { try { let response = await fetch(`http://localhost:{HTTP_PORT}/get-records`); if (response && response.ok) { let records = await response.json(); if (records && records.length > 0) { process.excitCode = 0; return; } } } catch(error) { ... } process.exitCode = -1 } main();
Debugging
- Use web developer tools to debug
- In chrome://inspect, which listens to the port on localhost, can observe the server activity
- Run with --inspect to broadcast the port on localhost