Node深入淺出(聖思園教育) 002

9次閱讀

node 的包管理機制和加載機制

npm search xxx
npm view xxx
npm 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 的文件流基於核心模塊 streamfs.createReadStreamfs.createWriteStream 分別返回可讀、可寫流對象。它們不會一次性把內容加載進內存,而是在內部維護緩衝區(默認 64 KB)按需讀取或寫入,適合處理大文件或持續數據流。

  • 常見事件:open(文件描述符已就緒)、data(讀取到數據塊)、end(可讀流結束)、finish(可寫流刷新完畢)、error(出現錯誤)、close(釋放資源)。
  • 重要參數:
  • highWaterMark:緩衝區大小,用於控制背壓。
  • encoding:可讀流默認輸出 Buffer,可以設置默認字符編碼。
  • flagsmode:控制文件打開方式與權限。
  • 背壓(Backpressure):當寫入目標處理不過來時,可寫流會返回 false,可讀流應暫停直到觸發 drain 事件,內置的 pipestream/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 堆外的一塊原生內存,用於處理二進制數據。常見場景有文件讀寫、網絡通信、加密、壓縮等。BufferUint8Array 互通,Node 18+ 默認 Buffer 實例也繼承自 Uint8Array

  • 創建方式:
  • Buffer.from(string[, encoding])
  • Buffer.from(array|ArrayBuffer)
  • Buffer.alloc(size[, fill[, encoding]])
  • Buffer.allocUnsafe(size)(跳過初始化,性能高但需立即寫滿)
  • 常見編碼:utf8(默認)、base64hexlatin1ascii
  • 推薦搭配 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 主動連服務器,可設置 hostporttimeout 等。
  • net.Socket 既是可讀可寫流,常用事件 dataenderrorclose,常用方法 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 可以是 BufferUint8Array 或字符串;如果是字符串,可通過 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();
  }
});
  1. 先運行 node tcp-server.js,服務器監聽 5000 端口。
  2. 在第二個終端運行 node tcp-client.js,通過鍵盤發送任意消息。
  3. 輸入 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:*,客戶端打印回覆後關閉。
正文完
 0