Node 系列之预热知识(1)

前生今世

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:

  1. 文件系统I/O:涉及到文件的读写操作,例如读取文件内容、写入文件、删除文件等。
  2. 网络I/O:涉及到网络通信,例如HTTP请求和响应处理、TCP/UDP通信等。
  3. 数据库I/O:与数据库的交互,例如查询数据库、插入数据、更新数据等。
  4. 标准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 会发生什么

  1. 根据根目录 package.json 文件寻找依赖项
  2. npm 生成 package-lock.json
  3. 从 npm 仓库下载依赖项,默认 npmjs.org 下载,如设置 npm 镜像,则从镜像中下载,从镜像中获取获取。常用如:淘宝镜像、华为云镜像、腾讯云镜像等。
  4. 读取缓存 中是否拥有相同版本的依赖项,如果有,则直接使用缓存 ,如果没有,则下载安装
  5. 压缩包解压安装到 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 包
...

版本规则

blog.csdn.net/qq_37860930...

yarn、tyarn、pnpm、cnpm 区别

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

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

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

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

扩展

  1. 自己如何发布一个 npm 包?
  2. 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);

流分为四种:

  1. Readable 可读流
  2. Writable 可写流
  3. Duplex 可读可写流
  4. 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
相关推荐
理想不理想v4 小时前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
暮毅9 小时前
10.Node.js连接MongoDb
数据库·mongodb·node.js
~甲壳虫14 小时前
说说webpack中常见的Plugin?解决了什么问题?
前端·webpack·node.js
~甲壳虫15 小时前
说说webpack中常见的Loader?解决了什么问题?
前端·webpack·node.js
~甲壳虫15 小时前
说说webpack proxy工作原理?为什么能解决跨域
前端·webpack·node.js
熊的猫16 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
前端青山1 天前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
GDAL1 天前
npm入门教程1:npm简介
前端·npm·node.js
郑小憨1 天前
Node.js简介以及安装部署 (基础介绍 一)
java·javascript·node.js
lin-lins2 天前
模块化开发 & webpack
前端·webpack·node.js