Skip to content

Node 基础笔记

Mar 30, 2023
巷寓
19 min read

模块

Node.js 在早期就采用了 CommonJS 规范实现了模块化,而 Web (浏览器) 中的 JS,在发展过程中诞生了 ECMAScript 标准,在 ES6(ES2015) 中才推出了 ECMAScript Modules 规范。

CommonJS

CJS 模块使用 requiremodule.exports 实现导入和导出。

文件后缀:.cjs | .js

导出

  • module.exports【推荐】
  • exports

exports 实际上是 module.exports 的一个引用,一旦将 exports 重新赋值,就会失效

js
// 导出一个名为 "hello" 的函数到 "exports" 对象中
// 函数中会将 "Hello World!" 的信息输出到控制台中
exports.hello = function () {
  console.log("Hello World!");
};

// 等价于
module.exports.hello = function () {
  console.log("Hello World!");
};
// 导致 exports 不再指向 module.exports
exports = {};

引入

  • require
js
// 导入模块 "./exports" 并将其赋值给变量 context
const context = require("./exports");

// 导入模块 "./exports" 中的 hello, userInfo 和 byebye,并赋值给相应的变量
const { hello, userInfo, byebye: by } = require("./exports");

ES Modules

  • export default
  • export
  • import

导出

  • export default
  • export
js
// 文件 export_default.js
// 导出默认对象
export default {
  // 定义 hello 方法,输出欢迎信息
  hello(name) {
    console.log(`Hello, ${name}!`);
  },
  // 定义 byebye 方法,输出道别信息
  byebye(name) {
    console.log(`byebye, ${name}!`);
  },
  // 定义 userInfo 对象,存储用户信息
  userInfo: {
    name: "forever", // 用户名
    age: 18, // 用户年龄
  },
};

// 从 './lib.js' 中导出 hello 和默认导出并重命名为 libData
export { hello, default as libData } from "./lib.js";

导入

  • import

文件后缀:.mjs | .js

js
// 导入 export_all.js 中所有被导出的模块成员,并作为 allValues 对象的属性
import * as allValues from "./export_all.js";

// usage.js
import { hello, byebye, libData, utilData } from "./index.js";

区别

CJS 与 ESM 之间的一些区别,包括 加载时机导出内容文件命名 上的区别:

  • 加载时机:CJS 支持动态加载模块 (require 语句可以出现在任意位置),ESM 会在所有模块都加载完毕后才执行代码 (通常会将 import 导入语句放在模块的顶部);
  • 导出内容:ESM 导入的是值的引用,而 CJS 导入的是值的拷贝;
  • 文件命名:一般都以 .js 结尾,通过 package.json"type":"module" 区分模块加载类型,也可以通过文件命名来区分 .cjs 表明是 CJS 规范的模块,.mjs 表明是 ESM 规范的模块。

reqire 查找文件顺序

  • 缓存的模块优先级最高
  • 如果是内置模块,则直接返回,优先级仅次缓存的模块
  • 如果是绝对路径 / 开头,则从根目录找
  • 如果是相对路径 ./开头,则从当前 require 文件相对位置找
  • 如果文件没有携带后缀,先从 js、json、node 按顺序查找
  • 如果是目录,则根据 package.json 的 main 属性值决定目录下入口文件,默认情况为 index.js
  • 如果文件为第三方模块,则会引入 node_modules 文件,如果不在当前仓库文件中,则自动从上级递归查找,直到根目录

如何处理循环依赖

当循环调用 require() 时,一个模块可能在未完成执行时被返回。

例如以下情况:

js
// a.js
console.log('a 开始');
exports.done = false;
const b = require('./b.js');
console.log('在 a 中,b.done = %j', b.done);
exports.done = true;
console.log('a 结束');

// b.js
console.log('b 开始');
exports.done = false;
const a = require('./a.js');
console.log('在 b 中,a.done = %j', a.done);
exports.done = true;
console.log('b 结束');

// mian.js
console.log('main 开始');
const a = require('./a.js');
const b = require('./b.js');
console.log('在 main 中,a.done=%j,b.done=%j', a.done, b.done);

main.js 加载 a.js 时, a.js 又加载 b.js。 此时, b.js 会尝试去加载 a.js。 为了防止无限的循环,会返回一个 a.jsexports 对象的 未完成的副本 给 b.js 模块。 然后 b.js 完成加载,并将 exports 对象提供给 a.js 模块。

main.js 加载这两个模块时,它们都已经完成加载。 因此,该程序的输出会是:

js
$ node main.js
     main 开始
     a 开始
     b 开始
     在 b 中,a.done = false
     b 结束
     在 a 中,b.done = true
     a 结束
     在 main 中,a.done=true,b.done=true

常用内置模块

模块名称说明
global全局对象,挂载了一些常用方法和属性
path提供与文件路径相关的实用工具方法
fs文件系统模块,用于操作文件和目录
util提供一些实用工具函数
http用于创建 HTTP 服务器,也可用于向已有服务发起请求并获取响应
child_process用于创建操作子进程
其它常用的工具模块urlTimersReadlinecrypto

全局对象和变量

特殊变量

  • __filename : 当前正在执行的脚本文件的绝对路径
  • __dirname :当前执行脚本所在目录的绝对路径

这 2 个变量,只在 CJS 模块下存在,如果是 ESM 将会出现以下的报错信息。

sh
ReferenceError: __dirname is not defined in ES module scope

在 ESM 中可以使用createRequire(import.meta.url):

sh
import { fileURLToPath } from 'url'
import { dirname } from 'path'
import { createRequire } from 'module'

console.log('__filename', __filename)
console.log('__dirname', __dirname)

const require = createRequire(import.meta.url)
console.log('name', require('./package.json').name)

常用 global 属性

process 属性

process 对象提供了与当前 Node.js 进程相关的信息和控制方法,

  • process.argv 获取启动 Node 进程时传递的命令行参数。
  • process.cwd() 获取当前工作目录的绝对路径
  • process.env 获取当前执行环境的环境变量
  • process.version 获取当前 Node 版本
  • process.exit([statusCode]) : 终止 Node.js 进程,指定code 参数作为退出状态码
  • process.pid:返回进程的 PID (进程 ID);
  • process.platform:返回运行 Node.js 的操作系统平台;
  • process.arch:获取 CPU 架构信息
  • process.stdin:用于从标准输入流 (stdin) 读取数据
  • process.stdout:标准输出流,常用 process.stdout.write 进行数据写入。

Buffer

Buffer 用于处理二进制数据。类似于数组,并提供了一些方便的方法来操作二进制数据

创建 Buffer 对象

js
const buf = Buffer.alloc(10); // 创建一个大小为 10 的 Buffer 对象,默认会用 0 填充
const buf2 = Buffer.from("Hello, world!"); // 创建一个包含字符串 'Hello, world!' 的 Buffer 对象
const buf3 = Buffer.from([0x68, 0x65, 0x6c, 0x6c, 0x6f]); // 内容为hello构成的16进制数组 Buffer 对象

转换内容格式

js
const buf = Buffer.from("Hello, world!");
// 转为字符串输出
console.log(buf.toString()); // 输出 'Hello, world!'

// 转为16进制字符串输出
console.log(buf.toString("hex")); // 输出 '48656c6c6f2c20776f726c6421'(对应的是 'Hello, world!' 的 ASCII 码)

// 转为数组输出
console.log(Array.from(buf)); // 输出 [
//    72, 101, 108, 108, 111,
//    44,  32, 119, 111, 114,
//   108, 100,  33
// ]

// 转为base64格式输出
console.log(buf.toString("base64")); // 输出 'SGVsbG8sIHdvcmxkIQ=='

写入内容

write

js
// 创建一个长度为 10 的 Buffer 实例并将它填充为 0
const buf = Buffer.alloc(10);

// 将字符串 'Hello' 写入 Buffer 实例的前 5 个字节
buf.write("Hello");

// 将字符串 'world' 写入 Buffer 实例的第 6 个字节开始的位置,由于 'world' 的长度为 5,所以不会覆盖掉之前写入的 'Hello'
buf.write("world", 5);

// 将 Buffer 实例转换为字符串并输出 'Hello world'
console.log(buf.toString());

合并多个 Buffer 对象

Buffer.concat

js
const buf1 = Buffer.from("Hello");
const buf2 = Buffer.from("World");
const buf3 = Buffer.concat([buf1, buf2]);
console.log(buf3.toString()); // 输出 'HelloWorld'

截取 Buffer 对象

Buffer.slice

js
const buf = Buffer.from("Hello, world!");
const buf2 = buf.slice(0, 5);
console.log(buf2.toString()); // 输出 'Hello'

path 路径处理

  • 拼接路径:joinresolve

  • 解析路径:parsedirnamebasenameextname

  • 规范化路径:normalize

  • 获取分隔符:sep

    区别

  • path.join -- 拼接相对路径

  • path.resolve -- 拼接绝对路径

  • path.dirname -- 返回路径目录名

  • path.basename -- 返回路径中的文件名,并可选地去除给定的文件扩展名

  • path.extname -- 获取文件拓展名

  • path.normalize -- 规范化路径,将路径中的不规范部分调整为标准格式(处理//或者./../情况)

  • path.parse -- 解析文件路径,将其拆分为一个对象

js
console.log(path.normalize("a//b//c/d/e/..")); // 规范化: a/b/c/d/e

FS 文件系统模块

fs 文件系统模块,用于操作文件和目录.

  • 同步 (Sync):例如 fs.readFileSync,会阻塞主线程;
  • 异步 (Async/Callback):fs.promises.readFilefs.readFile,不会阻塞主线程。

文件操作

文件读取

fs.readFileSync 基础用法如下:

  • 参数 1:设置要读取的文件路径 (相对或者绝对);
  • 参数 2:设置读取的编码格式。
js
const buf = fs.readFileSync("./test.txt");

// 打印Buffer大小
console.log(buf.length);
// 修改前2个字符
buf.write("gg");

// 输出修改后的内容
console.log(buf.toString());

文件写入

基础用法如下 fs.writeFileSync

  • 参数 1:输出文件路径;

  • 参数 2:输出内容;

  • 参数 3 (可选):编码格式。

写入二进制文件 (读取一个图片,然后输出到一个新的位置)。

js
// 读取一个图片
const imgBuf = fs.readFileSync("./logo.png");
console.log("isBuffer", Buffer.isBuffer(imgBuf), "bufferSize", imgBuf.length);

// 写入到新文件
fs.writeFileSync("newLogo.png", imgBuf, "binary");

获取文件信息

通过 fs.statSync 获取文件或者目录的基本信息。通过 isFile() 和 isDirectory() 判断是否文件/目录

js
const fileInfo = fs.statSync("./test.txt");
// 判断是文件还是目录
console.log(fileInfo.isFile(), fileInfo.isDirectory());

const dirInfo = fs.statSync("./test-dir");
// 判断是文件还是目录
console.log(dirInfo.isFile(), dirInfo.isDirectory());

try {
  // 查询一个不存在的文件/目录信息(会抛出异常,需要自行捕获)
  fs.statSync("not_exist.txt");
} catch (e) {
  console.log("文件不存在");
}

移动/重命名文件

fs.renameSync 方法用于文件移动,当然也可以是重命名文件

js
fs.renameSync("test.txt", "test2.txt"); // 重命名

fs.renameSync("test2.txt", "test-dir/test2.txt"); // 移动

删除文件

fs.unlinkSyncfs.rmSync 都可用于单文件删除.

js
fs.unlinkSync("test-dir/test2.txt");
// fs.rmSync('test-dir/test2.txt')

fs.rmSync支持删除目录, 递归删除子文件/目录等

js
// 删除 test-dir 目录(包含其子文件)
fs.rmSync("test-dir", { recursive: true });

目录操作

读取目录所有文件

通过 fs.readdirSync 获取目录下的文件信息。

默认情况下只会返回名称,指定第二个参数 withFileTypes:true 使返回结果包含类型。

js
const files2 = fs.readdirSync("test-dir", { withFileTypes: true });
console.log(
  files2.map((f) => ({ name: f.name, isDirectory: f.isDirectory() }))
);

创建目录

使用 fs.mkdirSync 创建目录,可通过设置 recursive:true 来递归创建多级目录

js
fs.mkdirSync("test-dir/a/b/c/d", { recursive: true });

删除目录

可以使用 fs.rmdirSync 删除目标目录,recursive: true 表明删除包含其子目录。

监听目录变更

利用 fs.watch 即可实现

js
import fs from "fs";
// 监听当前目录下所有的文件和子目录中的文件
fs.watch("./", { recursive: true }, (eventType, filename) => {
  console.log(`File '${filename}' has changed: ${eventType}`);
});

获取指定目录下所有文件路径

js
import fs from 'fs'
import path from 'path'

function getAllFiles(dirPath, arrayOfFiles) {
  const files = fs.readdirSync(dirPath, { withFileTypes: true })

  arrayOfFiles = arrayOfFiles || []

  files.forEach((file) => {
    if (file.isDirectory()) {
      arrayOfFiles = getAllFiles(path.resolve(dirPath, file.name), arrayOfFiles)
    } else {
      arrayOfFiles.push(path.resolve(dirPath, file.name))
    }
  })

  return arrayOfFiles
}

// 获取当前目录下所有文件
console.log(getAllFiles('./')

util 工具模块

对象转字符串

util.inspect(object, [options])depth 用于控制展开的层级

或者使用库 javascript-stringify

js
console.log(util.inspect(testObj, { depth: Math.Infinity }));

格式化字符串

util.format支持占位符 (%s、%d、%j 等) 来表示不同类型的变量,支持传入多个参数

js
import util from "util";

console.log(util.format("%s:%s", "foo", "bar")); // 'foo:bar'
console.log(util.format("%d + %d = %d", 1, 2, 3)); // '1 + 2 = 3'

console.log(
  util.format("My name is %j", { firstName: "John", lastName: "Doe" })
);
// 'My name is {"firstName":"John","lastName":"Doe"}'

promise 转回调

util.callbackify 可以将一个返回 promise 的函数转为回调形式的函数。

回调转 promise

util.promisify(original) 用于将常规带有回调函数的方法转为返回 Promise 对象的方法。

js
// 引入 Node.js 内置模块 util 和 fs
import util from "util";
import fs from "fs";
// 将 fs.readFile 方法转换为返回 Promise 的函数
const fsRead = util.promisify(fs.readFile);
// 使用 Promise 的方式读取文件内容并输出
fsRead("./package.json").then((data) => {
  console.log(data.toString());
});

HTTP 模块

  • request:url (请求路径)、method (请求方法)、headers (请求头部)、body (请求体)、query (url 携带查询参数);
  • response:statusCode (响应状态码)、setHeader (设置响应头)、write/end (设置响应内容)。

发起请求

发起 get 请求

http.get 或者 https.get

js
// 由于请求的目标资源是 https 协议,所以这里使用 https 模块
https.get(
  "https://api.juejin.cn/content_api/v1/content/article_rank?category_id=1&type=hot&count=3&from=1&aid=2608&uuid=7145810834685003271&spider=0",
  (res) => {
    // 响应内容拼接
    let content = "";
    res.on("data", (chunk) => {
      content += chunk;
    });

    // 读完对外暴露内容和状态码
    res.on("end", () => {
      console.log(content);
    });

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

发起任意请求

http.request

js
import https from "https";

const url = new URL(
  "https://api.juejin.cn/content_api/v1/content/article_rank?category_id=1&type=hot&count=3&from=1&aid=2608&uuid=7145810834685003271&spider=0"
);
console.log(url);
const req = https.request(
  {
    // 设置请求方法
    method: "GET",
    // http 80 https 443
    port: 443,
    hostname: url.hostname,
    path: url.pathname + url.search,
  },
  (res) => {
    let content = "";
    res.on("data", (chunk) => {
      content += chunk;
    });
    res.on("end", () => {
      console.log("statusCode", res.statusCode);
      console.log(content);
    });
  }
);
// 发送请求
req.end();

创建 HTTP Service

利用 http.createServer 即可创建一个简单的 Web 服务。

js
import http from "http";

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader("Content-Type", "text/html");
  res.end("<h1>Hello, World!</h1>");
});
server.listen(4275, () => {
  console.log("Server running at http://127.0.0.1:4275/");
});

请求

req.url -- 请求路径

req.method -- 请求方法

req.headers -- headers 参数

req.query -- 获取 query 参数,以直接使用 URL 模块的 searchParams 直接提取。

js
const { url, method } = req;
const query = Object.fromEntries(new URL(url, "http://localhost").searchParams);

res.body

POST 请求通常会通过 body 传递数据。

js
fetch("http://127.0.0.1:4275?hello=world", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "xm",
    age: 18,
  }),
});

通过监听 dataend 事件获取

js
let body = [];
req
  .on("data", (chunk) => {
    body.push(chunk);
  })
  .on("end", () => {
    body = Buffer.concat(body).toString();
    body = JSON.parse(body);
    console.log("body", body);
  });

响应

通过 res.statusCode 设置响应的状态码。

通过 res.setHeader 设置要向客户端返回额头信息。

js
res.statusCode = 404;

res.setHeader("Content-Type", "text/html");

可以通过 res.writeres.end 设置,终止并返回内容。

child_process 子进程

  • spawn 启动一个子进程来执行指定的命令,并且可以通过流式数据通信与子进程进行交互;
  • exec 启动一个 shell,并在 shell 中执行指定命令,执行完毕后插入 stdout/stderr 中,适用于一些命令行工具;
  • execFileexec 类似,但是可以直接执行某个文件,而无需通过 shell 执行;
  • fork 专门用于在 Node.js 中衍生新的进程来执行 JavaScript 文件,并且建立一个与子进程的 IPC 通信管道。

spawn 方法

支持 同步(spawnSync)异步(spawn) 2 种调用方式。

比如我们经常用 pwd 获取当前目录的路径,ls 获取当前目录下的文件,下面是通过 spawnSync 调用示例。

js
import ChildProcess from "child_process";

const { spawn, spawnSync } = ChildProcess;

const pwd = spawnSync("pwd");
console.log(pwd.stdout.toString());
const ls = spawnSync("ls", ["-lh"]);
console.log(ls.stdout.toString());

exec 方法

js
import { exec, execSync } from "child_process";

const pwd = execSync("pwd");
console.log(pwd.toString());
const ls = execSync("ls -lh");
console.log(ls.toString());

const file = "./../fs/index.mjs";
const execProcess = exec(`git log -1 --pretty="%ci" ${file}`);
execProcess.stdout.on("data", (data) => {
  console.log(`stdout: ${data}`);
  console.log(new Date(data));
});

execFile 方法

执行某个可执行文件,支持同步和异步两种方式,

fork 方法

js
import { fork } from "child_process";

const child = fork("child.mjs"); // 使用 fork() 方法创建子进程

child.on("message", (msg) => {
  // 监听来自子进程的消息
  console.log(`Message from child: ${msg}`);
});

child.send("Hello from parent!"); // 向子进程发送消息

Drama is a song written by IU.