前言
Node.js 是一个基于 Chrome V81V8 是 Google 开发的高性能 JavaScript 和 WebAssembly 引擎,用于 Chrome 和 Node.js。它将 JS 代码编译为机器码执行,支持 JIT(即时编译)优化。 引擎的 JavaScript 运行时环境。它让前端开发者可以用 JavaScript 写后端代码,其事件驱动、非阻塞 I/O 的特性使得它在处理高并发 I/O 密集型场景时表现出色。
本文整理 Node.js 日常开发中最核心的概念和用法。
Node.js 与浏览器的区别
| 对比项 | Node.js | 浏览器 |
|---|---|---|
| 全局对象 | global、process | window、document |
| 模块系统 | CommonJS / ESM | ES Module |
| DOM | 无 | document、DOM 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 / exports | export / export default |
| 加载方式 | 同步 | 异步 |
| 是否支持顶层 await | 否 | 支持(模块中) |
// CommonJSconst fs = require('node:fs');module.exports = { myFn };
// ESMimport fs from 'node:fs';export const myFn = () => {};Node.js 官方推荐使用 ESM 作为新项目的默认选择。但 CommonJS 生态仍极其庞大,两者在 require() 和 import() 间可以通过一些方式互操作。
核心模块 (Core Modules)
Node.js 内置了大量核心模块,无需安装即可使用:
// 推荐使用 node: 前缀,明确标识是核心模块const fs = require('node:fs');const path = require('node:path');const http = require('node:http');| 模块 | 用途 | 常用 API |
|---|---|---|
node:fs | 文件系统操作 | readFile、writeFile、createReadStream |
node:path | 路径处理 | join、resolve、basename、extname |
node:http / node:https | HTTP 客户端和服务端 | createServer、get |
node:url | URL 解析 | URL、URLSearchParams |
node:os | 操作系统信息 | platform、cpus、totalmem |
node:events | 事件发射器 | EventEmitter |
node:stream | 流处理(Buffer3Buffer 是 Node.js 提供的二进制数据容器,用于处理 TCP 流、文件系统操作等场景。在 ES6 引入 TypedArray 前,Buffer 是处理二进制数据的主要方式。 vs Stream4Stream(流)是 Node.js 处理大数据的核心抽象,分块读写而非一次性加载到内存。四种类型:Readable、Writable、Duplex、Transform。) | Readable、Writable、Transform |
node:child_process | 子进程 | spawn、exec |
node:crypto | 加密 | createHash、randomBytes |
node:util | 工具函数 | promisify、callbackify |
事件循环(Event Loop)5事件循环是 Node.js 非阻塞 I/O 的核心机制,由 libuv 库实现。它将异步任务分阶段处理(timers → poll → check 等),在单线程中高效调度 I/O 操作、定时器和回调。
事件循环是 Node.js 非阻塞 I/O 的核心机制。
各阶段说明
| 阶段 | 执行内容 |
|---|---|
| timers | 执行 setTimeout、setInterval 的回调 |
| pending callbacks | 执行某些系统操作的回调(如 TCP 错误) |
| idle, prepare | libuv6libuv 是 Node.js 的跨平台异步 I/O 库,提供事件循环、文件系统操作、网络 I/O、线程池等底层能力。不同操作系统的异步 API 差异由 libuv 屏蔽。 内部使用 |
| poll | 获取新的 I/O 事件,执行 I/O 回调。如果队列为空,检查是否有 setImmediate → 走 check 阶段;否则等待 |
| check | 执行 setImmediate 的回调 |
| close callbacks | 执行关闭事件的回调(如 socket.destroy()) |
setImmediate vs setTimeout
setImmediate | setTimeout(fn, 0) | |
|---|---|---|
| 执行时机 | check 阶段 | timers 阶段 |
| 在 I/O 周期内 | 总是先于 setTimeout | 晚于 setImmediate |
| 在顶层调用 | 取决于进程性能,顺序不确定 | 取决于进程性能 |
// 在 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 > 事件循环各阶段let bar = null;
function someAsyncApiCall(callback) { process.nextTick(callback);}
someAsyncApiCall(() => { console.log('bar', bar); // 1 — nextTick 等到了变量初始化});
bar = 1;process.nextTick 优先级最高,但递归调用可能导致 I/O 饿死。推荐优先使用 setImmediate,能更好地与事件循环配合。
微任务与宏任务
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 模块提供三种操作风格:
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 风格
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; }}同步风格(仅限启动时)
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 }) | 递归删除文件或目录 |
对于大文件,不要使用 readFile 一次性读入内存。应该使用 Stream(见下一节)逐块处理。readFileSync 更是在服务器运行中应当避免使用,它会阻塞整个事件循环。
Stream(流)
Stream 是 Node.js 处理大规模数据的方式——分块处理,而非一次性加载到内存。
四种 Stream
| 类型 | 用途 | 示例 |
|---|---|---|
| Readable | 读取数据 | fs.createReadStream、HTTP 请求 |
| Writable | 写入数据 | fs.createWriteStream、HTTP 响应 |
| Duplex | 可读可写 | TCP Socket |
| Transform | 转换数据 | zlib.createGzip |
读文件示例(可读流)
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:
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);// 回调风格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 服务器
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.method | HTTP 方法: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 | 状态码 |
获取请求体
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);Node.js 的 http 模块功能基础,实际项目中大多使用 Express / Fastify / Koa 等框架。但对于了解底层原理,自己写一个 http.createServer 很有帮助。
路径处理(path)
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.jspath.dirname('/a/b/index.js'); // /a/bpath.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: \使用 path.join 而不是手动拼接字符串(dir + '/' + file),因为前者能正确处理不同操作系统的路径分隔符。
事件发射器(EventEmitter)
Node.js 的很多核心模块都继承自 EventEmitter。理解它的工作方式对理解 Node.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 会抛出异常并退出进程:
const myEmitter = new EventEmitter();
// ❌ 如果没有这一行,emit('error') 会崩溃进程myEmitter.on('error', (err) => { console.error('捕获错误:', err.message);});
myEmitter.emit('error', new Error('出错了'));// 程序继续运行 ✅子进程(child_process)
当需要执行系统命令或其他程序时使用:
const { exec, spawn } = require('node:child_process');exec vs spawn
exec | spawn | |
|---|---|---|
| 返回 | 缓冲整个输出到内存 | Stream 流式输出 |
| 适用场景 | 少量输出、简单命令 | 大量输出、长时间运行 |
| 最大输出 | 200KB(默认 kill) | 无限制 |
// 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
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)
const crypto = require('node:crypto');常用操作
| 操作 | 方法 |
|---|---|
| MD5 哈希(不推荐) | createHash('md5') |
| SHA-256 哈希 | createHash('sha256') |
| 随机字节 | randomBytes(size) |
| UUID 生成 | randomUUID() |
| HMAC 签名 | createHmac('sha256', key) |
// SHA-256const hash = crypto.createHash('sha256').update('hello').digest('hex');console.log(hash); // 2cf24dba5fb0a30e26e83b2ac5b9e29e...
// 随机密码const buf = crypto.randomBytes(16);console.log(buf.toString('hex')); // 32 位随机十六进制
// UUIDcrypto.randomUUID(); // 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'环境变量与命令行参数
环境变量
// 读取(设置方式:NODE_ENV=production node app.js)console.log(process.env.NODE_ENV);console.log(process.env.PATH);
// 设置process.env.MY_VAR = 'value';推荐使用 dotenv 管理 .env 文件:
npm install dotenvrequire('dotenv').config();console.log(process.env.DB_HOST);命令行参数
// 运行:node app.js --port 3000 --verboseconsole.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() | 内存使用情况 |
// 优雅退出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;});用 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 常用字段
{ "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 → 锁定版本* → 任意版本(不推荐)调试
内置调试器
node inspect app.jsChrome DevTools 调试
node --inspect app.jsnode --inspect-brk app.js # 在第一行断点然后在 Chrome 打开 chrome://inspect,点击目标进程。
日志
node --inspect app.jsnode --inspect-brk app.js # 第一行暂停console.log('普通日志');console.error('错误日志(stderr)');console.table([{ a: 1, b: 2 }]);console.time('label');// ... 耗时操作console.timeEnd('label'); // label: xxx ms常用工具库
| 库 | 用途 | 安装 |
|---|---|---|
| express | Web 框架 | 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 |
| axios | HTTP 请求 | npm install axios |
| pino / winston | 日志库 | npm install pino |
| joi / zod | 数据校验 | npm install zod |
| pm2 | 进程管理(生产环境) | npm install -g pm2 |
常见误区
误区 1:readFileSync 在运行时使用
// ❌ 运行时同步读文件会阻塞事件循环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:不使用流处理大文件
// ❌ 把整个大文件读入内存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 事件
// ❌ 没有 error 监听器,可能崩溃const stream = fs.createReadStream('/no-such-file');stream.on('data', (chunk) => { /* ... */ });
// ✅ 始终监听 errorstream.on('error', (err) => { console.error('流错误:', err.message);});误区 4:全局安装所有东西
# ❌ 全局安装,不同项目版本冲突npm install -g express
# ✅ 项目内安装,通过 npx 运行npm install expressnpx express-generator my-app学习路线图
- 理解 Node.js 的事件驱动模型
- 掌握模块系统(CJS + ESM)
- 理解事件循环各阶段的执行顺序
- 学会 fs 模块的三种操作风格
- 掌握 Stream 和 pipeline 处理大文件
- 会用 http 模块搭建基础服务
- 理解 EventEmitter 模式
- 学会使用 child_process 执行系统命令
- 掌握进程管理(exit code、异常处理)
- 在生产中使用进程管理器(pm2)
总结
Node.js 的核心哲学可以总结为一句话:非阻塞 I/O + 事件驱动。
| 知识点 | 要点 | 核心模块/方法 |
|---|---|---|
| 模块系统 | CJS vs ESM | require / import |
| 事件循环 | 6 个阶段,nextTick 优先 | setTimeout / setImmediate / nextTick |
| 文件操作 | 异步优先,大文件用 Stream | fs/promises / fs.createReadStream |
| Stream | 分块处理,pipeline 安全 | pipeline() |
| 网络 | HTTP 服务器和客户端 | http.createServer |
| 进程 | 子进程、退出码、异常捕获 | child_process / process |
| 包管理 | npm / package.json / SemVer | npm install |
把所有文件都读进内存 是初学者最常犯的错误。记住:在 Node.js 里,能异步就别同步,能用 Stream 就别一次性读入,能用 pipeline 就别用 pipe。
参考来源
V8 是 Google 开发的高性能 JavaScript 和 WebAssembly 引擎,用于 Chrome 和 Node.js。它将 JS 代码编译为机器码执行,支持 JIT(即时编译)优化。
↩CommonJS(require/module.exports)是 Node.js 原生模块系统,同步加载。ESM(import/export)是 ES6 标准,异步加载,支持静态分析和 Tree Shaking。
↩Buffer 是 Node.js 提供的二进制数据容器,用于处理 TCP 流、文件系统操作等场景。在 ES6 引入 TypedArray 前,Buffer 是处理二进制数据的主要方式。
↩Stream(流)是 Node.js 处理大数据的核心抽象,分块读写而非一次性加载到内存。四种类型:Readable、Writable、Duplex、Transform。
↩事件循环是 Node.js 非阻塞 I/O 的核心机制,由 libuv 库实现。它将异步任务分阶段处理(timers → poll → check 等),在单线程中高效调度 I/O 操作、定时器和回调。
↩libuv 是 Node.js 的跨平台异步 I/O 库,提供事件循环、文件系统操作、网络 I/O、线程池等底层能力。不同操作系统的异步 API 差异由 libuv 屏蔽。
↩process.nextTick 在当前操作完成后、事件循环继续前立即执行,优先级高于 Promise 微任务。过度使用会导致 I/O 饥饿,推荐优先使用 setImmediate。
↩回调地狱指嵌套多层回调函数导致代码难以维护的现象。解决方案:Promise、async/await,或模块化拆分回调逻辑。
↩Promise 是 ES6 引入的异步编程解决方案,表示一个未来完成或失败的操作。状态不可逆:pending → fulfilled/rejected。支持链式调用(.then/.catch)。
↩
评论
GitHub 登录后可评论。
评论区会在滚动到这里时自动加载。