Node.js

Posted by huangqing on December 16, 2020

Node.js 事件循环

Node.js 是单进程单线程应用程序,但是因为 V8 引擎提供的异步执行回调接口,通过这些接口可以处理大量的并发,所以性能非常高。

Node.js 几乎每一个 API 都是支持回调函数的。

Node.js 基本上所有的事件机制都是用设计模式中观察者模式实现。

Node.js 单线程类似进入一个while(true)的事件循环,直到没有事件观察者退出,每个异步事件都生成一个事件观察者,如果有事件发生就调用该回调函数.

事件驱动程序

Node.js 使用事件驱动模型,当web server接收到请求,就把它关闭然后进行处理,然后去服务下一个web请求。

当这个请求完成,它被放回处理队列,当到达队列开头,这个结果被返回给用户。

这个模型非常高效可扩展性非常强,因为 webserver 一直接受请求而不等待任何读写操作。(这也称之为非阻塞式IO或者事件驱动IO)

在事件驱动模型中,会生成一个主循环来监听事件,当检测到事件时触发回调函数。

整个事件驱动的流程就是这么实现的,非常简洁。有点类似于观察者模式,事件相当于一个主题(Subject),而所有注册到这个事件上的处理函数相当于观察者(Observer)。

package.json

  • name 设置了应用程序/软件包的名称。
  • author 列出软件包的作者名称
  • contributors 除作者外,该项目可以有一个或多个贡献者。 此属性是列出他们的数组。
  • bugs 链接到软件包的问题跟踪器,最常用的是 GitHub 的 issues 页面。
  • homepage 设置软件包的主页。
  • version 表明了当前的版本。此属性遵循版本的语义版本控制记法,这意味着版本始终以 3 个数字表示:x.x.x。第一个数字是主版本号,第二个数字是次版本号,第三个数字是补丁版本号。
  • license 指定软件包的许可证
  • keywords 此属性包含与软件包功能相关的关键字数组
  • description 是应用程序/软件包的简短描述。
  • repository 此属性指定了此程序包仓库所在的位置。"repository": "github:nodejscn/node-api-cn"
  • main 设置了应用程序的入口点。
  • private 如果设置为 true,则可以防止应用程序/软件包被意外地发布到 npm。
  • scripts 定义了一组可以运行的 node 脚本。
  • dependencies 设置了作为依赖安装的 npm 软件包的列表。
  • devDependencies 设置了作为开发依赖安装的 npm 软件包的列表。
  • engines 设置了此软件包/应用程序在哪个版本的 Node.js 上运行。
  • browserslist 用于告知要支持哪些浏览器(及其版本)。

EventEmitter

方法: new events.EventEmitter()

  1. addListener(event, listener) 为指定事件添加一个监听器到监听器数组的尾部。
  2. on(event, listener) 为指定事件注册一个监听器,接受一个字符串 event 和一个回调函数。
  3. once(event, listener) 为指定事件注册一个单次监听器,即 监听器最多只会触发一次,触发后立刻解除该监听器。
  4. removeListener(event, listener) 移除指定事件的某个监听器,监听器必须是该事件已经注册过的监听器。它接受两个参数,第一个是事件名称,第二个是回调函数名称。
  5. removeAllListeners([event]) 移除所有事件的所有监听器, 如果指定事件,则移除指定事件的所有监听器。
  6. listeners(event) 返回指定事件的监听器数组
  7. emit(event, [arg1], [arg2], [...]) 按监听器的顺序执行执行每个监听器,如果事件有注册监听返回 true,否则返回 false。

类方法:events.emitter.listenerCount(eventName)

  1. listenerCount(emitter, event) 返回指定事件的监听器数量。

事件:

  1. newListener(event,listener)
  2. removeListener(event,listener)

Error:

EventEmitter 定义了一个特殊的事件 error,它包含了错误的语义,我们在遇到 异常的时候通常会触发 error 事件

// 引入 events 模块
var events = require('events');
// 创建 eventEmitter 对象
var eventEmitter = new events.EventEmitter();

// 绑定事件及事件的处理程序
eventEmitter.on('eventName', eventHandler);
// 触发事件
eventEmitter.emit('eventName');

模块系统

require:

var hello = require('./hello');

exports:

exportsmodule.exports 的使用

如果要对外暴露属性或方法,就用 exports 就行,要暴露对象(类似class,包含了很多属性和方法),就用 module.exports

exports.world = function() {
  console.log('Hello World');
}

module.exports = function() {
  // ...
}

Node.js 标准库

  • assert - 断言
  • async_hooks - 异步钩子
  • Buffer - 缓冲器
  • child_process - 子进程
  • cluster - 集群
  • console - 控制台
  • crypto - 加密
  • debugger - 调试器
  • dgram - 数据报
  • dns - 域名服务器
  • domain - 域
  • Error - 错误
  • events - 事件触发器
  • fs - 文件系统
  • global - 全局变量
  • http - HTTP
  • http2 - HTTP/2
  • https - HTTPS
  • inspector - 检查器
  • module - 模块
  • net - 网络
  • os - 操作系统
  • path - 路径
  • perf_hooks - 性能钩子
  • process - 进程
  • punycode - 域名代码
  • querystring - 查询字符串
  • readline - 逐行读取
  • repl - 交互式解释器
  • stream - 流
  • string_decoder - 字符串解码器
  • timer - 定时器
  • tls - 安全传输层
  • trace_events - 跟踪事件
  • tty - 终端
  • url - URL
  • util - 实用工具
  • v8 - V8引擎
  • vm - 虚拟机
  • wasi - WASI
  • worker_threads - 工作线程
  • zlib - 压缩

Node.js 框架和工具

  • AdonisJs: 一个全栈框架,高度专注于开发者的效率、稳定和信任。 Adonis 是最快的 Node.js Web 框架之一。
  • Express: 提供了创建 Web 服务器的最简单但功能最强大的方法之一。 它的极简主义方法,专注于服务器的核心功能,是其成功的关键。
  • Fastify: 一个 Web 框架,高度专注于提供最佳的开发者体验(以最少的开销和强大的插件架构)。 Fastify 是最快的 Node.js Web 框架之一。
  • hapi: 一个富框架,用于构建应用程序和服务,使开发者可以专注于编写可重用的应用程序逻辑,而不必花费时间来搭建基础架构。
  • koa: 由 Express 背后的同一个团队构建,旨在变得更简单更轻巧。 新项目的诞生是为了满足创建不兼容的更改而又不破坏现有社区。
  • Loopback.io: 使构建需要复杂集成的现代应用程序变得容易。
  • Meteor: 一个强大的全栈框架,以同构的方式使用 JavaScript 构建应用(在客户端和服务器上共享代码)。 曾经是提供所有功能的现成工具,现在可以与前端库 React,Vue 和 + Angular 集成。 也可以用于创建移动应用。
  • Micro: 提供了一个非常轻量级的服务器,用于创建异步的 HTTP 微服务。
  • NestJS: 一个基于 TypeScript 的渐进式 Node.js 框架,用于构建企业级的高效、可靠和可扩展的服务器端应用程序。
  • Next.js: 用于渲染服务器端渲染的 React 应用程序的框架。
  • Nx: 使用 NestJS、Express、React、Angular 等进行全栈开发的工具包! Nx 有助于将开发工作从一个团队(构建一个应用程序)扩展到多个团队(在多个应用程序上进行协作)!
  • Socket.io: 一个实时通信引擎,用于构建网络应用程序。

Buffer(缓冲区)

JavaScript 语言自身只有字符串数据类型,没有二进制数据类型。

但在处理像TCP流或文件流时,必须使用到二进制数据。因此在 Node.js中,定义了一个 Buffer 类,该类用来创建一个专门存放二进制数据的缓存区。

创建 Buffer 类

Buffer 提供了以下 API 来创建 Buffer 类:

  • Buffer.alloc(size[, fill[, encoding]]): 返回一个指定大小的 Buffer 实例,如果没有设置 fill,则默认填满 0
  • Buffer.allocUnsafe(size): 返回一个指定大小的 Buffer 实例,但是它不会被初始化,所以它可能包含敏感的数据
  • Buffer.allocUnsafeSlow(size)
  • Buffer.from(array): 返回一个被 array 的值初始化的新的 Buffer 实例(传入的 array 的元素只能是数字,不然就会自动被 0 覆盖)
  • Buffer.from(arrayBuffer[, byteOffset[, length]]): 返回一个新建的与给定的 ArrayBuffer 共享同一内存的 Buffer。
  • Buffer.from(buffer): 复制传入的 Buffer 实例的数据,并返回一个新的 Buffer 实例
  • Buffer.from(string[, encoding]): 返回一个被 string 的值初始化的新的 Buffer 实例
// 创建一个长度为 10、且用 0 填充的 Buffer。
const buf1 = Buffer.alloc(10);

// 创建一个长度为 10、且用 0x1 填充的 Buffer。 
const buf2 = Buffer.alloc(10, 1);

// 创建一个长度为 10、且未初始化的 Buffer。
// 这个方法比调用 Buffer.alloc() 更快,
// 但返回的 Buffer 实例可能包含旧数据,
// 因此需要使用 fill() 或 write() 重写。
const buf3 = Buffer.allocUnsafe(10);

// 创建一个包含 [0x1, 0x2, 0x3] 的 Buffer。
const buf4 = Buffer.from([1, 2, 3]);

// 创建一个包含 UTF-8 字节 [0x74, 0xc3, 0xa9, 0x73, 0x74] 的 Buffer。
const buf5 = Buffer.from('tést');

// 创建一个包含 Latin-1 字节 [0x74, 0xe9, 0x73, 0x74] 的 Buffer。
const buf6 = Buffer.from('tést', 'latin1');

写入缓冲区

buf.write(string[, offset[, length]][, encoding])
  • string - 写入缓冲区的字符串。
  • offset - 缓冲区开始写入的索引值,默认为 0 。
  • length - 写入的字节数,默认为 buffer.length
  • encoding - 使用的编码。默认为 ‘utf8’
buf = Buffer.alloc(256);
len = buf.write("www.runoob.com");

console.log("写入字节数 : "+  len);

从缓冲区读取数据

buf.toString([encoding[, start[, end]]])
buf = Buffer.alloc(26);
for (var i = 0 ; i < 26 ; i++) {
  buf[i] = i + 97;
}

console.log( buf.toString('ascii'));       // 输出: abcdefghijklmnopqrstuvwxyz
console.log( buf.toString('ascii',0,5));   //使用 'ascii' 编码, 并输出: abcde
console.log( buf.toString('utf8',0,5));    // 使用 'utf8' 编码, 并输出: abcde
console.log( buf.toString(undefined,0,5)); // 使用默认的 'utf8' 编码, 并输出: abcde

缓冲区合并

Buffer.concat(list[, totalLength])
  • list - 用于合并的 Buffer 对象数组列表。
  • totalLength - 指定合并后Buffer对象的总长度。
var buffer1 = Buffer.from(('菜鸟教程'));
var buffer2 = Buffer.from(('www.runoob.com'));
var buffer3 = Buffer.concat([buffer1,buffer2]);
console.log("buffer3 内容: " + buffer3.toString());

拷贝缓冲区

buf.copy(targetBuffer[, targetStart[, sourceStart[, sourceEnd]]])
var buf1 = Buffer.from('abcdefghijkl');
var buf2 = Buffer.from('RUNOOB');

//将 buf2 插入到 buf1 指定位置上
buf2.copy(buf1, 2);

console.log(buf1.toString());

缓冲区裁剪

buf.slice([start[, end]])
var buffer1 = Buffer.from('runoob');
// 剪切缓冲区
var buffer2 = buffer1.slice(0,2);
console.log("buffer2 content: " + buffer2.toString());

Stream(流)

Stream 是一个抽象接口,Node 中有很多对象实现了这个接口。例如,对http 服务器发起请求的request 对象就是一个 Stream,还有stdout(标准输出)。

Node.js,Stream 有四种流类型:

  • Readable - 可读操作。
  • Writable - 可写操作。
  • Duplex - 可读可写操作.
  • Transform - 操作被写入数据,然后读出结果。

所有的 Stream 对象都是 EventEmitter 的实例。常用的事件有:

  • data - 当有数据可读时触发。
  • end - 没有更多的数据可读时触发。
  • error - 在接收和写入过程中发生错误时触发。
  • finish - 所有数据已被写入到底层系统时触发。

从流中读取数据

var fs = require("fs");
var data = '';

// 创建可读流
var readerStream = fs.createReadStream('input.txt', 'utf-8');

// 设置编码为 utf8。
//readerStream.setEncoding('UTF8');

// 处理流事件 --> data, end, and error
readerStream.on('data', function(chunk) {
   data += chunk;
});

readerStream.on('end',function(){
   console.log(data);
});

readerStream.on('error', function(err){
   console.log(err.stack);
});

console.log("程序执行完毕");

写入流

var fs = require("fs");
var data = '菜鸟教程官网地址:www.runoob.com';

// 创建一个可以写入的流,写入到文件 output.txt 中
var writerStream = fs.createWriteStream('output.txt');

// 使用 utf8 编码写入数据
writerStream.write(data,'UTF8');

// 标记文件末尾
writerStream.end();

// 处理流事件 --> finish、error
writerStream.on('finish', function() {
    console.log("写入完成。");
});

writerStream.on('error', function(err){
   console.log(err.stack);
});

console.log("程序执行完毕");

管道流

var fs = require("fs");

// 创建一个可读流
var readerStream = fs.createReadStream('input.txt');

// 创建一个可写流
var writerStream = fs.createWriteStream('output.txt');

// 管道读写操作
// 读取 input.txt 文件内容,并将内容写入到 output.txt 文件中
readerStream.pipe(writerStream);

链式流

var fs = require("fs");
var zlib = require('zlib');

// 压缩 input.txt 文件为 input.txt.gz
fs.createReadStream('input.txt')
  .pipe(zlib.createGzip())
  .pipe(fs.createWriteStream('input.txt.gz'));
  
console.log("文件压缩完成。");

文件系统

Node.js 提供一组类似 UNIX(POSIX)标准的文件操作API。 Node 导入文件系统模块(fs)语法如下所示:

var fs = require("fs")

异步和同步

Node.js 文件系统(fs 模块)模块中的方法均有异步和同步版本,例如读取文件内容的函数有异步的 fs.readFile() 和同步的 fs.readFileSync()

异步的方法函数最后一个参数为回调函数,回调函数的第一个参数包含了错误信息(error)。

建议大家使用异步方法,比起同步,异步方法性能更高,速度更快,而且没有阻塞

  • fs.rmdir(path, callback)
  • fs.mkdir(path[, mode], callback)
  • fs.readdir(path, callback)

  • fs.open(path, flags[, mode], callback)
  • fs.read(fd, buffer, offset, length, position, callback)
  • fs.write(fd, buffer, offset, length[, position], callback)
  • fs.write(fd, data[, position[, encoding]], callback)
  • fs.close(fd, callback)
  • fs.unlink(path, callback)
  • fs.rename(oldPath, newPath, callback)
  • fs.stat(path, callback)

  • fs.readFile(filename[, options], callback)
  • fs.writeFile(filename, data[, options], callback)
  • fs.appendFile(filename, data[, options], callback)
  • fs.exists(path, callback)
var fs = require("fs");

//input.text 内容: site:www.runoob.com

// 异步读取
fs.readFile('input.txt', function (err, data) {
   console.log("异步读取: " + data.toString());
});
// 同步读取
var data = fs.readFileSync('input.txt');
// 获取文件信息
fs.stat('input.txt', function (err, stats) {
    // 检测文件类型
    console.log(stats.isFile());
})

// 异步打开文件
fs.open('input.txt', 'r+', function(err, fd) {
    console.log("文件打开成功!"); 

    // 截取文件
    fs.ftruncate(fd, 10, function(err){
        if (err){ console.log(err); } 
        //截取10字节内的文件内容,超出部分将被去除
        //site:www.r
    }

    // 读取文件
    var buf = new Buffer.alloc(1024);
    fs.read(fd, buf, 0, buf.length, 0, function(err, bytes){
        if (err){
            console.log(err);
        }
        console.log(bytes + "  字节被读取");
        
        // 仅输出读取的字节
        if(bytes > 0){
            console.log(buf.slice(0, bytes).toString());
        }

        // 关闭文件
        fs.close(fd, function(err){
            if (err){ console.log(err); } 
            console.log("文件关闭成功");
        });
    });
});

// 写入文件
fs.writeFile('input.txt', '我是通 过fs.writeFile 写入文件的内容',function(err) {
   if (err) {
       return console.error(err);
   }
   console.log("数据写入成功!");
});

// 删除文件
fs.unlink('input.txt', function(err) {
   if (err) {
       return console.error(err);
   }
   console.log("文件删除成功!");
});

// 创建目录
// tmp 目录必须存在
fs.mkdir("/tmp/test/",function(err){
   if (err) {
       return console.error(err);
   }
   console.log("目录创建成功。");
});

// 读取目录
fs.readdir("/tmp/",function(err, files){
   if (err) {
       return console.error(err);
   }
   files.forEach( function (file){
       console.log( file );
   });
});

// 删除目录
fs.rmdir("/tmp/test",function(err){
   if (err) {
       return console.error(err);
   }
});

路由

HTTP服务器的功能,需要的所有数据都会包含在 request 对象中。通过 urlquerystring 模块解析url。

                   url.parse(string).query
                                           |
           url.parse(string).pathname      |
                       |                   |
                       |                   |
                     ------ -------------------
http://localhost:8888/start?foo=bar&hello=world
                                ---       -----
                                 |          |
                                 |          |
              querystring.parse(queryString)["foo"]    |
                                            |
                         querystring.parse(queryString)["hello"]

全局对象

  • global :JavaScript 中有一个特殊的对象,称为全局对象(Global Object),它及其所有属性都可以在程序的任何地方访问,即全局变量。在浏览器 JavaScript 中,通常 window 是全局对象, 而 Node.js 中的全局对象是 global,所有全局变量(除了 global 本身以外)都是 global 对象的属性。
  • __filename : __filename 表示当前正在执行的脚本的文件名。它将输出文件所在位置的绝对路径
  • __dirname : __dirname 表示当前执行脚本所在的目录。
  • process : process 是一个全局变量,即 global 对象的属性。它用于描述当前Node.js 进程状态的对象,提供了一个与操作系统的简单接口。通常在你写本地命令行程序的时候会用到。
    • process.nextTick()
  • setTimeout(cb, ms)
  • clearTimeout(t)
  • setImmediate()
  • setInterval(cb, ms)
  • console
console.log([data][, ...])
console.info([data][, ...])
console.error([data][, ...])
console.warn([data][, ...])
console.dir(obj[, options])
console.time(label)
console.timeEnd(label)
console.trace(message[, ...])
console.assert(value[, message][, ...])

util 常用工具

const util = require('util');
  • util.callbackify : async 异步函数(或者一个返回值为 Promise 的函数)转换成遵循异常优先的回调风格的函数
  • util.inherits(constructor, superConstructor)
  • util.inspect(object,[showHidden],[depth],[colors]) 是一个将任意对象转换 为字符串的方法,通常用于调试和错误输出。它至少接受一个参数 object,即要转换的对象。
  • util.isArray(object) 如果给定的参数 object 是一个数组返回 true,否则返回 false
  • util.isRegExp(object)
  • util.isDate(object)

模块

OS 模块 提供基本的系统操作函数。 Path 模块 提供了处理和转换文件路径的工具。 Net 模块 用于底层的网络通信。提供了服务端和客户端的的操作。 DNS 模块 用于解析域名。 Domain 模块 简化异步代码的异常处理,可以捕捉处理try catch无法捕捉的。

Path 文件路径

path.dirname: 获取文件的父文件夹。 path.basename: 获取文件名部分。 path.extname: 获取文件的扩展名。 path.join() : 连接路径的两个或多个片段 path.resolve() : 获得相对路径的绝对路径计算 path.normalize() : 解析和规范化

Web 模块

index.js

var server = require("./server");
var router = require("./router");

server.start(router.route);

server.js

const http = require("http");
const url = require("url");
//cnpm install mime  下载mime模块
const mime = require("mime");

var static = require("./static");

const port = 8899;

async function readStaticFile(request, response) {
    let _url = request.url;
    _url = url.parse(_url);
    let path = _url.pathname;
    if (path === "/") {
        path = '/index.html';
    }
    let data = await static.readStatic(`.${path}`);

    if (data) {
        response.end(data);
        return true;
    }

    return false;
}

function readPostData(request) {

    return new Promise((resolve, reject) => {
        // 定义了一个post变量,用于暂存请求体的信息
        var post = '';

        // 通过req的data事件监听函数,每当接受到请求体的数据,就累加到post变量中
        request.on('data', function (chunk) {
            post += chunk;
        });

        // 在end事件触发后,通过querystring.parse将post解析为真正的POST请求格式,然后向客户端返回。
        request.on('end', function () {
            post = querystring.parse(post);
            //res.end(util.inspect(post));
            resolve(post);
        });
    });

}

async function readPostContent(request) {
    const data = await readPostData(request);
    return data;
}


function start(route) {
    async function onRequest(request, response) {
        // 解析url
        var _url = url.parse(request.url);
        var pathname = _url.pathname;
        var result;
        console.log(`Request for ${pathname} received.`);

        //post请求
        result = await readPostContent(request);
        if ("{}" !== JSON.stringify(result)) {
            response.end(util.inspect(result));
            return;
        }

        //请求静态文件
        result = await readStaticFile(request, response);
        if (result) {
            return;
        }

        // 路由
        result = route(pathname)
        if (result) {
            response.writeHead(200, { "Content-Type": "text/plain;charset=utf-8" });
            response.end(result);
            return;
        };

        // 未找到静态资源和路由 响应404
        response.writeHead(404, { "Content-Type": mime.getType(pathname) || "text/plain;charset=utf-8" });
        response.end();
    }

    http.createServer(onRequest).listen(port);
    console.log(`Server has started at port ${port}`);
}

exports.start = start;

static.js

let fs = require("fs");
let url = require("url");

const readFile = (path) => {
    return new Promise(function (resolve, reject) {
        fs.readFile(path, function (err, data) {
            if (err) {
                resolve({
                    success: false,
                    data: null
                });
                return;
            }

            resolve({
                success: true,
                data: data
            });
        });
    });
}

exports.readStatic = async function (path) {
    let result = await readFile(path);
    return result.success ? result.data : null;
}

router.js

function route(pathname) {
    if (pathname.includes("user")) {
        return `About to route a request for ${pathname}`;
    }

    return false;
}

exports.route = route;

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>nodjs www - index</title>
</head>
<link rel="stylesheet" href="css/index.css">

<body>
    <ul>
        <li><a href="/index.html">首页</a></li>
        <li><a href="/about.html">关于</a></li>
    </ul>

    <ul>
        <li><img src="/images/home.png" /></li>
        <li></li>
    </ul>

</body>
<script src="js/index.js"></script>

</html>

常用的工具库

  • formidable :A Node.js module for parsing form data, especially file uploads.
  • supervisor : It runs your program, and watches for code changes, so you can have hot-code reloading-ish behavior
  • mkdirp :
  • mz : MZ - Modernize node.js
  • nodemon : nodemon is a tool that helps develop node.js based applications by automatically restarting the node application when file changes in the directory are detected.
  • forever : A simple CLI tool for ensuring that a given script runs continuously

一些流行的全局软件包的示例有:

  • npm
  • create-react-app
  • vue-cli
  • grunt-cli
  • mocha
  • react-native-cli
  • gatsby-cli
  • forever
  • nodemon

引用