前沿

nodejs是一个JavaScript的跨平台解释器,目的是run JavaScript everywhere,其中的LTS是long-term support(长期支持版本)

Nodejs:nodejs

安装后可以通过以下命令进行检查:

1
2
node -v
npm -v

都能在控制台中打印出版本信息即安装成功。

更换镜像源

查看当前镜像源

1
node get registry

设置淘宝镜像源

1
node config registry https://registry.npmmirror.com/

常用的内置模块

模块名 功能
url模块 解析url请求 返回一个解析对象
path模块 用户获取文件的路径
buffer模块 处理二进制数据,可以看为一个存储二进制的固定数组
fs模块 用于操作文件(读取、写入、删除、创建)
stream模块 数据流处理,支持读取和写入流(处理大文件较为有效)
os模块 获取操作系统的信息,如类型,架构,CPU等
http模块 用于构建Web服务器,处理http请求和响应等
crypto模块 提供加密和解密的功能,支持各种hash算法,对称加密和非对称加密等
util模块 提供一些函数工具,如格式化输出

在文件夹中的终端中输入以下指令来初始化一个nodejs项目

1
npm init

按照要求输入一系列的配置后会生成一个package.json文件。

1
2
3
4
5
6
7
8
9
10
11
12
{
"name": "demo",
"type": "module",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "gcnanmu",
"license": "ISC"
}

如果使用的是powerShell,会出现无法加载文件的报错,本质上是权限的原因(只有新版的nodejs会出现这个报错)

可以通过以下命令来查看当前的执行策略:

1
Get-ExecutionPolicy

可能出现下面四种情况:

  • Restructed 禁止运行所有脚本
  • AllSigned 需要有效的数字签名
  • RemoteSigned 允许运行本地创建的脚本 网上来源需要数字签名
  • Unrestricted 允许所有运行

更改策略为RemoteSigned即可。

1
Set-ExecutionPolicy RemoteSigned 

更换标准

假设此时我们想要使用url模块,我们可能会像下面这样写

1
2
3
import url from 'url';

url.parse('http://www.example.com?name=Tom&age=20', true).query;

但是一旦runcode,就会出现如下的报错:

(node:7332) Warning: To load an ES module, set “type”: “module” in the package.json or use the .mjs extension.
(Use node --trace-warnings ... to show where the warning was created)
d:\Web\drNode\demo.js:1
import url from ‘url’;
^^^^^^

SyntaxError: Cannot use import statement outside a module
at internalCompileFunction (node:internal/vm:73:18)
at wrapSafe (node:internal/modules/cjs/loader:1176:20)
at Module._compile (node:internal/modules/cjs/loader:1218:27)
at Module._extensions..js (node:internal/modules/cjs/loader:1308:10)
at Module.load (node:internal/modules/cjs/loader:1117:32)
at Module._load (node:internal/modules/cjs/loader:958:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
at node:internal/main/run_main_module:23:47

Node.js v18.15.0

这是由于不同的模块标准导致的。

JavaScript有两种模块体系:CommonJSES模块(ECMAScript模块)

CommonJS是Node.js最早采用的模块系统,使用require()module.exports来导入和导出模块。文件扩展名通常是.js

1
2
3
4
5
const url = require("url");

let urlParest = parse('http://www.example.com?name=Tom&age=20', true)
let times = 10;
module.exports = {urlParest,times}

ES模块是JavaScript的官方模块系统,使用importexport语法。文件扩展名通常是.mjs

1
2
3
4
5
import url from "url"

let urlParest = parse('http://www.example.com?name=Tom&age=20', true)
let times = 10;
export default {urlParest,times}

报错中给了两个解决方法:

  1. 将文件名改为.mjs

  2. package.json中添加

    1
    2
    3
    {
    "type":"module"
    }

考虑到与ES6的兼容性,建议使用第二种方法。

url模块

可以将url生成一个URL对象

1
2
const myUrl = new URL('https://example.com:8000/hello.html?id=100&status=active');
console.log(myUrl);

得到的结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
URL {
href: 'https://example.com:8000/hello.html?id=100&status=active',
origin: 'https://example.com:8000',
protocol: 'https:',
username: '',
password: '',
host: 'example.com:8000',
hostname: 'example.com',
port: '8000',
pathname: '/hello.html',
search: '?id=100&status=active',
searchParams: URLSearchParams { 'id' => '100', 'status' => 'active' },
hash: ''
}

可以看到生成了一个对象,其中的属性都可以使用.或者[]来获取。

也可以使用parse对url进行解析,得到的结果也是一样的(如果不使用true参数可能会显示已弃用)

1
2
3
4
5
6
7
import url from "url";

let urlParse = url.parse(
"https://example.com:8000/hello.html?id=100&status=active",
true
);
console.log(urlParse);

如果你获取本地的文件,可能会出现fileUrl的形式(使用file:///作为前缀),这时候使用url模块的fileURLToPath即可将其解析为本地文件路径链接。

1
2
3
4
console.log(import.meta.url); // file:///d:/Web/drNode/demo.js

let __fileName = url.fileURLToPath(import.meta.url);
console.log(__fileName); // d:/Web/drNode/demo.js

path模块

path模块的作用是对路径进行处理

首先要注意一下不同操作系统的路径规则是不同的,path模块也细分为两个领域:

  1. win32 windows系统 可以使用path.win32强制规定
  2. posix Linux系统 可以使用path.posix强制规定

实际上nodejs是会自己判断操作系统的类型,不需要自己设定

主要操作分为:

  • 获取文件名(带扩展名) path.basename
  • 获取所在文件夹路径 path.dirname
  • 获取扩展名 path.extname
  • 路径拼接 path.join
  • 路径解析 path.parse
  • 是否为绝对路径 path.isAbsolute
  • 转化为绝对路径 path.basename
  • 转化为相对路径 path.relative(参考路径,当前路径)
  • 路径合法化 path.normalize

一些操作展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import path from "path";
import url from "url";

let inputPath = url.fileURLToPath(import.meta.url);

let basename = path.basename(inputPath);
console.log(basename); // demo.js

let dirname = path.dirname(inputPath);
console.log(dirname); // d:\Web\drNode

let extname = path.extname(inputPath);
console.log(extname); // .js

// 路径拼接
let joinPath = path.join(dirname, basename);
console.log(joinPath); // d:\Web\drNode\demo.js

// 路径解析
let parsePath = path.parse(inputPath);
console.log(parsePath);

/*
{
root: 'd:\\',
dir: 'd:\\Web\\drNode',
base: 'demo.js',
ext: '.js',
name: 'demo'
}
*/

// 转化为绝对路径
let resolvePath = path.resolve(inputPath);
console.log(resolvePath); // d:\Web\drNode\demo.js

// 路径分隔符
console.log(path.sep); // /

// 是否为绝对路径
console.log(path.isAbsolute(inputPath)); // true

// 路径合法化
let normalizePath = path.normalize("/Users/username/Documents/demo///demo.js");

console.log(normalizePath); // /Users/username/Documents/demo/demo.js

// 路径相对化
let relativePath = path.relative(
"/Users/username/Documents/demo",
"/Users/username/Documents/demo/demo.js"
);
console.log(relativePath); // demo.js

let url1 = path.win32.parse("C:\\Users\\username\\Documents\\demo\\demo.js");
console.log(url1);
/*
{
root: 'C:\\',
dir: 'C:\\Users\\username\\Documents\\demo',
base: 'demo.js',
ext: '.js',
name: 'demo'
}
*/
let url2 = path.posix.parse("/Users/username/Documents/demo/demo.js");
console.log(url2);
/*
{
root: '/',
dir: '/Users/username/Documents/demo',
base: 'demo.js',
ext: '.js',
name: 'demo'
}
*/

Buffer模块

Buffer是用来处理二进制数据(0或者1)的模块,常见的操作是将字符串转化为二进制,将二进制转化为字符串(很多API返回的都是二进制数据)。

Base64编码是一种用于将二进制数据转换为文本字符串的编码方法。它的主要作用是使二进制数据能够以文本形式表示和传输,特别是在需要通过文本协议(如HTTP、SMTP等)传输二进制数据时非常有用。二进制与base64编码相互转化也是很常见的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 创建一个长度为10字节以0填充的Buffer
console.log(Buffer.alloc(10)); // <Buffer 00 00 00 00 00 00 00 00 00 00>
// 创建一个长度为10字节以1填充的Buffer
console.log(Buffer.alloc(10, 1)); // <Buffer 01 01 01 01 01 01 01 01 01 01>

let Name = "hello world";
let nameBuffer = Buffer.from(Name, "utf-8"); // utf-8编码
console.log(nameBuffer); // <Buffer 68 65 6c 6c 6f 20 77 6f 72 6c 64>

// 获取二进制数据的长度
console.log(Buffer.byteLength(nameBuffer)); // 11
console.log(nameBuffer.length); // 11

// 转化为字符串
console.log(Buffer.from(Name, "utf-8").toString("utf-8")); // hello world

// base64 编码常用于网络传输,用来处理二进制数据
// 二进制转化为base64编码
let base64 = Buffer.from("hello world").toString("base64"); //base64编码
console.log(base64); // aGVsbG8gd29ybGQ=
// 将base64编码转化为二进制
console.log(Buffer.from(base64, "base64")); // <Buffer 68 65 6c 6c 6f 20 77 6f 72 6c 64>

fs模块

在nodejs中,fs的操作模块分为两种模式:同步与异步。

例如写入文件,分为fs.writeFilefs.writeFileSync两种实现方式。两者的作用是相同的,但是writeFileSync是异步的实现方式,他不会因等待而阻塞,writeFileSync默认提供了回调函数的实现方式,其他的操作函数也同理。

1
2
3
4
5
6
7
8
9
10
11
import fs from "fs";

fs.mkdir("demo/css", { recursive: true })

fs.mkdirSync("demo/css", { recursive: true }, (err) => {
if (err) {
console.log(err);
} else {
console.log("Directory created successfully!");
}
});

JavaScript是解释性语言,他只能按从上到下的顺序一行一行执行代码,如果使用默认的回调函数是不符合日常编写程序的逻辑的,因此fs模块还提供了promise的实现方式,这可以使用同步的方式实现异步代码,这样程序的执行逻辑才更符合一般的编程习惯。

1
2
3
4
5
6
7
8
9
10
11
12
import fs from "fs";

const createDir = async (dir) => {
try {
await fs.promises.mkdir(dir, { recursive: true });
console.log("Directory created successfully!");
} catch (error) {
console.log(error);
}
};

createDir("demo/js");

因此更加推荐使用async的方法。

fs模块有以下的常见操作:

  • mkdir 创建目录,如果想要创建多级目录设置{recursive:true}
  • writeFile 覆盖写入 会覆盖之前的内容
  • appendFile 追加写入
  • readFile 读取文件,返回的是buffer对象,需要使用指定编码进行转化
  • access 判断文件或者目录是否存在
  • stat 获取文件或者目录的详细信息,返回一个对象
  • rename 目录或者文件重命名
  • unlink 删除文件 不存在会报错
  • rmdir 删除文件夹 不存在则报错
  • readdir 获取当前文件夹下的目录或者文件

一些操作的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
import fs, { Dir, Dirent } from "fs";

// 创建目录
const createDir = async (dir) => {
try {
// recursive是一个布尔值,指示是否应创建父目录。默认为false。
await fs.promises.mkdir(dir, { recursive: true });
console.log("Directory created successfully!");
} catch (error) {
console.log(error);
}
};

// createDir("demo/js");

// 写入文件
const writeFile = async (file, data) => {
try {
await fs.promises.writeFile(file, data);
console.log("File written successfully!");
} catch (error) {
console.log(error);
}
};

// writeFile("demo/js/hello.txt", "Hello, world!");

// 追加写入文件
const appendFile = async (file, data) => {
try {
await fs.promises.appendFile(file, data);
console.log("Data appended successfully!");
} catch (error) {
console.log(error);
}
};

// appendFile("demo/js/hello.txt", "\nHello, world again!");

// 读取文件
const readFile = async (file) => {
try {
// 如果不指定解码,则返回原始的buffer对象。
const data = await fs.promises.readFile(file, { encoding: "utf-8" });
// 或者使用toString()方法将其转化为字符串
console.log(data);
} catch (error) {
console.log(error);
}
};

// readFile("demo/js/hello.txt");

/*
fs.promises.constants.F_OK - 检查文件或者目录是否存在
fs.promises.constants.R_OK - 检查文件是否可读
fs.promises.constants.W_OK - 检查文件是否可写
fs.promises.constants.X_OK - 检查文件是否可执行

如果文件存在,则返回undefined,否则返回一个错误对象。
Error: ENOENT: no such file or directory, access 'demo/js' (如果文件不存在)
EExist: file already exists, access 'demo/js' (如果文件已存在)
EACCES: permission denied, access 'demo/js' (如果没有权限)
*/

const checkDir = async (dir) => {
try {
await fs.promises.access(dir, fs.promises.constants.F_OK);
console.log("Directory exists!");
} catch (error) {
console.log("Directory does not exist!");
}
};

// checkDir("");

// 获取文件的详细信息 也可以检测文件是否存在

const getFileInfo = async (file) => {
try {
const stats = await fs.promises.stat(file);
// console.log(stats);

if (stats.isFile()) {
console.log(`${file} is a file`);
}

if (stats.isDirectory()) {
console.log(`${file} is a directory`);
}
} catch (error) {
console.log(error);
}
};

// getFileInfo("demo/js/hello.txt");
// getFileInfo("demo/js/hello2.txt");

// 文件或者目录的重命名

const renameFile = async (oldPath, newPath) => {
try {
await fs.promises.rename(oldPath, newPath);
console.log("File renamed successfully!");
} catch (error) {
console.log(error);
}
};

// renameFile("demo/js/", "demo/newjs");
// renameFile("demo/js/hello.txt", "demo/js/hello.txt");

// 删除文件

const deleteFile = async (file) => {
try {
await fs.promises.unlink(file);
console.log("File deleted successfully!");
} catch (error) {
console.log(error);
}
};

// deleteFile("demo/js/hello.txt");

// 删除目录

const deleteDir = async (dir) => {
try {
// 如果recursive为true,则删除目录及其所有子目录和文件。默认为false。
await fs.promises.rmdir(dir, { recursive: true });
console.log("Directory deleted successfully!");
} catch (error) {
console.log(error);
}
};

// deleteDir("demo/js");

// 读取目录下的目录和文件

const readDir = async (dir) => {
try {
// const files = await fs.promises.readdir(dir);
// 允许递归读取目录
const files = await fs.promises.readdir(dir, { recursive: true });
// 读取目录下的目录和文件 通过withFileTypes选项指定 返回一个Dirent对象数组
// Dirent是一个类,用于表示文件系统目录中的条目。它包含文件或目录的名称以及其他信息。
// const files = await fs.promises.readdir(dir, { withFileTypes: true });
console.log(files);
} catch (error) {
console.log(error);
}
};

// readDir("demo");

// 读取目录下的目录 不能递归读取目录

const readDir2 = async (dir) => {
try {
const files = await fs.promises.opendir(dir);
for await (const file of files) {
console.log(file.name);
}
} catch (error) {
console.log(error);
}
};

readDir2("demo");

// 读取目录下的目录和文件 递归读取目录

const readDir3 = async (dir) => {
try {
const files = await fs.promises.opendir(dir);
for await (const file of files) {
if (file.isDirectory()) {
console.log(`Directory: ${file.name}`);
readDir3(`${dir}/${file.name}`);
} else {
console.log(`File: ${file.name}`);
}
}
} catch (error) {
console.log(error);
}
}

readDir3("demo");

流式操作

stream的作用是将数据按照设定好的大小一块一块的读取,而不是整个读入,这样可以节省内存,降低服务压力。

  • 对于读取流([fs.createReadStream](vscode-file://vscode-app/e:/microsoft vs code/resources/app/out/vs/code/electron-sandbox/workbench/workbench-apc-extension.html)),默认缓冲区大小为64KB(65536字节)。
  • 对于写入流([fs.createWriteStream](vscode-file://vscode-app/e:/microsoft vs code/resources/app/out/vs/code/electron-sandbox/workbench/workbench-apc-extension.html)),默认缓冲区大小也是16KB(16384字节)

可以通过单独设置highWaterMark属性来设定最大的模块大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import fs from "fs";

// 未使用stream处理
const writefile = async (path, data) => {
try {
await fs.promises.writeFile(path, data);
console.log("数据写入文件成功");
} catch (error) {
console.error(error);
}
};

// 使用流式写入 默认大小为16kb 16*1024
const writeFileStr = (path, data) => {
const writeStream = fs.createWriteStream(path, { highWaterMark: 1024 * 12}); // 设置为12kb

writeStream.on("error", (error) => {
console.log(error);
});

writeStream.on("finish", () => {
console.log("覆盖写入成功");
});

writeStream.write(data, "utf-8");
writeStream.end();
};

// writeFileStr("demo/js/hello2.txt", "你好");

// 流式追加写入
const appendFileStr = (path, data) => {
const appendFileStream = fs.createWriteStream(path, { flags: "a" });

appendFileStream.on("error", (error) => {
console.log(error);
});

appendFileStream.on("finish", () => {
console.log("追加写入文件成功");
});

appendFileStream.write(data, "utf-8");
appendFileStream.end();
};

// appendFileStr("demo/js/hello2.txt", "\n追加写入");

// 流式读取 默认大小为64kb 64*1024
const readFileStr = (path) => {
const readFileStream = fs.createReadStream(path);

let content = "";

readFileStream.on("data", (chunk) => {
console.log(chunk);
content += chunk.toString("utf8");
});

readFileStream.on("end", () => {
// console.log("读取文件成功");
console.log(content);
});

readFileStream.on("error", (error) => {
console.log(error);
});
};

readFileStr("demo/js/hello2.txt");

OS模块

OS模块常用于获取系统信息,常见的系统信息如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import os from "os"

console.log("内核版本:", os.version());
console.log("系统类型:", os.type());
console.log("系统架构:", os.arch());
console.log("主机名:", os.hostname());
console.log("系统平台:", os.platform());
console.log("系统发行版本:", os.release());

console.log("CPU核心数:", os.cpus().length);
console.log("内存信息:", os.totalmem());
console.log("空闲内存:", os.freemem());
console.log("用户目录:", os.homedir());
console.log("用户信息:", os.userInfo());

console.log("临时目录:", os.tmpdir());
console.log("系统运行时间:", os.uptime());
// console.log("网络接口信息:", os.networkInterfaces());

crypto模块

提供了加密和解密的功能

常见的加密算法有MD5,SHA_1,AES-GCM等,分为对称加密和非对称加密。对称与否在于解密时使用的密匙和加密是否相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import crypto from "crypto";

const md5 = (data) => {
const hash = crypto.createHash("md5"); // 创建MD5哈希对象
hash.update(data); // 更新哈希内容
return hash.digest("hex"); // 计算并生成十六进制的MD5哈希值
};

const sha1 = (data) => {
const hash = crypto.createHash("sha1"); // 创建SHA-1哈希
hash.update(data); // 更新哈希内容
return hash.digest("hex"); // 计算并生成十六进制的SHA-1哈希值
};

console.log("md5:", md5("Hello, world!")); // 6cd3556deb0da54bca060b4c39479839
console.log("sha1:", sha1("Hello, world!")); // 943a702d06f34599aee1f8da8ef9f7296031d699

// 生成随机的密匙
const randomBytes = (size) => {
// 将其转化为16进制字符串刚好是size的两倍
// 如果size是奇数,那么size/2就是小数,需要向上取整,确保不会丢失数据
let newSize = Math.round(size / 2);
// console.log("newSize:", newSize);
let buffer = crypto.randomBytes(newSize);
let hexStr = buffer.toString("hex");
return hexStr.slice(0, size); // 截取指定长度的字符串并返回
};
console.log("随机的秘钥:", randomBytes(16));

// 对称加密

// 生成一个随机的AES密钥
const generateAESKey = () => {
// 秘钥的长度必须是16、24、32字节,对应AES-128、AES-192、AES-256
return crypto.randomBytes(32);
};

// AES-GCM模式加密
const aesEncrypt = (data, key) => {
// nonce(Number used ONCE)是一个只能使用一次的随机数(AES-GCM的长度应为12字节)
// 用于确保每次加密的结果都是不同的,防止被破解 即使是相同的明文也会生成不同的密文
const nonce = crypto.randomBytes(12); // 生成一个随机的12字节的nonce

// 创建一个AES-GCM加密器
const cipher = crypto.createCipheriv("aes-256-gcm", key, nonce);

// 使用加密器对数据进行加密并将结果转化为十六进制字符串
// update 用于加密数据的主体部分
let encrypted = cipher.update(data, "utf8", "hex");

// 对最后一块数据进行加密
// final 用于加密数据的结尾部分 确保数据被完整加密
encrypted += cipher.final("hex");

// 获取认证标签 用于验证数据完整性和来源的真实性,防止数据被篡改
// 将其改为十六进制字符串
const authTag = cipher.getAuthTag().toString("hex"); // 获取认证标签

// 将nonce、加密后的数据和认证标签组合成一个JSON字符串
const encryJson = JSON.stringify({
nonce: nonce.toString("base64"),
encrypted: encrypted,
authTag: authTag,
});
return encryJson;
};

// AES解密
const aesDecrypt = (encrypted, key) => {
// 将JSON字符串解析为JSON对象
const encryJson = JSON.parse(encrypted);

// 从JSON对象中获取nonce、加密后的数据和认证标签
const nonce = Buffer.from(encryJson.nonce, "base64");
const encryptedData = encryJson.encrypted;
const authTag = Buffer.from(encryJson.authTag, "hex");

// 创建一个AES-GCM解密器
const decipher = crypto.createDecipheriv("aes-256-gcm", key, nonce);

// 设置认证标签
decipher.setAuthTag(authTag);

// 使用解密器对数据进行解密并将结果转化为UTF-8字符串
let decrypted = decipher.update(encryptedData, "hex", "utf8");

// 对最后一块数据进行解密
decrypted += decipher.final("utf8");

return decrypted;
};

// 生成随机的AES密钥
const key = generateAESKey();
// 对数据进行加密
const encrypted = aesEncrypt("Hello, world!", key);
console.log("encrypted:", encrypted); // {"nonce":"aJ+l/lIYtteJBeOO","encrypted":"5741338b2c5a9b98d3470ab803","authTag":"51236a320d094a11fdf6e8dd57481799"}
console.log("decrypted:", aesDecrypt(encrypted, key)); // Hello, world!

nodemon

nodemon用来监视node.js应用程序中的任何更改并自动重启服务,其中mon是monitor(监视)的缩写。

安装nodemon

1
npm i nodemon -g

其中涉及到全局安装和局部安装的区别:

  1. 全局安装可以在任何目录下使用
  2. 局部安装只能在当前目录下使用,均被安装到目录下的node_modules文件夹中

在命令行中通过以下命令进行使用

1
nodemon filename.js

如果想要在项目中配置npm run dev的方法运行nodemon filename.js,可以在package.json的script中添加如下的配置项:

1
2
3
4
"scripts": {
"dev":"nodemon index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},

之后可以通过npm run dev运行设定好的nodemon index.js命令

http模块

http模块是一个用于创建服务器与发送请求的模块,可以通过创建server来对所有的请求作出回应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import http from "http";

// 不要使用https模块,因为它需要证书,会被浏览器拦截

const port = 8000;
const hostname = "127.0.0.1";

const server = http.createServer((req, res) => {
// 设置响应头
res.writeHead(200, { "Content-Type": "text/plain" });
// res.write("Hello World");
// 返回响应数据
res.end("Hello World\n");
});

// 监听端口,创建服务器
server.listen(port, hostname, () => {
console.log(`server is running on http://${hostname}:${port}`);
});

以上程序通过http模块创建了一个server对象,这个对象会一直监听设定好的端口号,一旦有requset请求被发送到服务器,则按照写好的逻辑对请求进行回应。回应部分使用的是res.end()返回具体的响应体,如有必要,可以通过res.writeHead()设置请求头。使用npm run dev运行程序后,打开127.0.0.1:8000可以看到页面显示Hello World。

获取请求数据

在生产实际中,往往需要先根据请求所发送的信息来返回处理后的数据。

在server对象中,使用req来作为请求对象,可以获取req的属性(例如请求方式,请求体,请求头)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import http from "http";

// 不要使用https模块,因为它需要证书,会被浏览器拦截

const port = 8000;
const hostname = "127.0.0.1";

const server = http.createServer((req, res) => {

console.log(`${req.method} ${req.url}`);
// 获取请求头
console.log(req.headers.referer);
// 有效划线的时候不能使用点语法
console.log(req.headers["user-agent"]);

// 设置响应头
res.statusCode = 200;
// 告知请求的类型 否则不能正常显示
res.setHeader("Content-Type", "text/html", "charset=utf-8");
// 设置允许跨域
// res.setHeader("Access-Control-Allow-Origin", "*");
// 返回html数据
res.write("<h1>Hello World</h1>");
// res.write("Hello World");
res.end();
});

// 监听端口,创建服务器
server.listen(port, hostname, () => {
console.log(`server is running on http://${hostname}:${port}`);
});

访问127.0.0.1:8000后控制台得到的打印结果如下:

1
2
3
4
5
6
GET /
undefined
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0.0.0
GET /favicon.ico
http://127.0.0.1:8000/
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36 Edg/127.0

得到两次重复的打印结果,通过观察可以知道,这是因为每次请求的同时都会默认对网站图标进行请求。如果想要去掉这个请求可以通过req.url属性进行条件判断。

1
2
3
4
5
6
7
const server = http.createServer((req, res) => {
if (req.url == "/favicon.ico") {
return;
}

// 其余代码
}

这样就可以只得到一次的打印结果

使用URL获取请求参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import http from "http";

// 不要使用https模块,因为它需要证书,会被浏览器拦截

const port = 8000;
const hostname = "127.0.0.1";

const server = http.createServer((req, res) => {
// http://127.0.0.1:8000/?id=1&web=baidu.com
if (req.url === "/favicon.ico") {
return;
}
// console.log(`${req.method} ${req.url}`);

// 构造完整的url
let fullurl = `http://${hostname}:${port}${req.url}`;
console.log(`fullurl:${fullurl}`);

let urlObj = new URL(fullurl);
// console.log(urlObj);

// 获取查询参数
const queryObj = new URLSearchParams(urlObj.search);

// 获取查询参数
console.log(queryObj);
console.log("id:", queryObj.get("id"));
console.log("web:", queryObj.get("web"));

res.writeHead(200, {
"Content-Type": "Application/json",
charset: "utf-8",
// "Access-Control-Allow-Origin": "*",
});

// 返回得到参数的json数据并显示在页面上
let result = {
id: queryObj.get("id"),
web: queryObj.get("web"),
};
res.end(JSON.stringify(result));
});

// 监听端口,创建服务器
server.listen(port, hostname, () => {
console.log(`server is running on http://${hostname}:${port}`);
});

使用npm run dev可以在控制台看到整个URL对象的属性信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
URL {
href: 'http://127.0.0.1:8000/?id=1&web=baidu.com',
origin: 'http://127.0.0.1:8000',
protocol: 'http:',
username: '',
password: '',
host: '127.0.0.1:8000',
hostname: '127.0.0.1',
port: '8000',
pathname: '/',
search: '?id=1&web=baidu.com',
searchParams: URLSearchParams { 'id' => '1', 'web' => 'baidu.com' },
hash: ''
}

一般来说,我们需要的是searchParams这个属性值

有两种处理方法:

  1. 使用urlObj.searchParams获取
  2. 使用new URLSearchParams(urlObj.search)获取

之后可以在网页中看到如下信息:

1
2
3
4
{
"id": "1",
"web": "baidu.com"
}

readline模块

是一个控制台的模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

// 从控制台读取用户输入
// 控制台输入的内容是一个字符串
// 通过 process.stdin 流来读取用户输入
// 通过 process.stdout 流来输出内容
// 通过 createInterface 方法创建一个 readline.Interface 实例
// 通过 question 方法向用户提问
// 通过 close 方法关闭 readline.Interface 实例

import { createInterface } from "readline";
import fs from "fs";


const rl = createInterface({
input: process.stdin,
output: process.stdout,
});

rl.question("What do you think of Node.js? ", (answer) => {
if (
answer === "Yes" ||
answer === "yes" ||
answer == "y" ||
answer == "Y"
) {
console.log("You are right!");
} else {
console.log("You are wrong!");
}
console.log(`Thank you for your valuable feedback: ${answer}`);
rl.close();
});

// 监听 close 事件,当 readline.Interface 实例关闭时触发
rl.on("close", () => {
console.log("Readline interface closed!");
process.exit(0);
});

通过上述的代码,可以看到如下的输出:

1
2
3
4
5
6
7
8
9
10
(base) PS D:\Web\drNode> node "d:\Web\drNode\tempCodeRunnerFile.js"
What do you think of Node.js? yes
You are right!
Thank you for your valuable feedback: yes
Readline interface closed!
(base) PS D:\Web\drNode> node "d:\Web\drNode\tempCodeRunnerFile.js"
What do you think of Node.js? no
You are wrong!
Thank you for your valuable feedback: no
Readline interface closed!