node 的包管理机制和加载机制
npm search xxxnpm view xxxnpm install xxx
nodejs 文件系统操作的 api
Node.js 的 fs 模块提供同步(Sync)与基于回调/Promise 的异步 API,可以操作本地文件与目录。日常开发中常用的能力包括读取、写入、追加、删除、遍历目录、监听变化等。以下示例基于 CommonJS 语法,若在 ES Module 中使用需要改成 import.
常用 API 速览
fs.readFile / fs.promises.readFile:一次性读取文件内容。fs.writeFile / fs.promises.writeFile:写入覆盖文件,自动创建不存在的文件。fs.appendFile / fs.promises.appendFile:在文件末尾追加内容。fs.mkdir / fs.promises.mkdir:创建目录,可级联创建。fs.readdir / fs.promises.readdir:读取目录下的文件名列表。fs.stat / fs.promises.stat:查看文件/目录详细信息(大小、类型、权限等)。fs.access / fs.promises.access:检查路径是否存在以及是否具备指定权限。fs.realpath / fs.promises.realpath:获取符号链接解析后的绝对路径。fs.unlink / fs.promises.unlink:删除文件。fs.rm / fs.promises.rm:删除文件或目录,可配合recursive/force。fs.watch:监听目录或文件的变化。fs.createReadStream / fs.createWriteStream:流式读写适合大文件或管道。
读取与写入
const fs = require('node:fs/promises');
async function readAndWrite() {
const content = await fs.readFile('./data.txt', 'utf8');
console.log('原始内容:', content);
await fs.writeFile('./output.txt', content.toUpperCase(), 'utf8');
await fs.appendFile(
'./output.txt',
'\n-- appended at ' + new Date().toISOString()
);
}
readAndWrite().catch(console.error);
目录遍历与详情
const fs = require('node:fs/promises');
const path = require('node:path');
async function listDir(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
const stats = await fs.stat(fullPath);
console.log({
name: entry.name,
isDirectory: entry.isDirectory(),
size: stats.size,
modified: stats.mtime,
});
}
}
listDir('./logs').catch(console.error);
确保目录存在
const fs = require('node:fs/promises');
async function ensureDir(dir) {
await fs.mkdir(dir, { recursive: true }); // 嵌套的方式创建目录
}
ensureDir('./uploads/images').catch(console.error);
权限检查 fs.access
fs.access(path[, mode]) 可用于在实际读写前检查目标路径是否存在以及调用进程对其拥有的权限。mode 默认为 fs.constants.F_OK(仅检测存在性),也可以按位组合 R_OK(可读)、W_OK(可写)、X_OK(可执行)。异步回调用约定是“无错即通过”,Promise 版本会在校验失败时抛出 ENOENT(不存在)、EACCES(无权限)等错误。
const fs = require('node:fs/promises');
async function ensureWritableConfig() {
try {
await fs.access('./config/app.json', fs.constants.R_OK | fs.constants.W_OK);
console.log('配置文件存在且可读写');
} catch (err) {
if (err.code === 'ENOENT') {
console.log('文件不存在,准备创建...');
await fs.writeFile('./config/app.json', '{}');
return;
}
throw err; // 由调用方决定是否提示权限不足等
}
}
ensureWritableConfig().catch((err) => {
console.error('权限检查失败:', err);
});
注意:
fs.access只能反映检查瞬间的状态,紧接着的真实读写仍可能因为条件变化而失败,因此对关键写操作仍需捕获错误。
解析实际路径 fs.realpath
fs.realpath(path[, options]) 会解析相对路径、符号链接、. / .. 段等内容,返回规范化后的绝对路径。默认以 UTF-8 字符串形式返回,可通过 options.encoding 设为 'buffer' 得到 Buffer。Promise 版本会在路径不存在(ENOENT)或链接循环(ELOOP)时抛出错误。
const fs = require('node:fs/promises');
async function resolveUpload(pathLike) {
const resolved = await fs.realpath(pathLike);
if (!resolved.startsWith('/var/www/uploads')) {
throw new Error('访问越界');
}
return resolved;
}
resolveUpload('./uploads/../uploads/avatar.jpg')
.then((absPath) => console.log('真实路径:', absPath))
.catch(console.error);
fs.realpath.native使用操作系统提供的原生实现,可能在某些平台更快但行为略有差异(尤其在 Windows UNC 路径上),除非有性能瓶颈一般优先常规版本。
删除文件与目录 fs.rm
fs.rm(target[, options]) 是 Node 14.14+ 推荐的删除 API,可删除单个文件、符号链接,也能在配置 options.recursive === true 时删除非空目录。常见选项:
recursive:默认为false,设为true即会递归删除目录树。force:忽略不存在的路径(不抛ENOENT)并尽量继续删除无法访问的文件,默认false。maxRetries/retryDelay:在 Windows 上处理句柄占用时可自动重试。
const fs = require('node:fs/promises');
async function cleanUploadTmp() {
await fs.rm('./uploads/tmp', {
recursive: true,
force: true, // 不存在也不报错
});
console.log('临时目录已清理');
}
cleanUploadTmp().catch((err) => {
console.error('删除失败:', err);
});
历史的
fs.rmdir(path, { recursive: true })已被弃用,建议统一使用fs.rm;在删除后续会重建的目录时,若存在并发写操作,应结合fs.mkdir的错误处理避免竞态。
重命名与移动文件
fs.rename / fs.promises.rename 可以在同一文件系统内对文件或目录进行重命名,目标路径可以包含新的目录结构(若目录不存在需提前创建)。
const fs = require('node:fs/promises');
const path = require('node:path');
async function renameLog() {
const src = path.resolve('./logs/app.log');
const destDir = path.resolve('./logs/archive');
await fs.mkdir(destDir, { recursive: true });
const dest = path.join(destDir, `app-${Date.now()}.log`);
await fs.rename(src, dest);
console.log(`已移动到: ${dest}`);
}
renameLog().catch((err) => {
if (err.code === 'ENOENT') {
console.error('源文件不存在');
return;
}
console.error('重命名失败:', err);
});
fs.rename 在不同磁盘或分区之间移动文件可能失败(EXDEV),此时应使用流或 fs.copyFile + fs.unlink 组合来实现复制后删除。
流式处理大文件
const fs = require('node:fs');
const path = require('node:path');
function copyLargeFile(src, dest) {
return new Promise((resolve, reject) => {
const readable = fs.createReadStream(src);
const writable = fs.createWriteStream(dest);
readable.on('error', reject);
writable.on('error', reject);
writable.on('finish', resolve);
readable.pipe(writable);
});
}
copyLargeFile(path.resolve('videos/big.mp4'), path.resolve('backup/big.mp4'))
.then(() => console.log('复制完成'))
.catch(console.error);
文件流详解
Node.js 的文件流基于核心模块 stream,fs.createReadStream 与 fs.createWriteStream 分别返回可读、可写流对象。它们不会一次性把内容加载进内存,而是在内部维护缓冲区(默认 64 KB)按需读取或写入,适合处理大文件或持续数据流。
- 常见事件:
open(文件描述符已就绪)、data(读取到数据块)、end(可读流结束)、finish(可写流刷新完毕)、error(出现错误)、close(释放资源)。 - 重要参数:
highWaterMark:缓冲区大小,用于控制背压。encoding:可读流默认输出 Buffer,可以设置默认字符编码。flags、mode:控制文件打开方式与权限。- 背压(Backpressure):当写入目标处理不过来时,可写流会返回
false,可读流应暂停直到触发drain事件,内置的pipe与stream/promises.pipeline会帮你处理。
逐块读取文件并统计字节数
const fs = require('node:fs');
function inspectFile(path) {
return new Promise((resolve, reject) => {
let total = 0;
const reader = fs.createReadStream(path, { highWaterMark: 16 * 1024 });
reader.on('open', (fd) => {
console.log('文件描述符:', fd);
});
reader.on('data', (chunk) => {
total += chunk.length;
console.log('读取块大小:', chunk.length);
});
reader.on('end', () => {
console.log('读取结束,总字节数:', total);
resolve(total);
});
reader.on('error', (err) => {
console.error('读取失败', err);
reject(err);
});
});
}
inspectFile('./logs/app.log').catch(console.error);
使用 pipeline 串联转换与写入
const fs = require('node:fs');
const zlib = require('node:zlib');
const { pipeline } = require('node:stream/promises');
async function compressLog() {
await pipeline(
fs.createReadStream('./logs/app.log', { encoding: 'utf8' }),
zlib.createGzip({ level: 9 }),
fs.createWriteStream('./logs/app.log.gz')
);
console.log('压缩完成');
}
compressLog().catch(console.error);
pipeline 内置背压处理和错误冒泡,推荐在复杂流组合时使用。处理二进制文件或音视频时,可以改为处理 Buffer,不设置编码即可。
监听文件变化
const fs = require('node:fs');
const watcher = fs.watch('./config.json', (eventType, filename) => {
console.log('文件变化:', eventType, filename);
});
process.on('SIGINT', () => {
watcher.close();
console.log('监听已停止');
});
Promise 风格(.then/.catch)写法示例
如果不想使用 async/await,可以直接对 fs.promises 返回的 Promise 链式调用:
const fs = require('node:fs/promises');
fs.readFile('./input.txt', 'utf8')
.then((text) => {
console.log('读取成功:', text);
return fs.writeFile('./result.txt', text.trim() + '\nProcessed');
})
.then(() => fs.stat('./result.txt'))
.then((stats) => {
console.log('写入完成,文件大小:', stats.size);
})
.catch((err) => {
console.error('操作失败:', err);
});
多个操作需要并行时,配合 Promise.all:
const fs = require('node:fs/promises');
Promise.all([
fs.readFile('./a.txt', 'utf8'),
fs.readFile('./b.txt', 'utf8'),
fs.readFile('./c.txt', 'utf8'),
])
.then(([a, b, c]) => fs.writeFile('./merged.txt', [a, b, c].join('\n')))
.then(() => console.log('并行读取并合并完成'))
.catch((err) => console.error('并行操作失败:', err));
提示:处理大量异步文件操作时,可结合
Promise.all或任务队列限制并发,避免同时打开过多文件描述符导致EMFILE错误。
文件流的字符流与二进制流对照
在 Java 中会明确区分“字符流(Reader/Writer)”与“字节流(InputStream/OutputStream)”。Node.js 中没有单独的字符流类,所有文件流本质上都是字节流(基于 Buffer)。是否表现为“字符”取决于是否设置了编码。以下示例展示两种常见模式:
文本流(指定编码)
const fs = require('node:fs');
const textReader = fs.createReadStream('./poem.txt', {
encoding: 'utf8', // 指定编码后 data 事件直接得到字符串
});
textReader.on('data', (chunk) => {
console.log('文本块:', chunk);
});
textReader.on('end', () => {
console.log('文本读取完成');
});
encoding 只影响读取出来的数据形态,不会改变底层 Buffer 的读取方式。没有设置编码时,chunk 会是 Buffer 对象。
二进制流(默认 Buffer)
const fs = require('node:fs');
const binaryReader = fs.createReadStream('./images/logo.png'); // 不设置 encoding
const chunks = [];
binaryReader.on('data', (chunk) => {
chunks.push(chunk);
});
binaryReader.on('end', () => {
const buffer = Buffer.concat(chunks);
console.log('PNG 头部签名:', buffer.slice(0, 8));
});
对于二进制数据,通常保持 Buffer 形式处理或写入其他可写流(如网络、压缩流)。
写入字符与二进制
const fs = require('node:fs');
// 写入文本,指定 UTF-8 编码
const textWriter = fs.createWriteStream('./output/hello.txt', {
encoding: 'utf8',
});
textWriter.write('你好,世界\n');
textWriter.end();
// 写入原始字节
const binaryWriter = fs.createWriteStream('./output/raw.bin');
binaryWriter.write(Buffer.from([0x00, 0xff, 0x10, 0x7a]));
binaryWriter.end();
总结:Node.js 文件流默认处理字节,借助编码即可模拟“字符流”效果;处理大对象或需要精准控制字节时保持 Buffer 更安全。
Buffer 模块详解
Buffer 是 Node.js 在 V8 堆外的一块原生内存,用于处理二进制数据。常见场景有文件读写、网络通信、加密、压缩等。Buffer 与 Uint8Array 互通,Node 18+ 默认 Buffer 实例也继承自 Uint8Array。
- 创建方式:
Buffer.from(string[, encoding])Buffer.from(array|ArrayBuffer)Buffer.alloc(size[, fill[, encoding]])Buffer.allocUnsafe(size)(跳过初始化,性能高但需立即写满)- 常见编码:
utf8(默认)、base64、hex、latin1、ascii。 - 推荐搭配
TextEncoder/TextDecoder做更细粒度的字符处理。
创建与编码转换
const bufUtf8 = Buffer.from('Node.js', 'utf8');
const bufHex = Buffer.from('e4bda0e5a5bd', 'hex'); // “你好”
console.log(bufUtf8); // <Buffer 4e 6f 64 65 2e 6a 73>
console.log(bufHex.toString('utf8')); // 你好
const base64 = bufUtf8.toString('base64');
console.log('Base64:', base64);
console.log('还原:', Buffer.from(base64, 'base64').toString('utf8'));
按字节写入与读取
const buf = Buffer.alloc(8);
buf.writeUInt16BE(0x1234, 0); // 大端
buf.writeUInt16LE(0x5678, 2); // 小端
buf.writeInt32BE(-1, 4);
console.log(buf); // <Buffer 12 34 78 56 ff ff ff ff>
console.log(buf.readUInt16BE(0)); // 4660
console.log(buf.readInt32BE(4)); // -1
切片、拷贝与拼接
const part1 = Buffer.from('Hello ');
const part2 = Buffer.from('World');
const full = Buffer.concat([part1, part2]);
console.log(full.toString()); // Hello World
const slice = full.slice(6); // 共用内存
console.log(slice.toString()); // World
const copyTarget = Buffer.alloc(5);
full.copy(copyTarget, 0, 6);
console.log(copyTarget.toString()); // World
Buffer 与 TypedArray 互操作
const arr = new Uint8Array([1, 2, 3, 4]);
const buf = Buffer.from(arr.buffer); // 共享底层 ArrayBuffer
buf[0] = 99;
console.log(arr[0]); // 99
const view = new Uint32Array(buf.buffer, buf.byteOffset, buf.byteLength / 4);
console.log(view); // Uint32Array(1) [...]
JSON 序列化与 base64 传输
Buffer 默认实现了 toJSON,因此 JSON.stringify(buffer) 会得到 { type: 'Buffer', data: [...] } 结构,反序列化后可以直接丢给 Buffer.from 还原:
const buffer = Buffer.from('你好世界');
const jsonString = JSON.stringify(buffer);
console.log(jsonString); // {"type":"Buffer","data":[228,189,160,229,165,189,228,184,150,231,149,140]}
const jsonObject = JSON.parse(jsonString);
console.log(jsonObject); // { type: 'Buffer', data: [ 228, 189, 160, 229, 165, 189, 228, 184, 150, 231, 149, 140 ] }
const buffer2 = Buffer.from(jsonObject);
console.log(buffer2.toString('utf8')); // 你好世界
在需要通过 JSON 通道传 Buffer 时可以配合 base64 降低体积(JSON 数组会显著增大体积):
const payload = Buffer.from(JSON.stringify({ id: 1, msg: 'hi' }), 'utf8');
const transport = payload.toString('base64');
// 接收方
const decoded = Buffer.from(transport, 'base64');
console.log(JSON.parse(decoded.toString('utf8'))); // { id: 1, msg: 'hi' }
注意:
Buffer.allocUnsafe创建的缓冲区包含旧内存数据,必须在写入后再使用;重复创建大量 Buffer 可能触发 GC 压力,可考虑复用或使用池化策略。
node 的网络模块
net 模块概述
net.createServer():创建 TCP 服务器实例,返回net.Server,通过connection事件拿到客户端socket。net.createConnection(options)/net.connect():客户端入口,建立net.Socket主动连服务器,可设置host、port、timeout等。net.Socket既是可读可写流,常用事件data、end、error、close,常用方法write()、end()、setEncoding()、setKeepAlive()等。server.address()、server.getConnections(cb)用于调试监听地址与连接数。
查看本地/远端连接信息
const net = require('net');
const server = net.createServer((socket) => {
console.log('local port:', socket.localPort);
console.log('local address:', socket.localAddress);
console.log('remote port:', socket.remotePort);
console.log('remote family:', socket.remoteFamily);
console.log('remote address:', socket.remoteAddress);
});
server.listen(8888, () => console.log('server is listening'));
socket.local* 属性表示当前服务器监听的端口/地址,socket.remote* 则指向客户端信息,调试多客户端接入或排查 NAT 问题很方便。
net 入门示例
// server.js
const net = require('net');
const server = net.createServer((socket) => {
console.log('client connected:', socket.remoteAddress, socket.remotePort);
socket.setEncoding('utf8');
socket.write('Hello from TCP server, type "bye" to quit.\n');
socket.on('data', (chunk) => {
const message = chunk.trim();
console.log('receive:', message);
if (message.toLowerCase() === 'bye') {
socket.end('Server closing connection.\n');
} else {
socket.write(`Server echo: ${message}\n`);
}
});
socket.on('end', () => console.log('client disconnected'));
socket.on('error', (err) => console.error('socket error:', err.message));
});
server.on('error', (err) => console.error('server error:', err.message));
server.listen(4000, () => {
const addr = server.address();
console.log(`TCP server listening on ${addr.address}:${addr.port}`);
});
// client.js
const net = require('net');
const client = net.createConnection({ host: '127.0.0.1', port: 4000 }, () => {
console.log('connected to server');
client.write('ping');
});
client.setEncoding('utf8');
client.on('data', (data) => {
console.log('server says:', data.trim());
if (data.includes('echo')) {
client.write('bye');
}
});
client.on('end', () => console.log('disconnected from server'));
client.on('error', (err) => console.error('client error:', err.message));
运行 node server.js 后再执行 node client.js 即可看到一问一答的交互流程。
nc(netcat)工具
nc 是类 Unix 系统常见的网络调试工具,可快速建立 TCP/UDP 连接,常用来测试端口监听、传输文本、转发流量。结合上面的服务端,可以在没有 Node 客户端时快速验证:
# 启动 server.js 后,用 nc 充当客户端
nc 127.0.0.1 4000
# 看到提示后输入文本,例如:
ping
hello
bye
nc 会把键盘输入以 TCP 流方式发送给服务器,非常适合排查 net 服务逻辑或协议格式,等价于一个轻量级 TCP 终端。
socket.write 使用说明
socket.write(chunk[, encoding][, callback]) 用于向对端发送数据,是 net.Socket 最常用的输出方法:
chunk可以是Buffer、Uint8Array或字符串;如果是字符串,可通过encoding(默认utf8)指定编码。- 返回值是布尔值,
false表示底层缓冲区已满,需要等待drain事件再写入,否则可能触发背压。 - 可选的
callback会在数据刷新到底层后调用,适合统计发送完成或错误处理。
常见写法如下:
socket.write('hello', 'utf8', (err) => {
if (err) {
console.error('send failed:', err);
return;
}
console.log('send success');
});
if (!socket.write(Buffer.from([0x01, 0x02]))) {
socket.once('drain', () => {
console.log('buffer drained, continue writing');
});
}
当需要结束连接时,可以使用 socket.end() 发送最后一块数据并触发 FIN,比单独 write() 后手动 destroy() 更优雅:
const net = require('net');
const server = net.createServer((socket) => {
socket.on('data', (msg) => {
if (msg.toString().trim() === 'bye') {
socket.end('Goodbye!\n'); // 发送最后一条消息并优雅关闭
return;
}
socket.write('Say "bye" to end connection.\n');
});
});
server.listen(4000, () => console.log('listening on 4000'));
客户端发送 bye 后,服务器立即返回 Goodbye! 并调用 socket.end(),底层 TCP 会完成 FIN/ACK 握手,远端 end 事件触发,连接正常关闭。
TCP 服务器/客户端完整示例
// tcp-server.js
const net = require('net');
const server = net.createServer((socket) => {
console.log(`new connection: ${socket.remoteAddress}:${socket.remotePort}`);
socket.setEncoding('utf8');
socket.write('Welcome! Type "quit" to close.\n');
socket.on('data', (chunk) => {
const msg = chunk.trim();
if (!msg) return;
if (msg.toLowerCase() === 'quit') {
socket.end('Bye!\n');
return;
}
socket.write(`Echo(${new Date().toLocaleTimeString()}): ${msg}\n`);
});
socket.on('end', () => console.log('client closed:', socket.remoteAddress));
socket.on('error', (err) => console.error('socket error:', err.message));
});
server.listen(5000, () => console.log('TCP server listening on port 5000'));
// tcp-client.js
const net = require('net');
const readline = require('readline');
const client = net.createConnection({ host: '127.0.0.1', port: 5000 }, () => {
console.log('connected to TCP server, type message then Enter');
});
client.setEncoding('utf8');
client.on('data', (data) => {
console.log(data.trim());
});
client.on('end', () => {
console.log('server closed connection');
rl.close();
});
client.on('error', (err) => console.error('client error:', err.message));
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
rl.on('line', (line) => {
client.write(line);
if (line.toLowerCase() === 'quit') {
rl.pause();
}
});
- 先运行
node tcp-server.js,服务器监听 5000 端口。 - 在第二个终端运行
node tcp-client.js,通过键盘发送任意消息。 - 输入
quit时客户端会发送终止命令,服务器调用socket.end()优雅关闭连接。
UDP 服务器/客户端完整示例
// udp-server.js
const dgram = require('dgram');
const server = dgram.createSocket('udp4');
server.on('message', (msg, rinfo) => {
console.log(`recv ${msg} from ${rinfo.address}:${rinfo.port}`);
const reply = Buffer.from(`ack:${msg.toString().toUpperCase()}`);
server.send(reply, rinfo.port, rinfo.address, (err) => {
if (err) console.error('send error:', err);
});
});
server.on('listening', () => {
const address = server.address();
console.log(`UDP server listening on ${address.address}:${address.port}`);
});
server.bind(41234);
// udp-client.js
const dgram = require('dgram');
const client = dgram.createSocket('udp4');
client.on('message', (msg) => {
console.log('server reply:', msg.toString());
client.close();
});
const payload = Buffer.from('hello udp');
client.send(payload, 41234, '127.0.0.1', (err) => {
if (err) {
console.error('send error:', err);
client.close();
return;
}
console.log('datagram sent');
});
- UDP 通过
dgram.createSocket创建无连接套接字,消息以数据报形式发送,可能丢失或乱序,不保证可靠性。 - 运行
node udp-server.js后再执行node udp-client.js,客户端发送一次数据报,服务器收到后立即回传ack:*,客户端打印回复后关闭。
主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://joyjs.cn/archives/4766