Node 基础笔记
模块
Node.js 在早期就采用了 CommonJS
规范实现了模块化,而 Web (浏览器) 中的 JS,在发展过程中诞生了 ECMAScript 标准
,在 ES6(ES2015) 中才推出了 ECMAScript Modules
规范。
CommonJS
CJS 模块使用 require
和 module.exports
实现导入和导出。
文件后缀:.cjs
| .js
导出
- module.exports【推荐】
- exports
exports
实际上是 module.exports
的一个引用,一旦将 exports 重新赋值,就会失效
// 导出一个名为 "hello" 的函数到 "exports" 对象中
// 函数中会将 "Hello World!" 的信息输出到控制台中
exports.hello = function () {
console.log("Hello World!");
};
// 等价于
module.exports.hello = function () {
console.log("Hello World!");
};
// 导致 exports 不再指向 module.exports
exports = {};
引入
- require
// 导入模块 "./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
// 文件 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
// 导入 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()
时,一个模块可能在未完成执行时被返回。
例如以下情况:
// 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.js
的 exports
对象的 未完成的副本 给 b.js
模块。 然后 b.js
完成加载,并将 exports
对象提供给 a.js
模块。
当 main.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 | 用于创建操作子进程 |
其它常用的工具模块 | url,Timers,Readline,crypto |
全局对象和变量
特殊变量
__filename
: 当前正在执行的脚本文件的绝对路径__dirname
:当前执行脚本所在目录的绝对路径
这 2 个变量,只在 CJS 模块下存在,如果是 ESM 将会出现以下的报错信息。
ReferenceError: __dirname is not defined in ES module scope
在 ESM 中可以使用createRequire(import.meta.url)
:
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 对象
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 对象
转换内容格式
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
// 创建一个长度为 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
const buf1 = Buffer.from("Hello");
const buf2 = Buffer.from("World");
const buf3 = Buffer.concat([buf1, buf2]);
console.log(buf3.toString()); // 输出 'HelloWorld'
截取 Buffer 对象
Buffer.slice
const buf = Buffer.from("Hello, world!");
const buf2 = buf.slice(0, 5);
console.log(buf2.toString()); // 输出 'Hello'
path 路径处理
拼接路径:
join
,resolve
;解析路径:
parse
,dirname
,basename
,extname
;规范化路径:
normalize
;获取分隔符:
sep
。区别
path.join -- 拼接相对路径
path.resolve -- 拼接绝对路径
path.dirname -- 返回路径目录名
path.basename -- 返回路径中的文件名,并可选地去除给定的文件扩展名
path.extname -- 获取文件拓展名
path.normalize -- 规范化路径,将路径中的不规范部分调整为标准格式(处理
//
或者./
和../
情况)path.parse -- 解析文件路径,将其拆分为一个对象
console.log(path.normalize("a//b//c/d/e/..")); // 规范化: a/b/c/d/e
FS 文件系统模块
fs 文件系统模块,用于操作文件和目录.
- 同步 (Sync):例如
fs.readFileSync
,会阻塞主线程; - 异步 (Async/Callback):
fs.promises.readFile
,fs.readFile
,不会阻塞主线程。
文件操作
文件读取
fs.readFileSync
基础用法如下:
- 参数 1:设置要读取的文件路径 (相对或者绝对);
- 参数 2:设置读取的编码格式。
const buf = fs.readFileSync("./test.txt");
// 打印Buffer大小
console.log(buf.length);
// 修改前2个字符
buf.write("gg");
// 输出修改后的内容
console.log(buf.toString());
文件写入
基础用法如下 fs.writeFileSync
:
参数 1:输出文件路径;
参数 2:输出内容;
参数 3 (可选):编码格式。
写入二进制文件 (读取一个图片,然后输出到一个新的位置)。
// 读取一个图片
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()
判断是否文件/目录
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
方法用于文件移动,当然也可以是重命名文件
fs.renameSync("test.txt", "test2.txt"); // 重命名
fs.renameSync("test2.txt", "test-dir/test2.txt"); // 移动
删除文件
fs.unlinkSync
和 fs.rmSync
都可用于单文件删除.
fs.unlinkSync("test-dir/test2.txt");
// fs.rmSync('test-dir/test2.txt')
fs.rmSync
支持删除目录, 递归删除子文件/目录等
// 删除 test-dir 目录(包含其子文件)
fs.rmSync("test-dir", { recursive: true });
目录操作
读取目录所有文件
通过 fs.readdirSync
获取目录下的文件信息。
默认情况下只会返回名称,指定第二个参数 withFileTypes:true
使返回结果包含类型。
const files2 = fs.readdirSync("test-dir", { withFileTypes: true });
console.log(
files2.map((f) => ({ name: f.name, isDirectory: f.isDirectory() }))
);
创建目录
使用 fs.mkdirSync
创建目录,可通过设置 recursive:true
来递归创建多级目录
fs.mkdirSync("test-dir/a/b/c/d", { recursive: true });
删除目录
可以使用 fs.rmdirSync
删除目标目录,recursive: true
表明删除包含其子目录。
监听目录变更
利用 fs.watch
即可实现
import fs from "fs";
// 监听当前目录下所有的文件和子目录中的文件
fs.watch("./", { recursive: true }, (eventType, filename) => {
console.log(`File '${filename}' has changed: ${eventType}`);
});
获取指定目录下所有文件路径
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
console.log(util.inspect(testObj, { depth: Math.Infinity }));
格式化字符串
util.format
支持占位符 (%s、%d、%j 等) 来表示不同类型的变量,支持传入多个参数
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 对象的方法。
// 引入 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
// 由于请求的目标资源是 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
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 服务。
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
直接提取。
const { url, method } = req;
const query = Object.fromEntries(new URL(url, "http://localhost").searchParams);
res.body
POST
请求通常会通过 body
传递数据。
fetch("http://127.0.0.1:4275?hello=world", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "xm",
age: 18,
}),
});
通过监听 data
和 end
事件获取
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
设置要向客户端返回额头信息。
res.statusCode = 404;
res.setHeader("Content-Type", "text/html");
可以通过 res.write
和 res.end
设置,终止并返回内容。
child_process 子进程
spawn
启动一个子进程来执行指定的命令,并且可以通过流式数据通信与子进程进行交互;exec
启动一个 shell,并在 shell 中执行指定命令,执行完毕后插入stdout/stderr
中,适用于一些命令行工具;execFile
与exec
类似,但是可以直接执行某个文件,而无需通过 shell 执行;fork
专门用于在 Node.js 中衍生新的进程来执行 JavaScript 文件,并且建立一个与子进程的 IPC 通信管道。
spawn 方法
支持 同步(spawnSync)
和 异步(spawn)
2 种调用方式。
比如我们经常用 pwd
获取当前目录的路径,ls
获取当前目录下的文件,下面是通过 spawnSync
调用示例。
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 方法
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 方法
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!"); // 向子进程发送消息