技术备忘 Node.jsJavaScript服务端

前言

Node.js 是一个基于 Chrome V81V8 是 Google 开发的高性能 JavaScript 和 WebAssembly 引擎,用于 Chrome 和 Node.js。它将 JS 代码编译为机器码执行,支持 JIT(即时编译)优化。 引擎的 JavaScript 运行时环境。它让前端开发者可以用 JavaScript 写后端代码,其事件驱动、非阻塞 I/O 的特性使得它在处理高并发 I/O 密集型场景时表现出色。

本文整理 Node.js 日常开发中最核心的概念和用法。

Node.js 与浏览器的区别

对比项Node.js浏览器
全局对象globalprocesswindowdocument
模块系统CommonJS / ESMES Module
DOMdocumentDOM API
I/O文件系统、网络、进程无文件系统,限制性网络
this 全局指向globalThis(空对象)window
ES 版本可控制运行时版本取决于用户浏览器

模块系统

Node.js 支持两种模块系统2CommonJS(require/module.exports)是 Node.js 原生模块系统,同步加载。ESM(import/export)是 ES6 标准,异步加载,支持静态分析和 Tree Shaking。

CommonJS (CJS)ECMAScript Modules (ESM)
默认扩展名.js / .cjs.mjs,或 package.json"type": "module"
导入require()import
导出module.exports / exportsexport / export default
加载方式同步异步
是否支持顶层 await支持(模块中)
js
// CommonJS
const fs = require('node:fs');
module.exports = { myFn };
// ESM
import fs from 'node:fs';
export const myFn = () => {};
NOTE

Node.js 官方推荐使用 ESM 作为新项目的默认选择。但 CommonJS 生态仍极其庞大,两者在 require()import() 间可以通过一些方式互操作。

核心模块 (Core Modules)

Node.js 内置了大量核心模块,无需安装即可使用:

js
// 推荐使用 node: 前缀,明确标识是核心模块
const fs = require('node:fs');
const path = require('node:path');
const http = require('node:http');
模块用途常用 API
node:fs文件系统操作readFilewriteFilecreateReadStream
node:path路径处理joinresolvebasenameextname
node:http / node:httpsHTTP 客户端和服务端createServerget
node:urlURL 解析URLURLSearchParams
node:os操作系统信息platformcpustotalmem
node:events事件发射器EventEmitter
node:stream流处理(Buffer3Buffer 是 Node.js 提供的二进制数据容器,用于处理 TCP 流、文件系统操作等场景。在 ES6 引入 TypedArray 前,Buffer 是处理二进制数据的主要方式。 vs Stream4Stream(流)是 Node.js 处理大数据的核心抽象,分块读写而非一次性加载到内存。四种类型:Readable、Writable、Duplex、Transform。ReadableWritableTransform
node:child_process子进程spawnexec
node:crypto加密createHashrandomBytes
node:util工具函数promisifycallbackify

事件循环(Event Loop)5事件循环是 Node.js 非阻塞 I/O 的核心机制,由 libuv 库实现。它将异步任务分阶段处理(timers → poll → check 等),在单线程中高效调度 I/O 操作、定时器和回调。

事件循环是 Node.js 非阻塞 I/O 的核心机制。

timers | setTimeout / setInterval

pending callbacks | 延迟到下一轮的 I/O 回调

idle, prepare | 内部使用

poll | 轮询 I/O(核心阶段)

check | setImmediate

close callbacks | 关闭事件:socket.on('close')

各阶段说明

阶段执行内容
timers执行 setTimeoutsetInterval 的回调
pending callbacks执行某些系统操作的回调(如 TCP 错误)
idle, preparelibuv6libuv 是 Node.js 的跨平台异步 I/O 库,提供事件循环、文件系统操作、网络 I/O、线程池等底层能力。不同操作系统的异步 API 差异由 libuv 屏蔽。 内部使用
poll获取新的 I/O 事件,执行 I/O 回调。如果队列为空,检查是否有 setImmediate → 走 check 阶段;否则等待
check执行 setImmediate 的回调
close callbacks执行关闭事件的回调(如 socket.destroy()

setImmediate vs setTimeout

setImmediatesetTimeout(fn, 0)
执行时机check 阶段timers 阶段
在 I/O 周期内总是先于 setTimeout晚于 setImmediate
在顶层调用取决于进程性能,顺序不确定取决于进程性能
js
// 在 I/O 周期内:immediate 总是先执行
const fs = require('node:fs');
fs.readFile(__filename, () => {
setImmediate(() => console.log('immediate')); // 先
setTimeout(() => console.log('timeout'), 0); // 后
});
// 在顶层(非 I/O 周期):顺序不确定
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));

process.nextTick7process.nextTick 在当前操作完成后、事件循环继续前立即执行,优先级高于 Promise 微任务。过度使用会导致 I/O 饥饿,推荐优先使用 setImmediate。

process.nextTick 技术上不属于事件循环的任何一个阶段,它会在当前操作完成后、事件循环继续前立即执行。

重要程度:process.nextTick > Promise > 事件循环各阶段
js
let bar = null;
function someAsyncApiCall(callback) {
process.nextTick(callback);
}
someAsyncApiCall(() => {
console.log('bar', bar); // 1 — nextTick 等到了变量初始化
});
bar = 1;
NOTE

process.nextTick 优先级最高,但递归调用可能导致 I/O 饿死。推荐优先使用 setImmediate,能更好地与事件循环配合。

微任务与宏任务

js
setTimeout(() => console.log('宏任务'), 0);
Promise.resolve().then(() => console.log('微任务'));
process.nextTick(() => console.log('nextTick'));
console.log('同步代码');
// 输出顺序:
// 同步代码 → nextTick → 微任务 → 宏任务
类型优先级示例
同步代码最高直接执行的语句
process.nextTick次高当前操作结束立即执行
微任务 (Microtask)Promise.then / catch / finally
宏任务 (Macrotask)setTimeout / setInterval / I/O

文件系统(fs)

Node.js 的 fs 模块提供三种操作风格:

js
const fs = require('node:fs');
const fsPromises = require('node:fs/promises');
风格函数名适用场景
回调fs.readFile(path, cb)旧代码,避免回调地狱8回调地狱指嵌套多层回调函数导致代码难以维护的现象。解决方案:Promise、async/await,或模块化拆分回调逻辑。
同步fs.readFileSync(path)启动时一次性加载配置
Promise9Promise 是 ES6 引入的异步编程解决方案,表示一个未来完成或失败的操作。状态不可逆:pending → fulfilled/rejected。支持链式调用(.then/.catch)。fsPromises.readFile(path)推荐,现代写法

推荐:Promise 风格

js
const fs = require('node:fs/promises');
async[^async-await] function readConfig() {
try {
const data = await fs.readFile('/path/to/config.json', 'utf8');
return JSON.parse(data);
} catch (err) {
console.error('读取配置失败:', err.message);
throw err;
}
}

同步风格(仅限启动时)

js
const fs = require('node:fs');
try {
const data = fs.readFileSync('./config.json', 'utf8');
const config = JSON.parse(data);
} catch (err) {
console.error(err);
}

核心方法速查

方法作用
readFile(path, options)读取整个文件
writeFile(path, data)写入文件(覆盖)
appendFile(path, data)追加内容到文件
mkdir(path, { recursive: true })创建目录
readdir(path)读取目录列表
stat(path)获取文件/目录信息
unlink(path)删除文件
rmdir(path)删除空目录
rm(path, { recursive: true })递归删除文件或目录
NOTE

对于大文件,不要使用 readFile 一次性读入内存。应该使用 Stream(见下一节)逐块处理。readFileSync 更是在服务器运行中应当避免使用,它会阻塞整个事件循环。

Stream(流)

Stream 是 Node.js 处理大规模数据的方式——分块处理,而非一次性加载到内存。

四种 Stream

类型用途示例
Readable读取数据fs.createReadStream、HTTP 请求
Writable写入数据fs.createWriteStream、HTTP 响应
Duplex可读可写TCP Socket
Transform转换数据zlib.createGzip

读文件示例(可读流)

js
const fs = require('node:fs');
const readStream = fs.createReadStream('large-file.log', { encoding: 'utf8' });
readStream.on('data', (chunk) => {
console.log('收到数据块:', chunk.length, '字节');
});
readStream.on('end', () => {
console.log('文件读取完成');
});
readStream.on('error', (err) => {
console.error('读取出错:', err.message);
});

pipeline(推荐)

旧的 .pipe() 方法在错误处理上有缺陷——一个流出错不会自动关闭其他流。应该使用 pipeline

js
const fs = require('node:fs');
const zlib = require('node:zlib');
const { pipeline } = require('node:stream/promises');
async function gzip(inPath, outPath) {
await pipeline(
fs.createReadStream(inPath),
zlib.createGzip(),
fs.createWriteStream(outPath)
);
console.log('压缩完成');
}
gzip('data.log', 'data.log.gz').catch(console.error);
js
// 回调风格
const { pipeline } = require('node:stream');
pipeline(
fs.createReadStream('input.txt'),
zlib.createGzip(),
fs.createWriteStream('input.txt.gz'),
(err) => {
if (err) console.error('Pipeline 失败', err);
else console.log('Pipeline 成功');
}
);
API错误处理推荐程度
stream.pipe()需手动处理错误❌ 不推荐
pipeline()自动清理所有流推荐
pipeline() + Promise自动清理,支持 await最推荐

网络编程

创建 HTTP 服务器

js
const http = require('node:http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('Hello World');
});
server.listen(3000, () => {
console.log('服务器已启动: http://localhost:3000');
});

请求对象(req)常用属性

属性说明
req.methodHTTP 方法:GET / POST / PUT / DELETE
req.url请求路径(含查询参数)
req.headers请求头对象
req.socket底层 TCP socket

响应对象(res)常用方法

方法/属性说明
res.writeHead(status, headers)设置状态码和响应头
res.setHeader(name, value)设置单个响应头
res.write(data)写入响应体(可多次调用)
res.end(data)结束响应
res.statusCode状态码

获取请求体

js
const http = require('node:http');
const server = http.createServer((req, res) => {
let body = '';
req.on('data', (chunk) => {
body += chunk; // Buffer 拼接
});
req.on('end', () => {
console.log('收到 body:', body);
res.end('OK');
});
});
server.listen(3000);
NOTE

Node.js 的 http 模块功能基础,实际项目中大多使用 Express / Fastify / Koa 等框架。但对于了解底层原理,自己写一个 http.createServer 很有帮助。

路径处理(path)

js
const path = require('node:path');
// 拼接路径
path.join('/usr', 'local', 'bin'); // /usr/local/bin
// 解析绝对路径
path.resolve('src', 'index.js'); // /当前目录/src/index.js
// 提取信息
path.basename('/a/b/index.js'); // index.js
path.dirname('/a/b/index.js'); // /a/b
path.extname('/a/b/index.js'); // .js
// 解析路径
path.parse('/a/b/index.js');
// { root: '/', dir: '/a/b', base: 'index.js', ext: '.js', name: 'index' }
// 跨平台分隔符
path.sep; // Unix: /, Windows: \
NOTE

使用 path.join 而不是手动拼接字符串(dir + '/' + file),因为前者能正确处理不同操作系统的路径分隔符。

事件发射器(EventEmitter)

Node.js 的很多核心模块都继承自 EventEmitter。理解它的工作方式对理解 Node.js 至关重要。

js
const EventEmitter = require('node:events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
// 注册监听器
myEmitter.on('event', (arg) => {
console.log('事件触发:', arg);
});
// 一次性监听
myEmitter.once('once', () => {
console.log('只触发一次');
});
// 触发事件
myEmitter.emit('event', 'Hello');
myEmitter.emit('event', 'World');
myEmitter.emit('once');
myEmitter.emit('once'); // 不执行
方法作用
emitter.on(event, listener)注册监听器
emitter.once(event, listener)注册一次性监听器
emitter.emit(event, ...args)触发事件
emitter.removeListener(event, listener)移除监听器
emitter.removeAllListeners(event)移除所有监听器
emitter.listenerCount(event)监听器数量

错误事件

如果 EventEmitter 触发了 error 事件但没有对应的监听器,Node.js 会抛出异常并退出进程:

js
const myEmitter = new EventEmitter();
// ❌ 如果没有这一行,emit('error') 会崩溃进程
myEmitter.on('error', (err) => {
console.error('捕获错误:', err.message);
});
myEmitter.emit('error', new Error('出错了'));
// 程序继续运行 ✅

子进程(child_process)

当需要执行系统命令或其他程序时使用:

js
const { exec, spawn } = require('node:child_process');

exec vs spawn

execspawn
返回缓冲整个输出到内存Stream 流式输出
适用场景少量输出、简单命令大量输出、长时间运行
最大输出200KB(默认 kill)无限制
js
// exec:输出少时方便
const { exec } = require('node:child_process');
exec('ls -la', (err, stdout, stderr) => {
if (err) return console.error(err);
console.log(stdout);
});
// spawn:大量输出时推荐
const { spawn } = require('node:child_process');
const ls = spawn('ls', ['-la']);
ls.stdout.on('data', (data) => {
console.log(`输出: ${data}`);
});
ls.stderr.on('data', (data) => {
console.error(`错误: ${data}`);
});
ls.on('close', (code) => {
console.log(`子进程退出码: ${code}`);
});

promisify exec

js
const util = require('node:util');
const exec = util.promisify(require('node:child_process').exec);
async function gitLog() {
try {
const { stdout, stderr } = await exec('git log --oneline -5');
console.log(stdout);
} catch (err) {
console.error(err.stderr);
}
}

加密(crypto)

js
const crypto = require('node:crypto');

常用操作

操作方法
MD5 哈希(不推荐)createHash('md5')
SHA-256 哈希createHash('sha256')
随机字节randomBytes(size)
UUID 生成randomUUID()
HMAC 签名createHmac('sha256', key)
js
// SHA-256
const hash = crypto.createHash('sha256').update('hello').digest('hex');
console.log(hash); // 2cf24dba5fb0a30e26e83b2ac5b9e29e...
// 随机密码
const buf = crypto.randomBytes(16);
console.log(buf.toString('hex')); // 32 位随机十六进制
// UUID
crypto.randomUUID(); // 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'

环境变量与命令行参数

环境变量

js
// 读取(设置方式:NODE_ENV=production node app.js)
console.log(process.env.NODE_ENV);
console.log(process.env.PATH);
// 设置
process.env.MY_VAR = 'value';

推荐使用 dotenv 管理 .env 文件:

bash
npm install dotenv
js
require('dotenv').config();
console.log(process.env.DB_HOST);

命令行参数

js
// 运行:node app.js --port 3000 --verbose
console.log(process.argv);
// ['node', '/path/to/app.js', '--port', '3000', '--verbose']
// 简单解析
const args = process.argv.slice(2);

进程管理

API用途
process.pid当前进程 ID
process.cwd()当前工作目录
process.exit(code)退出进程
process.exitCode设置退出码(推荐)
process.on('exit', cb)进程退出前回调
process.on('uncaughtException', cb)捕获未处理的异常
process.on('unhandledRejection', cb)捕获未处理的 Promise 拒绝
process.memoryUsage()内存使用情况
js
// 优雅退出
process.exitCode = 1; // 比 process.exit(1) 更好
// 全局异常捕获(兜底,不意味着可以忽略 try/catch)
process.on('uncaughtException', (err) => {
console.error('未捕获异常:', err);
process.exitCode = 1;
});
process.on('unhandledRejection', (reason) => {
console.error('未处理的 Promise 拒绝:', reason);
process.exitCode = 1;
});
NOTE

process.exitCode 而不是直接 process.exit()。前者让 Node.js 在完成清理工作后自然退出,后者强制立即退出。推荐使用 process.exitCode = 1

npm / 包管理

命令说明
npm init初始化项目
npm install <pkg>安装依赖(保存到 dependencies)
npm install -D <pkg>安装开发依赖
npm install -g <pkg>全局安装
npm update更新依赖
npm uninstall <pkg>卸载依赖
npm run <script>运行 package.json 脚本
npm audit检查安全漏洞
npm outdated查看可更新的包

package.json 常用字段

json
{
"name": "my-app",
"version": "1.0.0",
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js"
},
"dependencies": {
"express": "^4.18.0"
},
"devDependencies": {
"nodemon": "^3.0.0"
}
}

语义化版本(SemVer)

^1.2.3 → 兼容 1.x.x(仅允许小版本和补丁升级)
~1.2.3 → 兼容 1.2.x(仅允许补丁升级)
1.2.3 → 锁定版本
* → 任意版本(不推荐)

调试

内置调试器

bash
node inspect app.js

Chrome DevTools 调试

bash
node --inspect app.js
node --inspect-brk app.js # 在第一行断点

然后在 Chrome 打开 chrome://inspect,点击目标进程。

日志

bash
node --inspect app.js
node --inspect-brk app.js # 第一行暂停
js
console.log('普通日志');
console.error('错误日志(stderr)');
console.table([{ a: 1, b: 2 }]);
console.time('label');
// ... 耗时操作
console.timeEnd('label'); // label: xxx ms

常用工具库

用途安装
expressWeb 框架npm install express
fastify高性能 Web 框架npm install fastify
nodemon文件变更自动重启npm install -D nodemon
dotenv环境变量加载npm install dotenv
lodash工具函数库npm install lodash
date-fns日期处理(比 moment 轻)npm install date-fns
axiosHTTP 请求npm install axios
pino / winston日志库npm install pino
joi / zod数据校验npm install zod
pm2进程管理(生产环境)npm install -g pm2

常见误区

误区 1:readFileSync 在运行时使用

js
// ❌ 运行时同步读文件会阻塞事件循环
app.get('/data', (req, res) => {
const data = fs.readFileSync('/path/to/big-file.json'); // 阻塞!
res.json(JSON.parse(data));
});
// ✅ 异步读取
app.get('/data', async (req, res) => {
const data = await fsPromises.readFile('/path/to/big-file.json');
res.json(JSON.parse(data));
});

误区 2:不使用流处理大文件

js
// ❌ 把整个大文件读入内存
app.post('/upload', async (req, res) => {
const data = await fsPromises.readFile('/tmp/big-file'); // 内存爆炸
await fsPromises.writeFile('/dest', data);
});
// ✅ 使用 pipeline 流式处理
app.post('/upload', (req, res) => {
pipeline(
req,
fs.createWriteStream('/dest'),
(err) => { res.end(err ? '失败' : '成功'); }
);
});

误区 3:忽略 error 事件

js
// ❌ 没有 error 监听器,可能崩溃
const stream = fs.createReadStream('/no-such-file');
stream.on('data', (chunk) => { /* ... */ });
// ✅ 始终监听 error
stream.on('error', (err) => {
console.error('流错误:', err.message);
});

误区 4:全局安装所有东西

bash
# ❌ 全局安装,不同项目版本冲突
npm install -g express
# ✅ 项目内安装,通过 npx 运行
npm install express
npx express-generator my-app

学习路线图

  • 理解 Node.js 的事件驱动模型
  • 掌握模块系统(CJS + ESM)
  • 理解事件循环各阶段的执行顺序
  • 学会 fs 模块的三种操作风格
  • 掌握 Stream 和 pipeline 处理大文件
  • 会用 http 模块搭建基础服务
  • 理解 EventEmitter 模式
  • 学会使用 child_process 执行系统命令
  • 掌握进程管理(exit code、异常处理)
  • 在生产中使用进程管理器(pm2)

总结

Node.js 的核心哲学可以总结为一句话:非阻塞 I/O + 事件驱动

知识点要点核心模块/方法
模块系统CJS vs ESMrequire / import
事件循环6 个阶段,nextTick 优先setTimeout / setImmediate / nextTick
文件操作异步优先,大文件用 Streamfs/promises / fs.createReadStream
Stream分块处理,pipeline 安全pipeline()
网络HTTP 服务器和客户端http.createServer
进程子进程、退出码、异常捕获child_process / process
包管理npm / package.json / SemVernpm install

把所有文件都读进内存 是初学者最常犯的错误。记住:在 Node.js 里,能异步就别同步,能用 Stream 就别一次性读入,能用 pipeline 就别用 pipe。


参考来源

脚注
  1. V8 是 Google 开发的高性能 JavaScript 和 WebAssembly 引擎,用于 Chrome 和 Node.js。它将 JS 代码编译为机器码执行,支持 JIT(即时编译)优化。

  2. CommonJS(require/module.exports)是 Node.js 原生模块系统,同步加载。ESM(import/export)是 ES6 标准,异步加载,支持静态分析和 Tree Shaking。

  3. Buffer 是 Node.js 提供的二进制数据容器,用于处理 TCP 流、文件系统操作等场景。在 ES6 引入 TypedArray 前,Buffer 是处理二进制数据的主要方式。

  4. Stream(流)是 Node.js 处理大数据的核心抽象,分块读写而非一次性加载到内存。四种类型:Readable、Writable、Duplex、Transform。

  5. 事件循环是 Node.js 非阻塞 I/O 的核心机制,由 libuv 库实现。它将异步任务分阶段处理(timers → poll → check 等),在单线程中高效调度 I/O 操作、定时器和回调。

  6. libuv 是 Node.js 的跨平台异步 I/O 库,提供事件循环、文件系统操作、网络 I/O、线程池等底层能力。不同操作系统的异步 API 差异由 libuv 屏蔽。

  7. process.nextTick 在当前操作完成后、事件循环继续前立即执行,优先级高于 Promise 微任务。过度使用会导致 I/O 饥饿,推荐优先使用 setImmediate。

  8. 回调地狱指嵌套多层回调函数导致代码难以维护的现象。解决方案:Promise、async/await,或模块化拆分回调逻辑。

  9. Promise 是 ES6 引入的异步编程解决方案,表示一个未来完成或失败的操作。状态不可逆:pending → fulfilled/rejected。支持链式调用(.then/.catch)。