• 作者:老汪软件技巧
  • 发表时间:2024-10-05 07:00
  • 浏览量:

前生今世Nodejs 是基于 Chrome V8 引擎 的 JavaScript 运行环境

在 Nodejs 出现之前,JavaScript 脚本只能在浏览器上运行,它拥有浏览器环境提供的 BOM、DOM 接口对象。而 Nodejs 的诞生,JavaScript 得到了真正的释放,使得 JavaScript 像其他语言如 Java、Go 一样,得到了对网络、文件等服务端模块的操作能力。不仅如此,Nodejs 提供的运行时使 JavaScript 可以运行在桌面、手机等地方运行,使得前端得到了跨平台的能力,并且为后面兴起的前端工程化提供了基础。

无庸置疑,Nodejs 的出现是 JavaScript 历史上里程碑的一大事件。

如今 NodeJS 早已是前端必学技能之一,本人在 19 年学习前端时,有幸遇到 Nodejs,因当初认知不够,将自己局限于 "前端开发",对 NodeJS 未深入了解。直到在后来使用 NodeJS 搭建后端服务的过程中深入学习才发现了它的强大之处,对 Web 领域的认知也随着对 NodeJS 的了解而深化。

本文默认读者拥有基本的计算机操作能力

Nodejs 特性

Nodejs 最大的特性是异步非阻塞 I/O、事件驱动。

传统 Web 服务器(Apache、Tomcat)是多线程响应客户端请求,即每个客户端请求分配一个线程。这样设计的优点可以规避一个线程阻塞影响其他线程进行,缺点是线程是需要占用内存和 CPU 资源。

Nodejs 是单线程响应客户端请求,所有请求都由 V8 引擎主线程处理。由于单线程设计,所以 Nodejs 是轻量的。但单线程的缺点是一旦发生同步阻塞,所有请求都会阻塞。

I/O 是什么?

I/O 是计算机中的术语,是Input/Output的缩写,指代输入和输出。

在服务器中常见的 I/O:

文件系统I/O:涉及到文件的读写操作,例如读取文件内容、写入文件、删除文件等。网络I/O:涉及到网络通信,例如HTTP请求和响应处理、TCP/UDP通信等。数据库I/O:与数据库的交互,例如查询数据库、插入数据、更新数据等。标准I/O:包括标准输入(stdin)、标准输出(stdout)和标准错误输出(stderr)。

I/O 操作是硬件层面的操作,由操作系统内核发起,这些操作不需要 CPU 参与,所以 I/O 操作可以与 CPU 同步运行。

阻塞I/O 与非阻塞I/O

当应用程序向操作系统发起一个 I/O 操作,阻塞I/O 表现为 等待操作系统内核 I/O 操作完成后返回操作结果。而非阻塞 I/O 则是立即返回。如果使用阻塞 I/O,会导致应用程序一直处在挂起的状态,导致后续程序不能执行。所以使用非阻塞 I/O 更加合理。

异步 I/O 设计

异步I/O是计算机操作系统对输入输出的一种处理方式:发起I/O请求的线程不等I/O操作完成,就继续执行随后的代码,I/O结果用其他方式通知发起I/O请求的程序。与异步I/O相对的是更为常见的“同步(阻塞)I/O”:发起I/O请求的线程不从正在调用的I/O操作函数返回(即被阻塞),直至I/O操作完成。

NodeJS 采用异步I/O的设计模式,JavaScript 主线程执行程序,由libuv 的事件循环机制进行 I/O 调用,I/O 完成后通知主线程,形成回调。

安装

Nodejs 版本分为LTS (Long-Term Support) 和 Current 版本,LTS 版本为长期支持版本,Current 版本为最新版本。

官网地址:/en/download…

安装后,正常会将 NodeJS 设置成环境变量,在控制台中输入 “node” 会出现像浏览器控制台类似的面板,则表示安装成功

包管理工具

npm 是 Nodejs 的包管理工具,可以帮助我们快速安装、管理、更新 Nodejs 相关的模块。安装 Nodejs 通常会自动安装 npm,安装成功后,在控制台输入 “npm -v” 会出现 npm 版本号,则表示安装成功。

package.json

package.json 用于描述项目的依赖项、版本号、描述信息等。在项目构建初期,通过 npm init 命令可生成 package.json 文件。

{
  "name": "nodejs-study",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {},
  "author": "",
  "license": "ISC",
}

package-lock.json

package-lock.json 用于锁定项目依赖项的版本号,在项目构建过程中,npm 会根据 package.json 文件中的依赖项版本号,自动安装依赖项。

npm install 会发生什么根据根目录 package.json 文件寻找依赖项npm 生成 package-lock.json从 npm 仓库下载依赖项,默认 下载,如设置 npm 镜像,则从镜像中下载,从镜像中获取获取。常用如:淘宝镜像、华为云镜像、腾讯云镜像等。读取缓存中是否拥有相同版本的依赖项,如果有,则直接使用缓存,如果没有,则下载安装。压缩包解压安装到 node_modules 目录下。本地安装与全局安装

本地安装:将依赖项安装到当前项目的 node_modules 目录下。全局安装:将依赖项安装到全局目录下,可通过 npm root -g 进行查看全局目录路径,安装后可在命令行直接使用

 npm install
 npm install -g //全局安装

常用命令

npm outdated            查看当前包与最新包的版本号
npm update              升级版本
npm update --save       升级并更新 package.json 文件中的依赖项版本号
npm install xx@latest   安装最新版本的 xx 包
...

版本规则

/qq_37860930…

yarn、tyarn、pnpm、cnpm 区别

cnpm 淘宝开发的 npm 镜像,包从淘宝镜像下载

yarn 是 facebook 开源的包管理工具,与 npm 的最大区别就是并行下载、锁定依赖版本

tyarn 是 淘宝 开发的 yarn 国内镜像,包从淘宝镜像下载

pnpm 安装速度快,节省磁盘空间,但生态系统较小,兼容性相较前两者差一些

扩展自己如何发布一个 npm 包?Nodejs 版本控制器: NVM模块化开发CommonJS 规范

Nodejs 使用 Commonjs 规范实现的模块化开发。

CommonJS 提供了 require、exports、module 三个模块。

Module

module 代表当前模块,它是一个对象,它的 exports 为对外的接口

Exports

exports 是module.exports 的引用,它指向 module.exports 对象,可以对 exports 对象进行赋值,从而对外提供接口。

Require

引入一个模块,会得到该模块 module.exports 的内容,如模块未导出,则返回一个空对象。

举个例子:

// user.js
let username = "小明";
let age = 20;
exports.username = username;  // module.export.username = '小明'
exports.age = age;            // module.export.age = 20
module.exports.age = 10       // 覆盖 module.export.age = 10
// index.js
let user = require("./user");
console.log(user);  // { username: '小明', age: 10 }

exports 不允许直接赋值

// user.js
exports = { username: '小明', age: 10 }
// index.js
let user = require("./user");
console.log(user); // 相当于未导出,返回空对象 {}

导出的变量不会受到模块内部的影响

// user.js 文件
let username = "小明";
let age = 20;
const updateAge = (num) => {
  age = num;
};
module.exports = { username, age, updateAge };
// index.js 文件
let info = require("./a");
info.updateAge(26);
console.log(info.age);  // 20 不受 updateAge() 影响

require 加载规则

let user = require("./user"); // 相对路径
let user = require("/user") // 绝对路径
let user = require("user") // Nodejs 核心模块(fs、path) 或是Nodejs 全局安装模块

如果未定义文件路径后缀,则按照.js、.json、.node 的顺序依次查找。

引入文件夹

当 require 引入的是文件夹时,则会优先查找package.json文件,如果有则查找 package.json 文件中的 main 字段定义的文件,如果没有则查找 index.js 文件。


// package.json
{
  "name": "some-library",
  "main": "./test.js"
}
// test\index.js 
module.exports = { name: "小明" };
// test\test.js
module.exports = { name: "小红" };
// index.js
let info = require("./test");
console.log(info.name); // 小明, 如果package.json文件中没有main字段,则输出 小红

核心模块

Nodejs 提供了一系列的核心模块,这些模块是 Nodejs 运行时的一部分,不需要额外安装可以直接使用。

Stream 流

在应用程序中,流是指一种有序的、有起点和终点的数据流,在 Nodejs 中的 Stream 模块提供了对流的处理,包括读写操作。

举个例子,拷贝一张图片

const fs = require("fs");
let rs = fs.createReadStream("./image.png");
let ws = fs.createWriteStream("./new-image.png");
rs.pipe(ws);

流分为四种:

Readable 可读流Writable 可写流Duplex 可读可写流Transform 转换流Readable 可读流

可读流就是提供数据流的源头。

常见的可读流通过 fs.createReadStream 方法创建

工作模式

可读流拥有两种工作模式,流动(flowing)与暂停(paused)。

paused mode

当被可读流创建时,默认处于暂停状态,也可通过 pause() 方法让其回到暂停状态。

flowing mode

当可读流通过 data 事件监听时,也可通过resume()、pipe() 方法进入流动状态

let rs = fs.createReadStream("./image.png");
rs.on("data", (e) => {
  console.log(e)
})
rs.on("end", () => {
  console.log('传输结束')
})

当可读流绑定了 data 事件,会进入流动状态,当数据源有数据可读时,会触发 data 事件,并将数据传递给监听器。

绑定 end 事件没有更多的数据可读时触发。

通过 Readable 类创建可读流


let { Readable } = require("stream"); 
let r = new Readable();
r.push("Hello");
r.push(" Nodejs!");
r.push(null);
r.on("data", function (chunk) {
  console.log(chunk.toString());  // 输出两次 1. Hello 2. Nodejs!
});

readable 事件

readable 表明有了新数据,或到了流的尾部

let { Readable } = require("stream");
let fs = require("fs");
let r = new Readable();
r.push("Hello");
r.push(" Nodejs!");
r.push(null);
let ws = fs.createWriteStream("./output.txt");
r.pipe(ws);
r.on("readable", function () {
  console.log(r.read(undefined));
});

readable 会触发两次,第一次是在 push("Hello") 时触发,此时,r.read(undefiend) 为可用数据,第二次在 r.push(null) 触发, r.read(undefiend) 为 null

Writeable 可写流

可写流是数据流传输的目的地,常见的可写流通过 fs.createWriteStream 方法创建

背压

当可读流往可写流里传递数据时,会先写入缓存区,当写入过快时,会导致缓存区被写满,再写入则会导致缓存区溢出的情况,而 Nodejs 已经实现背压平衡机制,当缓存区满时,会自动停止写入,直到缓存区空闲时,再恢复写入,此时可写流会给可读流发送一个 drain 信息,可读流通过 resume 方法恢复写入。

let fs = require("fs");
let rs = fs.createReadStream("./image-1.png");
let ws = fs.createWriteStream("./new-image.png");
rs.on("data", function (chunk) {
  ws.write(chunk);
});
ws.on("drain", () => {
  rs.resume();
});

pipe 方法

pipe 方法是 Nodejs 提供可读流可写流 流动 的方法,将可读流和可写流连接起来并流动,方法内部实现了背压机制, 类似 data 事件与 drain 的组合

例子,复制图片

let rs = fs.createReadStream("./image.png");
let ws = fs.createWriteStream("./new-image.png");
rs.pipe(ws);

Duplex Streams 双工流

双工流既可以作为 Readable 可读流,又可以作为 Writable 可写流。

Transform Streams 转换流

转换流是 Duplex 双工流的特例,内部可通过方法将作为可写流接收到的数据转换后传入可读流中,通常用于流转换,如压缩、加密等

Buffer

服务端常常需要处理网络、文件、数据库等等,Nodejs 开发语言是 JavaScript,而 JavaScript 没有简单的方式处理二进制数据,为了解决这个问题,Nodejs 提供了 Buffer 类,该类用于创建一个专门存放二进制数据的缓存区

二进制

二进制是计算技术中广泛采用的一种数制。二进制数据是用0和1两个数码来表示的数。它的基数为2,进位规则是“逢二进一”,借位规则是“借一当二”。

hello nodejs 转成二进制表示为:01101000 01100101 01101100 01101100 01101111 00100000 01101110 01101111 01100100 01100101 01101010 01110011

其中单个0或1称为位,8个二进制位为一个字节(byte)。

十进制

十进制,也称为基数为10的计数系统,是我们日常生活中最常用的数字系统。它基于位值的概念,每一位的值都是10的幂次方

hello nodejs 转成十进制表示为 104,101,108,108,111,32,110,111,100,101,106,115

十六进制

十六进制(Hexadecimal)是一种基数为16的计数系统,它使用16个符号来表示数值:0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F。其中A到F代表的值分别是10到15。十六进制在计算机科学中广泛使用,因为它可以简洁地表示二进制数。

hello nodejs 转成十六进制表示为 68 65 6c 6c 6f 6e 6f 64 65 6a 73

字符编码

字符编码是字符集与计算机的二进制数据相互转换的方法,是一种映射规则,

如今常见的字符编码主要有 ascii、base64、utf-x、gb、unicode等

ascii

ascii 是为了解决计算机之间数据互换的问题,统一的字符编码方案。ascii 码本身是一种十进制表示法,所以在ASCII 对照表中,十进制列就是ASCII码

示例:A:65

base64

由于二进制数据在不同系统之间传输时可能会出现乱码,需要一种编码方式将二进制数据转换为文本形式,所以Base64出现了Base64是网络上最常见的用于传输8Bit字节码的编码方式之一,常用于电子邮件、HTTP\MIME等协议中传输二进制数据

示例:hello nodejs 转成 base64 编码为:aGVsbG8gbm9kZWpz

unicode 字符集

由于 ASCII 码只适用于英语、数字和一些特殊符号,对于其他语言、符号等,无法适用于其它国家的语言字符,为了能够容纳世界上所有文字和符号,需要制定一个新的字符编码方案,Unicode 诞生

unicode 通常用 \uxxxx 来表示,他是unicode字符集的其中一个字符,不是一种字符编码

utf-8

由于UTF-16用两个字节编码英文字符,对于纯英文存储,对空间是一种极大的浪费,所以发明了UTF-8,它对于ASCII范围内的字符,编码方式与ASCII完全一致,其它字符则采用2字节、3字节甚至4字节的方式存储,所以UTF-8是一种变长编码。对于常见的中文字符,UTF-8使用3字节存储。

“你好” utf-8表示为: 你好

"hello" utf-8表示为:"hello"

以上的预备知识会贯穿整个编程生涯,希望每个程序员都能够牢记。

常用 API

Buffer 在 Nodejs 中无处不在,在上一章讲到的 Stream 流中,data 事件返回的都是 Buffer 对象。

let fs = require("fs");
let rs = fs.createReadStream("./test.txt");
let ws = fs.createWriteStream("./output.txt");
rs.on("data", (e) => {
  // 查看可读流数据 buffer 长度
  console.log(e.length); 
  // 将 buffer 写成 hello nodejs,长度为 12 位
  e.write("hello nodejs"); 
  // 将 buffer 写到 output.txt 文件中
  ws.write(e); 
});

output.txt 文件内容为:hello nodej,因为原来的 buffer 长度为 11 位,所以 nodejs最后一位的 s 无法写入

创建

let bf = Buffer.alloc(10);
console.log(bf) // 

得到的是填充指定长度的十六进制格式的缓冲区

从现有的数据创建 Buffer

let bf = Buffer.from('hello nodejs');
console.log(bf) // 

allocUnsafe 会创建一个未初始化的缓冲区,会跳过 alloc 方法中的清理和填充 0 的过程,缓冲区会被分配在可能包含旧数据的内容区域中

let bf = Buffer.allocUnsafe(10000);
console.log(bf.);

会得到随机旧数据

所以 allocUnsafe 仅被使用在你有很好的理由使用的情况下(例如,性能优化)使用。每当使用它时,请确保永远不在没有使用新数据填充完整它的情况下返回 buffer 实例,否则你可能会泄漏敏感的信息。

其他

API 和 JavaScript 数组与字符串类似, 举个例子

let buffer = Buffer.from("Hello World");
let buffer2 = Buffer.alloc(20);
// 写入buffer2
buffer2.write(" hello nodejs");
// 合并
let buffer3 = Buffer.concat([buffer, buffer2]);
// 获取buffer3的长度
console.log(buffer3.length);  // 11 + 20 = 31
// 截取buffer3的前5个字节
let subBuffer = buffer3.slice(0, 5);
// 输出截取后的内容
console.log(subBuffer.toString());  // "Hello"
// copy buffer3 to buffer4
let buffer4 = Buffer.alloc(buffer3.length);
buffer3.copy(buffer4);
// 输出buffer4的内容
console.log(buffer4.toString());  // "Hello World hello nodejs"
let buffer5 = Buffer.from("Hello World");
// 比较打印 buffer5 和 buffer 是否相等
console.log(buffer.equals(buffer5));  // true

Process 进程模块


上一条查看详情 +HBase中的Region拆分与合并经验总结
下一条 查看详情 +没有了