node常用模块

144 阅读9分钟

前记

简单记录一下node中常用的几个模块

在node中默认是使用CommonJS模块化标准的, 也支持ESModule, 本文使用的是ESModule, 开启方式如下:

  • 使用.mjs文件代替.js, .mjs文件默认会使用ESModule
  • package.json文件中将type字段修改为module也可以

url

url模块可以用于解析url

parse 和 format

import * as url from "url";

const urlStr = "http://example.com:8080/one/two?a=1&b=2#hello";

// 将url转换为对象格式
const urlObj = url.parse(urlStr);

console.log(urlObj);
// Url {
//   protocol: 'http:',
//   slashes: true,
//   auth: null,
//   host: 'example.com:8080',
//   port: '8080',
//   hostname: 'example.com',
//   hash: '#hello',
//   search: '?a=1&b=2',
//   query: 'a=1&b=2',
//   pathname: '/one/two',
//   path: '/one/two?a=1&b=2',
//   href: 'http://example.com:8080/one/two?a=1&b=2#hello'
// }

// 可以对其拼接参数
urlObj.searchParams.append("c", "3");

// 或设置一些其他参数
urlObj.username = "hello";
urlObj.password = "123456";

// 使用 url.format 同样可以将其转换为url字符串 
console.log(url.format(urlObj)); // http://hello:123456@example.com:8080/one/two?a=1&b=2&c=3#hello


// url.format(urlObject: URL, options?: URLFormatOptions) 方法支持第二个配置参数,其ts类型定义如下: 
interface URLFormatOptions {
    auth?: boolean | undefined;
    fragment?: boolean | undefined;
    search?: boolean | undefined;
    unicode?: boolean | undefined;
}

URL构造函数

const myURL = new URL('/foo', 'https://example.org/');
console.log(myURL); // https://example.org/foo

fileURLToPath 和 pathToFileURL

import * as url from "url";

// file协议 转换为系统路径格式
console.log(new URL("file:///C:/path/").pathname); // 错误 /C:/path/
console.log(url.fileURLToPath("file:///C:/path/")); // 正确 C:\path\

// 系统路径格式转换为 file协议
console.log(new URL("file#1", "file:").href); // 错误 file:///file#1
console.log(url.pathToFileURL("file#1").href); // 正确 file:///F:/project/auxiliary-util/file%231

path

path模块可以用于处理文件路径

在介绍path模块前, 先介绍两个变量, __filename__dirname, __dirname用的比较多

  • __filename: 表示当前正在执行的脚本的文件名, 注意在ESModule下没有该变量(可以模拟出来)
  • __dirname: 表示当前正在执行的脚本的目录, 注意在ESModule下没有该变量(可以模拟出来)

CommonJS

console.log(__filename); // C:\Users\33153\Desktop\hello\index.js
console.log(__dirname); // C:\Users\33153\Desktop\hello

ESModule

import * as url from "url";
import * as path from "path";

const __filename = url.fileURLToPath(import.meta.url);
console.log(__filename); // C:\Users\33153\Desktop\hello\index.mjs

const __dirname = path.resolve();
console.log(__dirname); // C:\Users\33153\Desktop\hello

常用方法如下:

方法或属性名说明
sep作为路径段分隔符, 在 Windows 上是 ``, 在 Linux/macOS 上是 /
delimiter作为路径定界符, 在 Windows 上是 ;, 在 Linux/macOS 上是 :
basename()返回路径的最后一部分。 第二个参数可以过滤掉文件的扩展名
dirname()返回路径的目录部分
extname(path)返回路径中文件的后缀名, 即路径中最后一个.之后的部分 如果一个路径中并不包含.或该路径只包含一个. 且这个.为路径的第一个字符, 则此命令返回空字符串
isAbsolute()判断是否是绝对路径, 返回布尔值
parse()解析对象的路径为组成其的片段
format()path.parse()格式的对象转换为字符串路径
resolve([from ...], to)to 参数解析为绝对路径, 给定的路径的序列是从右往左被处理的, 后面每个 path 被依次解析, 直到构造完成一个绝对路径 会把 /\ 开头的字符串片段解析成根目录
join(path1, path2[, ...])用于连接路径, 主要用途在于, 会正确使用当前系统的路径分隔符, Unix系统是 / , Windows系统是**** 对 /\ 开头的字符串片段只返回自身(不会解析成根路径)
normalize(path)会尝试计算path实际的路径(归一化路径)
import * as path from "path";

const __dirname = path.resolve();

console.log(path.sep); // \
console.log(path.posix.sep); // /

console.log(path.delimiter); // ;
console.log(path.posix.delimiter); // :

console.log(path.basename("/test/something")); // something
console.log(path.basename("/test/something.txt")); // something.txt

console.log(path.dirname("/test/something"));  // /test
console.log(path.dirname("/test/something/file.txt"));  // /test/something

console.log(path.extname("index.js")); // .js
console.log(path.extname("./package.json")); // .json

console.log(path.isAbsolute("/test/something"));  // true
console.log(path.isAbsolute("./test/something"));  // false

const fileObj = path.parse("/abc/hello/test.txt");

console.log(fileObj);
// {
//   root: "/", // 根路径
//   dir: "/abc/hello", // 从根路径开始的文件夹路径
//   base: "test.txt", // 文件名 + 扩展名
//   ext: ".txt", // 文件扩展名
//   name: "test" // 文件名
// }


// 归一化
console.log(path.normalize("/users/joe/..//test.txt")); // \users\test.txt


console.log(path.resolve(__dirname, "./src/index.ts")); // F:\project\hello\src\index.ts

// resolve 最后参数以 ./开头 表示当前目录正常拼接
console.log(path.resolve("/bar/foo", "./baz")); // F:\bar\foo\baz

// resolve 最后的参数以 /开头 表示从根路径开始处理
console.log(path.resolve("/foo/bar", "/tmp/file/")); // F:\tmp\file
console.log(path.resolve("www", "images/png/", "/gif/image.gif")); // F:\gif\image.gif


// join 使用系统的连接符连接路径
console.log(path.join(__dirname, "./src/index.ts")); // F:\project\hello\src\index.ts
console.log(path.join("/bar/foo", "./baz")); // \bar\foo\baz
console.log(path.join("/foo/bar", "/tmp/file/")); // \foo\bar\tmp\file\
console.log(path.join("www", "images/png/", "/gif/image.gif")); // www\images\png\gif\image.gif

fs

fs模块可以用于操作系统文件

fs模块的API都分为同步和异步的(带Sync后缀的是同步), 而异步的回调函数的参数node统一约定了第一个参数为错误对象

方法名说明
writeFile(file, data[, options], callback)写入文件, file文件路径, data数据, options写入的一些配置, callback回调 Node写入不存在的文件时会创建对应的文件
readFile(path[, options], callback)文件读取, path读取文件路径, options读取的选项, callback回调 文件标识符
appendFile(path, data, callback)追加到文件, path文件路径, data追加的数据, callback回调
existsSync(path)检查一个路径是否存在
statSync(path)获取文件的状态, 返回一个对象, 对象包括文件的大小, 创建时间, 是否是文件或文件夹
unlinkSync(path)删除文件(不包括文件夹)
rmdirSync(path[, options])删除文件夹(不包括文件)
readdir(path[, options], callback)读取一个目录的目录结构, 回调会收到目录数组
truncataSync(path, len, callback)截断文件, 将文件修改为指定的大小, 单位是字节, 截断中文可能会会乱码(一个字占3个字节)
mkdirSync(path[, mode])创建一个文件夹(目录), mode可以为{ recursive: true }这样可以创建父目录
renameSync(oldPath, newPath)对文件进行重命名, oldPath旧的路径, newPath新的路径
watchFile(filename[, options], listener)监视文件的修改, filename要监视的文件, listener回调(当文件发生变化时调用), 参数为当前文件的状态, 修改前的文件状态
copyFile(path1, path2)复制文件

fs模块的API除了读取文件的内容格式需要注意一下, 其它对着文档用就行了

import * as fs from "fs";
import * as path from "path";

const __dirname = path.resolve();

const packPath = path.resolve(__dirname, "./package.json");
fs.readFile(packPath, ((err, data) => {
  if (err) {
    // 处理错误
  }

  console.log(data); // Buffer 数据
  console.log(data.toString()); // 使用 toString() 方法转换为字符串数据
  // 或者第二个可选的配置参数设置 encoding 为 "utf-8" 就会自动转换对应格式的数据
}));

stream

流, 理论知识可以见官方

使用流读取和写入文件

fs.createReadStream(path): 根据给定的路径创建一个可读流对象并返回(不存在对应的文件会报错)

可读流提供了一些事件, 来读取文件, 如下:

import * as fs from "fs";
import * as path from "path";

const __dirname = path.resolve();
const packPath = path.resolve(__dirname, "./package.json");

// 创建可读取
const rs = fs.createReadStream(packPath);

rs.on("open", () => {
  console.log("可读流打开了文件");
});

rs.on("data", (chunk) => {
  console.log("可读流读取文件内容");
  console.log(chunk.toString());
});

rs.on("end", () => {
  console.log("可读流读取完成");
});

rs.on("close", () => {
  console.log("可读流关闭了");
});

fs.createWriteStream: 根据给定的路径创建一个可写流对象并返回(不存在对应的文件会被创建)

可写流可以使用write()方法来向指定的文件中写入内容

import * as fs from "fs";
import * as path from "path";

const __dirname = path.resolve();
const testPath = path.resolve(__dirname, "./test.txt");

// 创建可写流
const ws = fs.createWriteStream(testPath);

// 通过 write方法可以同步的向可写流向文件中写入内容
ws.write("通过可写流写入的内容\r\n");
ws.write("通过可写流写入的第二次内容\r\n");
ws.write("最后一行就不写了");

// 关闭可写流
ws.end();

pipe()可读流对象有一个方法可以直接将可读流读取的内容输入到指定的可写流中

import * as fs from "fs";
import * as path from "path";

const __dirname = path.resolve();

// 可读流 
const rs = fs.createReadStream(path.resolve(__dirname, "./package.json"));
// 可写流
const ws = fs.createWriteStream(path.resolve(__dirname, "./package2.json"));

// 通过 pipe方法 将可读流传输到可写流里
rs.pipe(ws);

ws.write("write方法写入的数据永远在pipe方法传输的数据前面(同步)");

rs.on("end", () => {
  // 关闭流
  ws.end();
});

更多的流相关的知识可见官方

Buffer

数组中不能存储二进制文件, 而Buffer(缓冲区)就是专门用来存储二进制数据的

使用 Buffer 不需要引入模块, 在 Buffer 中存储的都是二进制数据,

但是在显示时是以16进制显示的, Buffer的操作方法和数组类似

字符串和Buffer的互相转换

const str = "hello buffer";

// 将字符串转换为 Buffer 数据
const buf = Buffer.from(str);
console.log(buf); // <Buffer 68 65 6c 6c 6f 20 62 75 66 66 65 72>

// 将 Buffer 数据转换为字符串
console.log(buf.toString()); // hello buffer (默认utf8)
console.log(buf.toString("hex")); // 68656c6c6f20627566666572
console.log(buf.toString("base64")); // aGVsbG8gYnVmZmVy

分配指定大小的Buffer

// 创建一个 1024 个字节大小的 Buffer 使用零进行初始化
const buf = Buffer.alloc(1024); 
console.log(buf); // <Buffer 00 00 00 00 ...>

Buffer的访问和操作

const str = "abcde";
const buf = Buffer.from(str);

// 下标操作访问
buf[0] = 65; // 对应 A

console.log(buf[1]); // 98

console.log(buf.length); // 5

// 循环
for (const b of buf) {
  console.log(b); // 输出对应的 Unicode 码
}

console.log(buf.toString()); // Abcde

// 会覆盖之前的数据
buf.write("ooo");
console.log(buf.toString()); // ooode

// 截取 Buffer
const newBuf = buf.slice(3, 5);
console.log(newBuf.toString()); // de


// 创建的一个空的 Buffer
const bufCopy = Buffer.alloc(4);
// 将 buf 复制到 Buffer
buf.copy(bufCopy);
console.log(bufCopy.toString()); // oood

crypto

crypto模块用于加解密数据

摘要算法

import * as crypto from "crypto";

const cipher = crypto.createHash("md5"); // 指定算法
/**
 * update方法, 可以多次调用
 * 第一个参数表示要加密的数据, 必须是字符串格式
 * 第二参数表示要传入数据的格式, 可以是"utf8", "binary", "ascii", "latin1", 默认: "utf8"
 * 第三个参数表示要加密数据的输出格式, 可以是"latin1",  "base64" 或者 "hex", 没有则返回Buffer
 */
cipher.update("hello");
cipher.update("world");

const data = JSON.stringify({ id: 1, name: "张三" });
cipher.update(data);

// 指定输出的加密格式, 
const md5 = cipher.digest("base64");
console.log(md5); // RChFS9qOBovOqHnadv2tRA==

hmc算法

import * as crypto from "crypto";

const secret = "密钥"
// 使用 createHmac方法, 指定为 sha256 算法, 然后给定一个密钥
const hmacsha256 = crypto.createHmac("sha256", secret);

// 加密数据
hmacsha256.update("data");
hmacsha256.update("append data1");
hmacsha256.update("append data2");

// 加密后的格式指定为 hex
const pwd = hmacsha256.digest("hex");
console.log(pwd); // 1433c412d4400ff24832a79305a5be8a4bdde65ff03042cc8e68e254d43fe17a

对称加密

import * as crypto from "crypto";

const dataJson = JSON.stringify({ id: 1, name: "张三" });
const key = Buffer.alloc(16); // 初始化密钥(16为字节)
const iv = Buffer.alloc(16); // 初始化向量(16为字节)

// 加密
const encrypt = (key, iv, data) => {
  try {
    const cipher = crypto.createCipheriv("aes-128-cbc", key, iv);
    // 加密数据格式为 utf8, 输出加密格式为 hex
    // 调用了 cipher.final() 方法,则 Cipher 对象就不能再用于加密数据, 多次调用 cipher.final() 将导致抛出错误 
    return cipher.update(data, "utf8", "hex") + cipher.final("hex");
  } catch (e) {
    console.log("加密失败");
    return e.message || e;
  }
}

// 加密数据
const hash = encrypt(key, iv, dataJson);
console.log(hash); // 30ea685209dd3fce13aa8e90a147d475ed42922aaff8faefd0cd2fb569dd0e25

// 解密
const decipher = (key, iv, crypted) => {
  try {
    const ciphed = crypto.createDecipheriv("aes-128-cbc", key, iv);
    // 解密数据格式为 hex, 输出解密格式为 utf8
    return ciphed.update(crypted, "hex", "utf8") + ciphed.final("utf8");
  } catch (e) {
    console.log("解密失败");
    return e.message || e;
  }
}
const dataStr = decipher(key, iv, hash);
console.log(dataStr); // {"id":1,"name":"张三"}

http

http模块可以开启一个http服务器

import * as http from "http";
const port = 8888;

// 创建一个 http 实例
http.createServer((req, res) => {
  // console.log("请求对象", req);
  // console.log("响应对象", res);
  
  const urlObj = new URL(req.url, `http:127.0.0.1:${port}`);
  
  if (["/", "/index"].includes(urlObj.pathname)) {
    // 响应
    res.end("hello index");
  } else {
    res.end("404 not found");
  }

  // 监听 8888 端口
}).listen(port, () => {
  console.log(`服务启动 ${port} 端口`);
});

配合zlib模块使用

import * as fs from "fs";
import * as http from "http";
import * as zlib from "zlib";

const port = 8888;

// 获取 gzip
const gzip = zlib.createGzip();

http.createServer((req, res) => {
  // 读取一个html文件
  const rs = fs.createReadStream("./index.html");

  // 指定响应头
  res.writeHead(200, {
    "Content-Type": "text/html; charset=utf-8", // 指定字符集不然会乱码
    "Content-Encoding": "gzip" // 指定压缩格式
  })

  rs.pipe(gzip) // 压缩后传输指定的流
    .pipe(res);
    
}).listen(port, () => {
  console.log(`服务启动 ${port} 端口`);
});

http模块涉及到很多的内容, 更多的可以见官方