前生今世
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 版本为最新版本。
官网地址:nodejs.org/en/download...
安装后,正常会将 NodeJS 设置成环境变量 ,在控制台中输入 "node" 会出现像浏览器控制台类似的面板,则表示安装成功
包管理工具
npm 是 Nodejs 的包管理工具 ,可以帮助我们快速安装、管理、更新 Nodejs 相关的模块。 安装 Nodejs 通常会自动安装 npm, 安装成功后,在控制台输入 "npm -v" 会出现 npm 版本号,则表示安装成功。
package.json
package.json 用于描述项目的依赖项、版本号、描述信息等 。在项目构建初期,通过 npm init 命令可生成 package.json 文件。
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 仓库下载依赖项,默认 npmjs.org 下载,如设置 npm 镜像,则从镜像中下载,从镜像中获取获取。常用如:淘宝镜像、华为云镜像、腾讯云镜像等。
- 读取缓存 中是否拥有相同版本的依赖项,如果有,则直接使用缓存 ,如果没有,则下载安装。
- 压缩包解压安装到 node_modules 目录下。
本地安装与全局安装
本地安装:将依赖项安装到当前项目的 node_modules 目录下。 全局安装:将依赖项安装到全局目录下,可通过 npm root -g 进行查看全局目录路径,安装后可在命令行直接使用
arduino
npm install
npm install -g //全局安装
常用命令
sql
npm outdated 查看当前包与最新包的版本号
npm update 升级版本
npm update --save 升级并更新 package.json 文件中的依赖项版本号
npm install xx@latest 安装最新版本的 xx 包
...
版本规则
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 的内容,如模块未导出,则返回一个空对象。
举个例子:
ini
// 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 不允许直接赋值
javascript
// user.js
exports = { username: '小明', age: 10 }
// index.js
let user = require("./user");
console.log(user); // 相当于未导出,返回空对象 {}
导出的变量不会受到模块内部的影响
ini
// 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 加载规则
javascript
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 文件。
java
// 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 模块提供了对流的处理,包括读写操作。
举个例子,拷贝一张图片
ini
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() 方法进入流动状态
javascript
let rs = fs.createReadStream("./image.png");
rs.on("data", (e) => {
console.log(e)
})
rs.on("end", () => {
console.log('传输结束')
})
当可读流绑定了 data 事件,会进入流动状态,当数据源有数据可读时,会触发 data 事件,并将数据传递给监听器。
绑定 end 事件没有更多的数据可读时触发。
通过 Readable 类创建可读流
javascript
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 表明有了新数据,或到了流的尾部
ini
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 方法恢复写入。
ini
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 的组合
例子,复制图片
ini
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 对象。
ini
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 无法写入
创建
ini
let bf = Buffer.alloc(10);
console.log(bf) // <Buffer 00 00 00 00 00 00 00 00 00 00>
得到的是填充指定长度的十六进制格式的缓冲区
从现有的数据创建 Buffer
csharp
let bf = Buffer.from('hello nodejs');
console.log(bf) // <Buffer 68 65 6c 6c 6f 20 6e 6f 64 65 6a 73>
allocUnsafe 会创建一个未初始化的缓冲区,会跳过 alloc 方法中的清理和填充 0 的过程,缓冲区会被分配在可能包含旧数据的内容区域中
ini
let bf = Buffer.allocUnsafe(10000);
console.log(bf.);
会得到随机旧数据
所以 allocUnsafe 仅被使用在你有很好的理由使用的情况下(例如,性能优化)使用。每当使用它时,请确保永远不在没有使用新数据填充完整它的情况下返回 buffer 实例,否则你可能会泄漏敏感的信息。
其他
API 和 JavaScript 数组与字符串类似, 举个例子
ini
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 进程模块
在阅读前,强烈推荐阅读一遍 www.ruanyifeng.com/blog/2013/0...
进程是操作系统和一个核心概念,是对正在运行程序的一个抽象,被 CPU 执行。
任务管理器中拥有一个专门管理进程的模块。
Nodejs 运行一个程序后,会创建一个进程,Nodejs 的 Process 模块可以获取当前进程相关信息
process.env
返回包含用户环境的对象,包括:用户变量和系统变量
举个例子,在 Windows 系统变量中新建:test = 1111
打印process.env.test,可以看到输出为 1111
前端的 process.env
在写前端的过程中,常常用到该变量,最常见的是根据不同开发环境设置不同的常量,如后端请求地址、debug 开关等等
ini
let baseUrl = process.env.NODE_ENV === 'development'? 'xxxx' : 'xxxx';
process 是 Nodejs 环境下的变量,为什么在前端(基于打包工具)可以访问呢?因为在打包过程中,举个例子:webpack工具,webpack 的 definePlugin 插件会将代码中的变量替换成其他值,如代码中写了 process.env.BASE_URL,webpack会将其替换成在definePlugin插件设置的值,所以并没有访问到nodejs 的 process 模块
process.argv
返回一个数组 array, array[0] 是启动 Nodejs 进程的可执行文件的绝对路径名, array[1] 返回正在执行的 Javascript 文件路径,后面的是自定义命令行参数
例如执行以下命令:
ini
node ./index hello 123 NODE_ENV=production
返回
process.stdin、process.stdout
process.stdout 和 process.stdin 分别代表了标准输出和标准输入 Nodejs 中的console.log 由 process.stdout.write 实现
实现命令行功能
process.stdin 是可读流,拥有 data、readable 等可读流事件,data 可以监听命令行输入的数据 process.stdout 是可写流,拥有 write、writable 等可写流事件,write 可以向命令行输出数据
示例:实现一个简单的 cs 命令行工具,可以输入 cs -v 查看版本信息,cs -info 查看简介,cs -exit 退出程序
lua
process.stdout.write("欢迎来到 CustomShell");
process.stdout.write("\n\n>:");
process.stdin.setEncoding("utf-8");
process.stdin.on("data", (e) => {
let input = e.trim();
let eventMap = {
"cs -v": () => {
process.stdout.write("CustomShell v1.0.0");
process.stdout.write("\n\n>:");
},
"cs -info": () => {
process.stdout.write(
"欢迎来到Custom Shell \n\n CustomShell 是一个..................."
);
process.stdout.write("\n\n>:");
},
"cs -exit": () => {
process.stdin.emit("end");
},
};
if (input && eventMap[input]) {
eventMap[input]();
} else {
process.stdout.write(">:");
}
});
process.cwd()
返回进程工作目录的绝对路径
arduino
process.cwd() // d:\project\nodejs-document