入门
REPL 与运行脚本
REPL(读取-求值-打印循环)是一个交互式 shell,用于试验 Node.js 代码。使用 node --watch 可在开发时自动重启。ES 模块需要在 package.json 中设置 type:module 或使用 .mjs 扩展名。
# start the REPL (interactive shell)
node
# run a script
node app.js
# run with ES modules (type: module in package.json)
node --input-type=module app.js
# execute inline code
node -e "console.log(process.version)"
# watch mode (auto-restart on change)
node --watch app.js版本与运行时标志
node --version 打印运行时版本。--trace-warnings 显示运行时警告的堆栈跟踪。NODE_OPTIONS 是一个保存 CLI 标志的环境变量,在容器或 npm 脚本中很方便。--inspect 开启 Chrome DevTools 调试器。
# check version
node --version # v20.11.0
node -p "process.versions.v8"
# enable warning stack traces
node --trace-warnings app.js
# pass NODE_OPTIONS (max ~128KB string)
NODE_OPTIONS="--max-old-space-size=4096" node app.js
# inspect memory usage at runtime
node --inspect app.jspackage.json 与项目初始化
npm init 创建 package.json。"type" 字段决定默认模块系统:"module" 在 .js 文件中启用 ES 模块语法,"commonjs"(默认)保留 require/module.exports。"main" 是包被 require 时的入口文件。
# scaffold a project (answer prompts)
npm init
# non-interactive defaults
npm init -y
# "type" controls module system
# "commonjs" (default) -> require/module.exports
# "module" -> import/export
{
"name": "my-app",
"version": "1.0.0",
"type": "module",
"main": "index.js",
"scripts": { "start": "node index.js" }
}CommonJS 与 ES 模块
Node 支持两种模块系统。CommonJS(require/module.exports)是同步的、历史悠久的默认选择。ES 模块(import/export)是现代标准。文件扩展名(.mjs/.cjs)或 package.json 的 "type" 字段决定 .js 文件使用哪种解析器。
# CommonJS (.cjs or type:commonjs) ----
const fs = require('fs'); // import
module.exports = { greet: () => 'hi' }; // export
# ES Modules (.mjs or type:module) -------
import fs from 'fs'; // import
export function greet() { return 'hi'; } // named export
export default { greet }; // default export
# .mjs is always ESM, .cjs is always CJS
node file.mjs # forced ESM
node file.cjs # forced CJS全局对象
Node 提供了 process、console、Buffer 和定时器等无需 import 的全局对象。__dirname 和 __filename 仅在 CommonJS 中可用 —— 在 ES 模块中改用 import.meta.url。global 是根作用域对象(类似浏览器中的 window)。
# available everywhere, no import needed
global; // global namespace object
process; // env, argv, stdin/stdout
console; // log, error, warn, table
setTimeout; // schedule a callback
setInterval; // schedule repeating callback
Buffer; // binary data (Uint8Array)
queueMicrotask;// schedule microtask
structuredClone; // deep clone
# CommonJS-only globals (not in ESM)
__dirname; // current directory
__filename; // current file path可执行脚本(Shebang)
Shebang 行(#!/usr/bin/env node)让文件在 Unix 上可作为独立命令运行。chmod +x 后可直接运行 ./cli.js。package.json 的 "bin" 字段将命令名映射到脚本,发布到 npm 的 CLI 工具用它使全局安装后暴露该命令。
#!/usr/bin/env node
// cli.js - make a file directly runnable
const name = process.argv[2] || 'world';
console.log(`Hello, ${name}!`);
# make it executable (Unix) and run directly
chmod +x cli.js
./cli.js Alice # Hello, Alice!
# or wire it up in package.json
{
"bin": { "mycli": "./cli.js" }
}模块系统
CommonJS require 与 exports
require() 同步加载 CommonJS 模块并返回其 module.exports。require 解析相对路径(./、../)、内置模块(fs)和 node_modules。替换整个导出时务必赋值给 module.exports(而非 exports),因为 exports 最初只是 module.exports 的别名。
// math.js
function add(a, b) { return a + b; }
const PI = 3.14;
// export via module.exports
module.exports = { add, PI };
// or: exports.add = add; exports.PI = PI;
// app.js
const math = require('./math'); // .js optional
console.log(math.add(1, 2)); // 3
console.log(math.PI); // 3.14
// destructuring
const { add } = require('./math');
console.log(add(5, 7)); // 12ES 模块 import/export
ES 模块使用静态 import/export —— 绑定是实时的且会被提升。默认导出可用任意名称导入(无花括号);命名导出必须用花括号且名称匹配。import * as 创建命名空间对象。ESM 设计上是异步的,是现代标准。
// math.mjs (or type:module)
export function add(a, b) { return a + b; }
export const PI = 3.14;
export default function square(x) { return x * x; }
// app.mjs
import square, { add, PI } from './math.mjs';
console.log(add(1, 2)); // 3
console.log(square(5)); // 25
// import all as namespace
import * as math from './math.mjs';
console.log(math.PI); // 3.14动态 import()
import() 是动态、异步的导入,CJS 和 ESM 均可使用。它返回一个解析为模块命名空间的 Promise,支持懒加载和条件导入 —— 非常适合减少启动时间或加载可选功能。也可用于服务端代码分割。
// dynamic import returns a Promise
async function loadPlugin(name) {
const mod = await import(`./plugins/${name}.mjs`);
return mod.default;
}
// lazy-load heavy modules on demand
if (needPdf) {
const { exportPdf } = await import('./pdf.mjs');
exportPdf(data);
}
// works in both CommonJS and ESM
import('./optional.mjs')
.then(mod => mod.run())
.catch(err => console.error('load failed', err));内置模块
Node 自带数十个内置模块 —— 无需 npm install。大多数暴露回调 API;许多还在 'module/promises' 路径下提供 promise 变体(fs/promises、dns/promises、stream/promises)。require('module')(无 ./)优先解析内置模块,然后是 node_modules。
// core modules - no install, just require/import
const fs = require('fs'); // file system
const path = require('path'); // path handling
const http = require('http'); // HTTP server/client
const os = require('os'); // OS info
const crypto = require('crypto'); // hashing/encryption
const { EventEmitter } = require('events');
const { Buffer } = require('buffer');
const stream = require('stream'); // streams
const net = require('net'); // TCP
const child_process = require('child_process');
// some are promise-based (Node 10+)
const fsp = require('fs/promises'); // async/await fs模块缓存与循环依赖
require() 按解析路径缓存模块,因此同一模块代码只运行一次,导出对象会被复用。循环依赖可以工作,但先加载的模块会看到第二个模块不完整的(部分)导出对象 —— 应设计时规避。开发时可用 delete require.cache 热重载。
// modules are cached after first load
// a.js
require('./b'); // b loads, sets module.exports
const b = require('./b'); // returns cached b (same object)
console.log(b === require('./b')); // true
// circular dependency: a -> b -> a
// a.js: const b = require('./b'); b.f();
// b.js: const a = require('./a'); // a is PARTIAL here!
// module.exports = { f: () => a.x };
// inspect the cache
console.log(require.cache); // map of loaded modules
delete require.cache[require.resolve('./b')]; // force reload__dirname 与 import.meta.url
__dirname 和 __filename 是 CommonJS 专用的全局变量,给出当前文件夹/文件路径。ES 模块没有这些全局变量 —— 需通过 fileURLToPath 从 import.meta.url 派生。import.meta.url 是当前模块的 file:// URL,是 ESM 中 __filename 的替代品。
// CommonJS - synchronous path globals
console.log(__dirname); // /app/src
console.log(__filename); // /app/src/index.js
// ES Modules - no __dirname, use import.meta
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const dataPath = join(__dirname, 'data.json');
// import.meta also carries the module URL
console.log(import.meta.url); // file:///app/src/index.mjsnpm 与 pnpm
npm init 与 package.json
npm init 脚手架生成 package.json。-y 跳过提示使用默认值。npm set 将配置存储到 ~/.npmrc 供后续 init 使用。作用域包(@org/name)适用于组织以及避免注册表上的命名冲突。
# create package.json interactively
npm init
# accept all defaults
npm init -y
# pnpm equivalent
pnpm init
# set author/license globally
npm set init-author-name "Alice"
npm set init-license MIT
# scope packages with @username
npm init --scope=@myorg安装依赖
npm install(无参数)安装所有列出的依赖。带名称时它会添加到 package.json。--save-dev 将其放入 devDependencies(测试/构建工具,不发布)。^ 允许 minor+patch 更新,~ 仅允许 patch。-g 全局安装 CLI 工具。
# install everything in package.json
npm install # alias: npm i
pnpm install # alias: pnpm i
# add a runtime dependency -> dependencies
npm install express
pnpm add express
# add a dev-only dependency -> devDependencies
npm install --save-dev jest # npm i -D jest
pnpm add -D jest
# install a specific version
npm install [email protected]
npm install lodash@^4.17.0 # caret: 4.x
npm install lodash@~4.17.0 # tilde: 4.17.x
# global install (CLI tools)
npm install -g pm2
pnpm add -g pm2npm 脚本
package.json 中的脚本通过 npm run <name> 运行。start 和 test 是特殊的(npm start / npm test)。pre/post 钩子(pretest、postbuild)会自动运行。用 -- 将参数转发给底层命令。pnpm 对自定义脚本可省略 "run"。
// package.json
{
"scripts": {
"start": "node index.js",
"dev": "node --watch index.js",
"test": "jest",
"build": "tsc && vite build"
}
}
# run a script
npm run dev # npm run-script dev
pnpm dev # pnpm has no "run" requirement
# "start" and "test" have shortcuts
npm start
npm test
# pre/post hooks run automatically
// "pretest": "npm run lint",
// "postbuild": "npm run deploy"
# pass extra args with --
npm run build -- --mode productionnpx 与 pnpm dlx
npx 无需全局安装即可运行包二进制文件 —— 它下载、执行并丢弃。非常适合脚手架工具(create-vite)和运行本地 node_modules/.bin 工具。--yes 跳过安装确认。pnpm dlx 是 pnpm 的对应物。
# run a one-off command without installing globally
npx create-vite my-app
npx cowsay "hello"
# pin a version
npx prettier@3 --write .
# pnpm equivalent
pnpm dlx create-vite my-app
# execute a local bin from node_modules
npx jest # runs ./node_modules/.bin/jest
# never prompt, always download latest
npx --yes create-react-app my-app版本控制与锁文件
语义化版本:MAJOR 用于破坏性变更,MINOR 用于功能,PATCH 用于修复。^(脱字符)是 npm 默认值,允许 minor+patch 更新;~(波浪号)仅允许 patch。锁文件(package-lock.json)冻 结整个依赖树以实现可复现安装 —— 在 CI 中使用 npm ci 以提速并保安全。
# semantic versioning: MAJOR.MINOR.PATCH
# 1.4.2 -> 1:breaking 4:feature 2:fix
"^1.4.2" # >=1.4.2 <2.0.0 (caret, default)
"~1.4.2" # >=1.4.2 <1.5.0 (tilde)
"1.4.2" # exact 1.4.2
">=1.4.0 <2.0.0" # range
# lockfiles pin the installed tree
npm install # writes/updates package-lock.json
pnpm install # writes pnpm-lock.yaml
yarn install # writes yarn.lock
# install exactly as locked (CI)
npm ci # clean install from lockfile
# find outdated deps
npm outdated
npm update # update within ^ ranges发布与生命周期
npm publish 将你的包上传到注册表。npm version 提升版本号、创建 git 标签并提交。生命周期钩子(prepublishOnly、postpublish)在发布时自动执行构建/测试。unpublish 仅允许在 72 小时内 —— 旧版本建议使用 npm deprecate。
# login once
npm login
# publish to the registry
npm publish
npm publish --access public # scoped default restricted
# bump version (updates package.json + tag + commit)
npm version patch # 1.0.0 -> 1.0.1
npm version minor # 1.0.0 -> 1.1.0
npm version major # 1.0.0 -> 2.0.0
# lifecycle scripts (in package.json)
// "prepublishOnly": "npm test && npm run build",
// "prepublish": "npm run build",
// "postpublish": "git push --tags"
# unpublish within 72h
npm unpublish [email protected]文件系统(fs)
读取文件
fs 提供同步(readFileSync)、回调(readFile)和 promise(fs/promises readFile)三种 API。同步调用会阻塞事件循环 —— 服务端应避免。fs/promises 是 async/await 的现代选择。不带编码参数时 readFile 返回 Buffer。
const fs = require('fs');
// synchronous (blocks the event loop)
const text = fs.readFileSync('data.txt', 'utf8');
const buf = fs.readFileSync('image.png'); // Buffer
// asynchronous with callback (error-first)
fs.readFile('data.txt', 'utf8', (err, data) => {
if (err) return console.error(err);
console.log(data);
});
// async with fs/promises (preferred)
const fsp = require('fs/promises');
async function read() {
const text = await fsp.readFile('data.txt', 'utf8');
return text;
}写入文件
writeFile 默认替换整个文件。flag 选项改变行为:'a' 追加,'wx' 在文件已存在时失败(EEXIST)—— 适用于原子文件创建。JSON 应在写入前 stringify。同步写入会阻塞;优先使用 fs/promises。
const fs = require('fs');
const fsp = require('fs/promises');
// sync
fs.writeFileSync('out.txt', 'hello');
// async callback
fs.writeFile('log.txt', 'line1\n', err => {
if (err) throw err;
});
// promise
await fsp.writeFile('config.json', JSON.stringify(cfg, null, 2));
// flags: 'w' write (default), 'a' append, 'wx' fail if exists
await fsp.writeFile('new.txt', 'x', { flag: 'wx' }); // throws if exists目录
mkdir 配合 { recursive: true } 模仿 mkdir -p。readdir 列出条目;{ withFileTypes: true } 返回 Dirent 对象,其 isDirectory()/isFile() 避免额外的 stat 调用。fsp.rm 配合 recursive+force(Node 14+)替代 rmdir 处理非空目录。
const fsp = require('fs/promises');
// create (recursive like mkdir -p)
await fsp.mkdir('a/b/c', { recursive: true });
// list entries
const entries = await fsp.readdir('.');
// ['app.js', 'data', 'package.json']
// list with type info
const detailed = await fsp.readdir('.', { withFileTypes: true });
detailed.forEach(e => console.log(e.name, e.isDirectory()));
// remove
await fsp.rmdir('empty'); // empty dir only
await fsp.rm('folder', { recursive: true, force: true }); // rm -rf
// sync versions exist too: mkdirSync, readdirSync, rmSync文件信息与元数据
stat() 返回 Stats 对象,包含 size、mtime 和类型辅助方法。不要使用 fs.exists(已废弃);用 access() 配合 try/catch 做无竞争的存在性检查 —— 不过直接打开文件更安全。fsWatch 发出 change 事件,但跨平台可能不一致。
const fsp = require('fs/promises');
const stats = await fsp.stat('data.txt');
stats.isFile(); // true
stats.isDirectory(); // false
stats.size; // bytes
stats.mtime; // Date modified
stats.birthtime; // Date created
stats.mode; // permission bits
// check existence (avoid existsSync for races)
try {
await fsp.access('config.json');
console.log('exists');
} catch {
console.log('missing');
}
// watch for changes
const watcher = fs.watch('.');
watcher.on('change', (eventType, filename) => {});fs.promises API
fs/promises(Node 10+)提供所有 fs 方法的 promise 版本,非常适合 async/await。opendir() 返回异步迭代器,可高效遍历目录。结合 path.join 实现可移植路径。所有方法出错时抛出 —— 用 try/catch 包裹。
const fsp = require('fs/promises');
const path = require('path');
// full async/await file operations
async function loadConfig() {
const file = path.join(__dirname, 'config.json');
const raw = await fsp.readFile(file, 'utf8');
return JSON.parse(raw);
}
async function saveConfig(cfg) {
const file = path.join(__dirname, 'config.json');
await fsp.writeFile(file, JSON.stringify(cfg, null, 2));
}
// iterate directory tree
for await (const entry of await fsp.opendir('.')) {
console.log(entry.name);
}追加、重命名、删除
appendFile 在文件末尾追加内容(文件不存在则创建)。rename 在同一文件系统上是原子的,也用于移动文件。unlink 删除文件。copyFile 复制内容;fsp.cp(Node 16+)递归处理目录。truncate 将文件调整为指定长度。
const fsp = require('fs/promises');
// append to a file (creates if missing)
await fsp.appendFile('log.txt', new Date() + ' started\n');
// atomic rename (also for moving)
await fsp.rename('old.txt', 'new.txt');
// move across dirs
await fsp.rename('a/x.txt', 'b/x.txt');
// delete a file
await fsp.unlink('temp.txt');
// copy
await fsp.copyFile('src.txt', 'dest.txt');
// or fsp.cp('src', 'dest', { recursive: true })
// truncate to a length
await fsp.truncate('data.bin', 1024);路径处理
path.join 与 path.resolve
path.join 用平台分隔符拼接片段并规范化结果。path.resolve 从 process.cwd() 起生成绝对路径;绝对片段会丢弃之前的片段。始终用 path.join 而非字符串拼接,使路径在 Windows 和 POSIX 上都能工作。
const path = require('path');
// join segments with the OS separator
path.join('src', 'utils', 'file.js');
// 'src/utils/file.js' (Linux) or 'src\utils\file.js' (Windows)
// resolve to an ABSOLUTE path (from cwd)
path.resolve('src', 'file.js');
// '/current/dir/src/file.js'
// resolve with absolute segment resets
path.resolve('/a', 'b', '/c', 'd'); // '/c/d'
// normalize '..' and '.' segments
path.normalize('a/b/../c/./d'); // 'a/c/d'
// never hardcode '/' — use path.join for portabilitydirname、basename、extname
dirname 返回目录,basename 返回文件名(可选地去除扩展名),extname 返回最后一个扩展名(含点)。对于 .tar.gz 这样的多段扩展名,只返回最后的 .gz。这些都是纯字符串操作 —— 不访问文件系统。
const path = require('path');
const file = '/app/src/utils/helper.js';
path.dirname(file); // '/app/src/utils'
path.basename(file); // 'helper.js'
path.basename(file, '.js'); // 'helper' (strip ext)
path.extname(file); // '.js'
path.extname('archive.tar.gz'); // '.gz' (last ext)
// for the current file (CommonJS)
path.dirname(__filename); // current dir
path.basename(__filename); // current file namepath.parse 与 format
path.parse 将路径拆分为 root、dir、base、name、ext。path.format 将它们重新组装。path.relative 计算从一个目录到另一个目录所需的相对路径。它们让路径操作变得声明式,而非繁琐的字符串切片。
const path = require('path');
// decompose a path into parts
const p = path.parse('/app/src/helper.test.js');
// { root: '/', dir: '/app/src',
// base: 'helper.test.js', name: 'helper.test', ext: '.js' }
// rebuild from a parsed object
const rebuilt = path.format({
dir: '/app/dist',
name: 'bundle',
ext: '.min.js'
});
// '/app/dist/bundle.min.js'
// get the relative path from A to B
path.relative('/a/b/c', '/a/x'); // '../../x'跨平台路径
path.sep 和 path.delimiter 因操作系统而异。path.win32 和 path.posix 强制使用特定解析器,无论宿主 OS —— 在处理来自其他平台的路径时很方便。用 path.delimiter 可移植地拆分 PATH 类环境变量,而非硬编码 ':' 或 ';'。
const path = require('path');
// platform-specific values
path.sep; // '/' (POSIX) or '\\' (Windows)
path.delimiter; // ':' (POSIX) or ';' (Windows)
// force a specific platform's behavior
path.win32.join('a', 'b'); // 'a\\b'
path.posix.join('a', 'b'); // 'a/b'
// parse PATH env var portably
process.env.PATH.split(path.delimiter).forEach(dir => {
console.log(dir);
});
// check if absolute
path.isAbsolute('/etc'); // true
path.isAbsolute('a/b'); // false
path.win32.isAbsolute('C:\\'); // true文件 URL 与 pathToFileURL
ES 模块以 file:// URL 标识。用 url.pathToFileURL 将普通路径转为 URL 以便动态 import(),用 url.fileURLToPath 反向转换。在 ESM 中,import.meta.url 已经是 file URL —— 用 fileURLToPath 转为路径以复制 __filename。
const path = require('path');
const url = require('url');
// convert a path to a file:// URL (ESM import)
const fileUrl = url.pathToFileURL('/app/src/mod.mjs');
// URL { href: 'file:///app/src/mod.mjs' }
// and back
const p = url.fileURLToPath('file:///app/src/mod.mjs');
// '/app/src/mod.mjs'
// in ESM, import.meta.url is already a file URL
// convert to a normal path:
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);HTTP 服务器
基础 HTTP 服务器
http.createServer 返回一个服务器;回调处理每个请求。res.writeHead 设置状态码和头,res.end 发送主体并结束响应。listen() 开始在指定端口接受连接。一个 Node 进程通过事件循环处理许多并发请求。
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello, World!\n');
});
// listen on a port
server.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});
// or with a host and backlog
server.listen(3000, '127.0.0.1', 511, () => {});请求与响应
req 暴露 method、url 和 headers;res 让你设置 statusCode/headers 再 write+end。必须调用 res.end 才能结束响应。对于大负载,通过 res.write 流式传输或将 Readable pipe 到 res,避免把所有内容缓冲在内存中。
const http = require('http');
http.createServer((req, res) => {
// request info
console.log(req.method); // 'GET'
console.log(req.url); // '/users?id=5'
console.log(req.headers); // { host, 'user-agent', ... }
// response helpers
res.statusCode = 200;
res.setHeader('Content-Type', 'application/json');
res.write('partial chunk\n');
res.end(JSON.stringify({ ok: true }));
}).listen(3000);
// streaming a large response
res.writeHead(200);
readStream.pipe(res); // pipe file -> response路由与 URL 解析
Node 没有内置路由器 —— 用 if/else 针对 req.method 和 req.url 实现一个,或用 URL 构造器解析 pathname 和 searchParams。对真实应用,使用 Express 或框架中的路由器。URL 构造器在解析相对 req.url 时需要 base。
const http = require('http');
const { URL } = require('url');
http.createServer((req, res) => {
const url = new URL(req.url, 'http://localhost');
const path = url.pathname;
const q = url.searchParams.get('id');
if (req.method === 'GET' && path === '/') {
res.end('home');
} else if (path === '/users') {
res.end('users');
} else {
res.writeHead(404);
res.end('not found');
}
}).listen(3000);处理 JSON 与 POST 请求体
请求体以流的形式到达 —— 必须先收集 chunk 再解析。async 迭代模式(for await of req)是现代做法。响应 JSON 时务必设置 Content-Type: application/json。生产环境应限制请求体大小以防内存耗尽。
const http = require('http');
http.createServer(async (req, res) => {
if (req.method === 'POST') {
// collect the streamed body
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
const body = JSON.parse(Buffer.concat(chunks).toString());
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ received: body }));
}
}).listen(3000);
// test: curl -X POST localhost:3000 -d '{"a":1}'服务器事件与错误
服务器是一个 EventEmitter。必须处理 'error' —— EADDRINUSE 表示端口被占用。'connection' 在每个客户端 socket 触发。优雅关闭时,监听 SIGTERM/SIGINT 并调用 server.close(),让进行中的请求在进程退出前完成。
const server = http.createServer(handler);
server.on('listening', () => console.log('ready'));
server.on('connection', socket => console.log('client connected'));
server.on('error', err => {
if (err.code === 'EADDRINUSE') {
console.error('port 3000 already in use');
}
});
server.on('close', () => console.log('server closed'));
// graceful shutdown
process.on('SIGTERM', () => {
server.close(() => process.exit(0));
});
server.listen(3000);HTTPS 与 HTTP 客户端
https.createServer 需要私钥和证书(如来自 Let's Encrypt)。对于发起请求,Node 18+ 内置全局 fetch() —— 优先使用它而非原始的 http.get 回调 API。fetch 基于 promise,支持 async/await、headers 和流。
// HTTPS server (needs TLS certs)
const https = require('https');
const fs = require('fs');
https.createServer({
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem')
}, handler).listen(443);
// HTTP client: simple GET
http.get('http://example.com', res => {
let data = '';
res.on('data', c => data += c);
res.on('end', () => console.log(data));
});
// modern fetch (Node 18+, global, promise-based)
const res = await fetch('https://api.example.com');
const json = await res.json();Express 基础
Express 搭建与 Hello World
Express 是最流行的 Node Web 框架。app.get 注册路由处理器;res.send 发送响应(自动检测 Content-Type)。app.listen 启动服务器。Express 用路由、中间件和便捷辅助封装了 Node 的 http 模块。
// install: npm install express
import express from 'express';
const app = express();
const PORT = 3000;
app.get('/', (req, res) => {
res.send('Hello, World!');
});
app.listen(PORT, () => {
console.log(`Server on http://localhost:${PORT}`);
});路由与路由参数
Express 将 HTTP 方法(get/post/put/delete)匹配到路径。:id 把 URL 片段捕获到 req.params;req.query 保存查询字符串值。app.route 让你对同一路径链式调用多个方法。路由按注册顺序匹配,因此具体路由应定义在通用路由之前。
app.get('/users', (req, res) => res.send('list users'));
app.post('/users', (req, res) => res.send('create user'));
// URL parameters
app.get('/users/:id', (req, res) => {
res.send(`user ${req.params.id}`);
});
// query strings: /search?q=node
app.get('/search', (req, res) => {
res.send(`searching: ${req.query.q}`);
});
// chain handlers for one path
app.route('/book')
.get((req, res) => res.send('get book'))
.post((req, res) => res.send('add book'));中间件
中间件是在请求周期中运行的函数,接收 req、res 和 next。调用 next() 继续到下一个处理器。app.use 全局 或按路径前缀挂载中间件。错误处理中间件有 4 个参数(err 在前),应最后注册。
// middleware: (req, res, next) => {}
app.use(express.json()); // parse JSON bodies
app.use(express.static('public'));// serve static files
app.use((req, res, next) => { // custom logger
console.log(req.method, req.url);
next(); // pass control forward
});
// mounted on a path
app.use('/api', (req, res, next) => {
req.apiCall = true;
next();
});
// error-handling middleware (4 args)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Server Error');
});请求数据
只有挂载正确的中间件时 Express 才解析请求体:express.json() 处理 JSON,express.urlencoded() 处理表单,multer 处理文件上传。req.params(URL 片段)、req.query(查询字符串)和 req.body(解析后的负载)是三大请求数据来源。
// JSON body (needs express.json() middleware)
app.post('/api', (req, res) => {
console.log(req.body); // parsed JSON object
});
// URL params and query
app.get('/u/:id', (req, res) => {
const { id } = req.params;
const { tab } = req.query;
});
// form data (urlencoded)
app.use(express.urlencoded({ extended: true }));
app.post('/form', (req, res) => res.json(req.body));
// file uploads: use multer middleware
// const upload = multer({ dest: 'uploads/' });
// app.post('/upload', upload.single('file'), handler);
// headers & cookies
req.headers['user-agent'];
req.cookies; // needs cookie-parser提供静态文件
express.static 从目录提供文件。多次挂载可实现回退行为,或加路径前缀。maxAge 等选项设置缓存头。它自动检测 MIME 类型并支持音视频的范围请求。始终使用绝对路径以避免依赖 cwd 的 bug。
const path = require('path');
// serve files from /public at /
app.use(express.static('public'));
// GET /style.css -> public/style.css
// mount at a prefix
app.use('/static', express.static('public'));
// GET /static/style.css -> public/style.css
// multiple directories (fallthrough)
app.use(express.static('public'));
app.use(express.static('uploads'));
// absolute path + options
app.use(express.static(path.join(__dirname, 'public'), {
maxAge: '1d',
setHeaders: (res, filePath) => {
if (filePath.endsWith('.html')) res.setHeader('Cache-Control', 'no-cache');
}
}));错误处理与路由器
将错误传给 next(err)或在 async 处理器中 throw 以触发错误处理中间件。最后注册一个集中的 4 参数处理器。express.Router() 将路由模块化到单独文件 —— 用 app.use('/prefix', router) 组合大型 API。
// throw to jump to error middleware
app.get('/broken', (req, res, next) => {
const err = new Error('boom');
err.status = 400;
next(err); // or throw err (Express 4+)
});
// centralized error handler (last middleware)
app.use((err, req, res, next) => {
const status = err.status || 500;
res.status(status).json({ error: err.message });
});
// modular routing with express.Router()
const router = express.Router();
router.get('/', (req, res) => res.send('api root'));
router.get('/users', (req, res) => res.send('users'));
app.use('/api', router); // mount at /api流(Streams)
可读流
可读流产生数据。在 flowing 模式下,'data' 事件自动推送 chunk;在 paused 模式下,你显式调用 read()。务必处理 'error' —— 未处理的 error 事件会使进程崩溃。highWaterMark 控制内部缓冲区大小。
const fs = require('fs');
// create a readable stream from a file
const rs = fs.createReadStream('big.log', { highWaterMark: 64 * 1024 });
// 'data' mode (flowing)
rs.on('data', chunk => {
console.log('got', chunk.length, 'bytes');
});
rs.on('end', () => console.log('done'));
rs.on('error', err => console.error(err));
// paused mode: pull on demand
rs.on('readable', () => {
let chunk;
while ((chunk = rs.read()) !== null) {
process(chunk);
}
});可写流
可写流消费数据。write() 在内部缓冲区满时返回 false —— 等待 'drain' 事件再写更多以遵守背压。end() 表示不再有数据;'finish' 在所有数据冲刷后触发。process.stdout 本身就是一个可写流。
const fs = require('fs');
const ws = fs.createWriteStream('out.log');
// write returns false when the buffer is full (backpressure)
const ok = ws.write('line\n');
if (!ok) ws.once('drain', () => writeMore());
// signals completion
ws.end('final line\n');
ws.on('finish', () => console.log('all data flushed'));
ws.on('error', err => console.error(err));
// process.stdout is a writable stream
process.stdout.write('hello');pipe 与 pipeline
pipe() 将可读流转发到可写流并自动处理背压。stream.pipeline(优先使用 promise 版本)链式多个流并正确传播错误和清理 —— 单独的 .pipe() 在流中途出错时会泄漏资源。始终优先用 pipeline 而非手动 .pipe() 链。
const fs = require('fs');
const { pipeline } = require('stream/promises');
// pipe: readable -> writable (auto backpressure)
fs.createReadStream('in.txt')
.pipe(fs.createWriteStream('out.txt'));
// pipeline: chain transforms, with error handling
const { Transform } = require('stream');
const upper = new Transform({
transform(chunk, enc, cb) { cb(null, chunk.toString().toUpperCase()); }
});
await pipeline(
fs.createReadStream('in.txt'),
upper,
fs.createWriteStream('out.txt')
);
// pipeline rejects on error, unlike .pipe()转换流
转换流读取输入、处理后发出转换后的输出 —— 非常适合在管道中解析、压缩或加密。实现 transform(chunk, enc, cb) 以及可选的 flush(cb)。objectMode 让你推送对象而非 Buffer。zlib 和 crypto 提供现成的转换流。
const { Transform } = require('stream');
// a Transform is both Readable and Writable
const csvToJson = new Transform({
objectMode: false,
transform(chunk, encoding, callback) {
// convert each chunk
const lines = chunk.toString().trim().split('\n');
const json = lines.map(l => JSON.stringify(l.split(','))).join('\n');
callback(null, json + '\n');
},
flush(callback) {
callback(null, ']\n'); // finalize
}
});
process.stdin.pipe(csvToJson).pipe(process.stdout);
// built-in transforms: zlib, crypto streams
const { createGzip } = require('zlib');
fs.createReadStream('log').pipe(createGzip()).pipe(fs.createWriteStream('log.gz'));流模式与背压
流通过 pause()/resume() 在 flowing 和 paused 模式间切换。背压意味着慢消费者通知生产者减速 —— async 迭代(for await)通过 await 每个 chunk 自动处理,是最干净的现代模式。始终设置编码以获得字符串而非 Buffer。
const rs = fs.createReadStream('huge.bin');
// switch between modes
rs.pause();
rs.resume();
rs.setEncoding('utf8');
// respect backpressure manually
function writeChunk(ws, data) {
if (!ws.write(data)) {
rs.pause();
ws.once('drain', () => rs.resume());
}
}
// async iteration (Node 10+) — cleanest pattern
async function consume() {
for await (const chunk of rs) {
await handle(chunk); // naturally respects backpressure
}
}
consume();自定义流
继承 Readable/Writable/Duplex 并实现 _read/_write,或对已有数据使用更简单的 Readable.from()。push(null) 表示流结束。Duplex 流可独立地读和写(如 TCP socket);Transform 是输出由输入派生的 Duplex。
const { Readable, Writable, Duplex } = require('stream');
// custom Readable: generate a counter
const counter = new Readable({
read() {
if (++this._n > 10) this.push(null); // end
else this.push(String(this._n));
}
});
counter._n = 0;
counter.pipe(process.stdout);
// from an array / async iterable
Readable.from(['a', 'b', 'c']).pipe(process.stdout);
// a Duplex: both readable and writable (e.g. a socket)
class Echo extends Duplex {
_read() {}
_write(chunk, enc, cb) { this.push(chunk); cb(); }
}Buffer(缓冲区)
创建 Buffer
Buffer.alloc 创建零填充缓冲区(安全)。Buffer.allocUnsafe 跳过零填充以提速,但可能泄露旧内存 —— 仅在你将覆盖每个字节时使用。Buffer.from 转换字符串/数组/缓冲区。旧的 'new Buffer()' 构造器因安全问题已废弃。
// allocate zeroed bytes (SAFE)
const b1 = Buffer.alloc(8); // <Buffer 00 00 00 00 00 00 00 00>
// allocate WITHOUT zeroing (faster, may contain old memory)
const b2 = Buffer.allocUnsafe(8); // random old data!
// from a string with encoding
const b3 = Buffer.from('hello', 'utf8');
// <Buffer 68 65 6c 6c 6f>
// from an array of bytes
const b4 = Buffer.from([0x48, 0x49]); // 'HI'
// from another buffer (copy)
const b5 = Buffer.from(b3);
// NEVER use new Buffer() — deprecated and unsafe读取与写入
Buffer 保存原始字节。使用带显式大小(UInt8/16/32)和字节序(BE/LE)的读/写方法。大端序是网络字节序;小端序在 x86 上常见。toString(encoding, start, end) 将字节解码为字符串。始终传入偏移量以避免覆盖前面的字节。
const buf = Buffer.alloc(8);
// write numeric values at an offset
buf.writeUInt8(255, 0); // 1 byte
buf.writeUInt16BE(0x1234, 1); // 2 bytes, big-endian
buf.writeInt32LE(1000, 3); // 4 bytes, little-endian
// read them back
buf.readUInt8(0); // 255
buf.readUInt16BE(1); // 0x1234
buf.readInt32LE(3); // 1000
// string read/write
buf.write('Hi', 0, 'utf8');
buf.toString('utf8', 0, 2); // 'Hi'拼接与比较
Buffer.concat 拼接缓冲区数组(可选的总长度提示可提升性能)。equals/compare 让你比较字节序列;indexOf/includes 查找子缓冲区 —— 在二进制协议中很有用。copy(target, targetStart, sourceStart, sourceEnd) 将切片复制到另一个缓冲区。
const a = Buffer.from('foo');
const b = Buffer.from('bar');
// concatenate buffers
const c = Buffer.concat([a, b]); // 'foobar'
Buffer.concat([a, b], 6); // total length hint
// compare
Buffer.compare(a, b); // -1 (a < b), 0, or 1
a.equals(b); // false
// find a sub-buffer
const big = Buffer.from('hello world');
big.indexOf(Buffer.from('world')); // 6
big.includes(Buffer.from('hello')); // true
// copy bytes
const target = Buffer.alloc(5);
big.copy(target, 0, 0, 5); // copy first 5 bytes编码
编码在字符串和字节之间映射。utf8 是文本默认。base64/hex 常用于文本中的二进制(data URL、哈希、传输)。Buffer.byteLength 给出字节大小(utf8 中多字节字符多于一个字节)—— 在为缓冲区分配大小时使用它,而非 string.length。
const text = 'Hello, 世界';
// string -> buffer (encode)
const utf8 = Buffer.from(text, 'utf8');
const base64 = Buffer.from(text).toString('base64');
const hex = Buffer.from(text).toString('hex');
const latin1 = Buffer.from(text, 'latin1');
// buffer -> string (decode)
utf8.toString('utf8'); // 'Hello, 世界'
Buffer.from(base64, 'base64').toString('utf8');
Buffer.from(hex, 'hex').toString('utf8');
// common encodings: utf8, utf16le, latin1, ascii,
// base64, base64url, hex
Buffer.byteLength('世界', 'utf8'); // 6 bytes (not 2)Buffer 与 TypedArrays
Buffer 继承 Uint8Array,因此可在任何使用 TypedArray 的地方使用。Buffer.from(arrayBuffer) 共享内存(无拷贝)—— 但 Buffer 可能切片自更大的池化 ArrayBuffer,因此创建视图时务必遵守 byteOffset/byteLength。这让你将 Buffer API 与 DataView 混用以进行细粒度数值访问。
// Buffer is a subclass of Uint8Array
const buf = Buffer.from([1, 2, 3]);
buf instanceof Uint8Array; // true
// share memory with TypedArrays without copying
const u8 = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
view.getInt32(0);
// Buffer from an ArrayBuffer
const ab = new ArrayBuffer(8);
const buf2 = Buffer.from(ab);
// note: a Buffer's underlying buffer may be larger
// (allocUnsafe pools) -> always use byteOffset切片、填充、交换
subarray(或 slice)返回共享原始内存的视图 —— 修改会影响两者。fill 在缓冲区中写入一个字节或重复字符串。swap16/swap32 原地反转字节序以转换字节序。Buffer 可迭代,产出每个字节值 0–255。
const buf = Buffer.from('abcdef');
// slice shares memory (no copy) — like subarray
const s = buf.subarray(1, 4); // 'bcd'
s[0] = 88; // mutates buf too -> 'aXcdef'
// fill with a value or pattern
Buffer.alloc(4).fill(0xff); // <Buffer ff ff ff ff>
Buffer.alloc(6).fill('ab'); // 'ababab'
// reverse byte order in place (for endianness swaps)
const n = Buffer.from([0x01, 0x02, 0x03, 0x04]);
n.swap16(); // swaps pairs: <Buffer 02 01 04 03>
// iterate with for...of
for (const byte of buf) console.log(byte); // 97, 98, ...EventEmitter(事件)
创建与触发事件
EventEmitter 是 Node 的发布/订阅核心。emit(eventName, ...args) 触发事件;on() 订阅。许多内置类(流、http 服务器)继承它。事件名是任意字符串,但 'error' 是特殊的。监听器按注册顺序同步调用。
const { EventEmitter } = require('events');
class Clock extends EventEmitter {
start() {
this.emit('start', Date.now()); // emit with args
setInterval(() => this.emit('tick', new Date()), 1000);
}
}
const clock = new Clock();
clock.on('tick', date => console.log('tick', date));
clock.on('start', ts => console.log('started at', ts));
clock.start();
// on() == addListener; once() fires only onceonce 与 removeListener
once() 注册的监听器在触发后自动移除 —— 适合一次性初始化。移除监听器必须保留对同一函数的引用(箭头函数每次创建新引用)。removeAllListeners 清除一个或所有事件的监听器。off 是 removeListener 的别名。
const ee = new EventEmitter();
// once: auto-removed after first fire
ee.once('init', () => console.log('init fired'));
// remove a specific listener (need the same function ref)
const handler = () => console.log('hi');
ee.on('msg', handler);
ee.off('msg', handler); // off() == removeListener
// remove all listeners for an event (or all events)
ee.removeAllListeners('msg');
ee.removeAllListeners(); // everything
// inspect
ee.listenerCount('msg');
ee.eventNames(); // ['msg', 'init']错误事件
如果触发 'error' 事件且未注册监听器,Node 会抛出并使进程崩溃。务必为 EventEmitter(尤其是流和 HTTP 服务器)附加错误处理器。captureRejections(Node 12+)将 async 监听器的 rejected promise 路由到 'error' 事件。
const ee = new EventEmitter();
// 'error' is special: if unhandled, it CRASHES the process
ee.on('error', err => {
console.error('caught:', err.message);
});
// later, emit an error safely
ee.emit('error', new Error('something broke'));
// guard against crashes from unhandled errors
// (monitor with the static capture)
EventEmitter.captureRejections = true; // for async handlers
// always register an error listener on streams/http异步监听器
EventEmitter 同步调用监听器,因此 async 监听器的 promise 不会被 await —— emit() 在其 settle 前就返回了。用 once(ee, name) 将单个事件作为 promise 等待。启用 captureRejections,使 rejected 的 async 监听器触发 'error' 事件而非被静默丢弃。
const ee = new EventEmitter();
// listeners run synchronously; await doesn't pause the chain
ee.on('save', async () => {
await fs.promises.writeFile('x', 'y');
console.log('saved');
});
ee.emit('save'); // returns before the await finishes
// wait for async listeners in sequence
const { once } = require('events');
async function waitForReady() {
await once(ee, 'ready'); // promise resolves on first 'ready'
console.log('now ready');
}
// handle async errors properly
EventEmitter.captureRejections = true;
ee.on('job', async () => { throw new Error('boom'); });
ee.on('error', err => console.error(err));事件命名与 newListener
newListener 和 removeListener 是在添加/移除监听器时触发的特殊事件 —— 适用于钩子或分析。setMaxListeners 提高泄漏检测阈值(默认 10);该警告通常意味着你忘记清理监听器。用 Symbol 或常量作为事件名以防止拼写错误。
const ee = new EventEmitter();
// special events fired by EventEmitter itself
ee.on('newListener', (event, listener) => {
console.log('listener added for', event);
});
ee.on('removeListener', (event, listener) => {
console.log('listener removed for', event);
});
// limit listener count to catch leaks
ee.setMaxListeners(20);
ee.getMaxListeners(); // 20
// MaxListenersExceededWarning fires if exceeded
// convention: camelCase event names, 'error' reserved
// prefer symbols/const strings to avoid typos
const READY = Symbol('ready');
ee.emit(READY);子进程
exec(shell、缓冲)
exec() 通过 shell 运行命令并缓冲 stdout/stderr —— 适合你想一次性获取所有输出的短命令。它会启动 shell,因此会扩展通配符和管道。绝不要将用户输入插入命令字符串 —— 这是命令注入漏洞;改用带参数数组的 spawn。
const { exec } = require('child_process');
// runs a command in a shell, buffers all output
exec('ls -la', (err, stdout, stderr) => {
if (err) { console.error('failed:', err); return; }
console.log(stdout); // full stdout as a string
console.error(stderr);
});
// promisified (Node 12+)
const { promisify } = require('util');
const execP = promisify(exec);
const { stdout } = await execP('git rev-parse HEAD');
// WARNING: shell interpolation = injection risk!
// NEVER: exec(`rm ${userInput}`)spawn(流式、无 shell)
spawn() 用参数数组启动进程(无 shell)并流式输出 stdout/stderr —— 对大型或长时间运行的输出安全且内存高效。因为参数直接传递(不经过 shell),没有通配符扩展或注入风险。可将子进程流 pipe 到其他流以构建管道。
const { spawn } = require('child_process');
// no shell by default -> safe, stream-based
const child = spawn('ls', ['-la', '/tmp']);
child.stdout.on('data', chunk => process.stdout.write(chunk));
child.stderr.on('data', chunk => process.stderr.write(chunk));
child.on('close', code => console.log('exit code', code));
child.on('error', err => console.error('failed to spawn', err));
// pipe streams directly
const gzip = spawn('gzip');
process.stdin.pipe(gzip.stdin);
gzip.stdout.pipe(process.stdout);execFile 与 fork
execFile 不经 shell 运行可执行文件 —— 比 exec 运行已知二进制带参数更安全。fork() 启动一个 Node 进程并打开内置 IPC 通道(send/on('message')),是 cluster 和 worker_threads 类跨 Node 进程消息传递的基础。
const { execFile, fork } = require('child_process');
// execFile: like exec but no shell (safer, slight overhead saving)
execFile('node', ['--version'], (err, stdout) => {
console.log(stdout.trim()); // v20.x.x
});
// fork: spawn another Node process with an IPC channel
const worker = fork('./worker.js');
worker.send({ task: 'compute', n: 100 });
worker.on('message', msg => console.log('result:', msg));
worker.on('exit', code => console.log('worker exited', code));
// in worker.js:
// process.on('message', msg => { process.send(result); });stdio 与参数
stdio 控制子进程的标准流:'inherit' 共享父进程终端(适合 npm 脚本),'ignore' 丢弃,'pipe' 捕获。detached + unref() 启动独立于父进程的后台进程。参数作为数组传递 —— spawn 为你加引号,因此空格和特殊字符是安全的。
const { spawn } = require('child_process');
// control stdio: 'pipe' (default), 'inherit', 'ignore', or fd
const child = spawn('npm', ['test'], {
stdio: 'inherit' // child uses parent's stdin/stdout/stderr
});
// per-stream: [stdin, stdout, stderr]
spawn('cat', [], { stdio: ['pipe', process.stdout, 'ignore'] });
// set env, cwd, and detached background process
const bg = spawn('node', ['server.js'], {
cwd: '/app',
env: { ...process.env, NODE_ENV: 'production' },
detached: true // runs independently of parent
});
bg.unref(); // let parent exit without waiting
// pass arguments safely as array (no shell parsing)
spawn('echo', ['hello world']); // one arg, no quotes needed退出码与信号
'exit' 事件报告 code(被信号杀死时为 null)和 signal(正常退出时为 null)。'close' 在所有 stdio 流关闭后触发 —— 当你 pipe 了输出并需要知道流已冲刷时使用它。kill() 发送信号;SIGTERM 是优雅的,SIGKILL 无法捕获。
const child = spawn('sleep', ['60']);
// 'exit' fires with code + signal
child.on('exit', (code, signal) => {
if (code === 0) console.log('success');
else if (signal) console.log('killed by', signal);
else console.log('failed with code', code);
});
// send a signal to the child (SIGTERM by default)
setTimeout(() => child.kill('SIGTERM'), 1000);
child.kill(); // default SIGTERM
child.kill('SIGKILL'); // force-kill (cannot be caught)
// 'close' vs 'exit': 'close' fires after streams are closed
child.on('close', code => console.log('streams closed', code));
// check if process is still running
child.killed; // true after kill() called集群(Cluster)
基础集群
cluster 让你 fork 多个 Node 进程共享一个端口,跨 CPU 核心扩展。primary fork worker(通常是 os.cpus().length)。每个 worker 是运行同一脚本的完整 Node 进程。在 'exit' 时重启 worker 以增强韧性。用 cluster.isPrimary(原 isMaster)分支逻辑。
const cluster = require('cluster');
const http = require('http');
const os = require('os');
if (cluster.isPrimary) { // old: isMaster
// fork one worker per CPU core
for (let i = 0; i < os.cpus().length; i++) cluster.fork();
cluster.on('exit', (worker, code) => {
console.log(`worker ${worker.process.pid} died`);
cluster.fork(); // restart it
});
} else {
// workers share the same port
http.createServer((req, res) => res.end('hi'))
.listen(3000);
console.log('worker started', process.pid);
}Worker 管理
primary 通过 'online'(进程已启动)、'listening'(服务器已绑定)、'disconnect' 和 'exit' 事件跟踪 worker。disconnect() 在杀死 worker 前先排空连接 —— 用于零停机重载。cluster.workers 将 worker ID 映射到 Worker 对象,便于发送消息或关闭。
cluster.on('online', worker => {
console.log('worker responsive', worker.id);
});
cluster.on('listening', (worker, address) => {
console.log('worker bound to', address.port);
});
cluster.on('disconnect', worker => {
console.log('worker disconnected', worker.id);
});
// gracefully shut down a worker
const w = cluster.workers[1];
w.disconnect(); // stop accepting connections, then exit
w.send('shutdown'); // custom message
// iterate all workers
for (const id in cluster.workers) {
cluster.workers[id].send({ cmd: 'reload' });
}共享服务器端口
所有 worker 监听同一端口;primary 负载均衡传入连接。SCHED_RR(轮询,多数平台默认)均匀分配;SCHED_NONE 交给 OS。worker 之间不共享状态 —— 用 Redis 或数据库共享会话/缓存。
const cluster = require('cluster');
const http = require('http');
if (cluster.isPrimary) {
cluster.schedulingPolicy = cluster.SCHED_RR; // round-robin (default on non-Windows)
cluster.fork();
cluster.fork();
} else {
// all workers call listen(3000) — primary load-balances
http.createServer((req, res) => {
res.end(`handled by ${process.pid}`);
}).listen(3000);
}
// SCHED_RR: primary distributes connections (default)
// SCHED_NONE: OS balances (may be uneven)Worker 生命周期与 IPC
cluster.fork() 打开 IPC 通道(类似 child_process.fork):worker.send 和 process.on('message') 在 primary 与 worker 间交换 JSON。用于派发任务或通知优雅关闭。对 CPU 密集型工作也可考虑 worker_threads,它共享内存并避免 IPC 开销。
// primary -> worker messaging
if (cluster.isPrimary) {
const worker = cluster.fork();
worker.on('message', msg => console.log('from worker:', msg));
worker.send({ task: 'heavy-compute' });
} else {
process.on('message', msg => {
console.log('from primary:', msg);
// do work, then report back
process.send({ done: true, result: 42 });
});
}
// graceful shutdown signal
process.on('message', msg => {
if (msg === 'shutdown') {
server.close(() => process.exit(0));
}
});优雅重载(零停机)
滚动重启一次重载一个 worker,使应用永不停机:断开一个 worker(它停止接受新连接),等待它排空后退出,再 fork 一个替代者。用 SIGHUP 触发。始终配合每个 worker 的 'shutdown' 消息,使每个 worker 仅在处理中的请求完成后才关闭服务器并退出。
// rolling restart: reload workers one at a time
function reload() {
const workers = Object.values(cluster.workers || {});
let i = 0;
const next = () => {
if (i >= workers.length) return;
const w = workers[i++];
w.send('shutdown'); // tell worker to drain
w.disconnect(); // stop accepting connections
w.once('exit', () => {
cluster.fork(); // replacement ready
next(); // move to the next
});
};
next();
}
// trigger reload on SIGHUP
process.on('SIGHUP', reload);
// in the worker: finish in-flight requests then exit
process.on('message', msg => {
if (msg === 'shutdown') server.close(() => process.exit(0));
});异步编程
回调与错误优先约定
Node 经典异步模式是错误优先回调:第一个参数是错误(成功时为 null),后续参数是结果。始终先检查 err 并提前返回。这种风格随嵌套扩展性差 —— 即臭名昭著的回调地狱 —— 因此现代代码使用 Promise 和 async/await。
// Node callbacks: (err, result) => {}
const fs = require('fs');
fs.readFile('data.txt', 'utf8', (err, data) => {
if (err) return console.error('read failed', err);
console.log(data);
});
// custom error-first function
function divide(a, b, cb) {
if (b === 0) return cb(new Error('divide by zero'));
cb(null, a / b);
}
divide(10, 2, (err, result) => {
if (err) return console.error(err);
console.log(result); // 5
});Promise
Promise 表示一个未来的值。then() 处理成功,catch() 处理拒绝,finally() 无论如何都运行。链式 then() 顺序转换值。util.promisify 将错误优先回调函数转为返回 Promise 的函数 —— 将遗留 API 桥接到 async/await。
// create a promise
const p = new Promise((resolve, reject) => {
setTimeout(() => resolve('done'), 100);
});
// consume with then/catch/finally
p.then(val => console.log(val)) // 'done'
.catch(err => console.error(err))
.finally(() => console.log('settled'));
// chain transforms
fetch(url)
.then(r => r.json())
.then(data => render(data));
// convert callback APIs
const { promisify } = require('util');
const readFile = promisify(fs.readFile);
const text = await readFile('x.txt', 'utf8');async/await
async/await 让异步代码看起来同步。async 函数返回 Promise;await 暂停直到其 settle。用 try/catch 包裹 await 调用以处理拒绝。顶层 await 在 ES 模块中允许,(带标志)在 CommonJS 中也可用,消除了 async IIFE 包装的需要。
// async functions always return a Promise
async function loadUser(id) {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error('not found');
return res.json();
}
// consume with try/catch
try {
const user = await loadUser(5);
console.log(user);
} catch (err) {
console.error('failed:', err);
}
// top-level await works in ESM and CommonJS (Node 14.8+)
const config = await loadConfig();Promise.all 与 allSettled
Promise.all 并行运行 promise 并以结果数组解析,但任一 promise 拒绝时立即拒绝(快速失败)—— 在所有都必须成功时使用。Promise.allSettled 无论结果如何都等待每个 promise,返回 {status, value/reason} 对象 —— 在部分成功也可接受时最佳。
// all: run in parallel, reject fast on first failure
const [a, b, c] = await Promise.all([
fetch('/a').then(r => r.json()),
fetch('/b').then(r => r.json()),
fetch('/c').then(r => r.json())
]);
// allSettled: wait for ALL, never rejects
const results = await Promise.allSettled([
fetch('/x'),
fetch('/y'),
fetch('/z')
]);
results.forEach(r => {
if (r.status === 'fulfilled') console.log(r.value);
else console.error(r.reason); // rejected reason
});Promise.race 与 Promise.any
Promise.race 以第一个 settle(resolve 或 reject)的 promise 为准 —— 适用于超时和竞速镜像。Promise.any 以第一个成功的为准,仅当所有 promise 都失败时才拒绝(AggregateError)—— 适合冗余端点,一个健康响应即足够。
// race: first to SETTLE (resolve or reject) wins
const fastest = await Promise.race([
fetch('/primary'),
fetch('/mirror'),
timeout(5000)
]);
// any: first to FULFILL wins; rejects only if ALL reject
const alive = await Promise.any([
fetch('/node1'),
fetch('/node2'),
fetch('/node3')
]);
// implement a timeout with race
function withTimeout(p, ms) {
return Promise.race([
p,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('timeout')), ms))
]);
}顺序与并行
在循环中 await 是顺序执行(慢)。映射为 promise 数组并用 Promise.all 并行(快,但无界并发可能压垮资源)。并发限制池可限制并行工作 —— 在调用限速 API 或对数千项做重 I/O 时必不可少。
// SEQUENTIAL: one after another (slow)
const results = [];
for (const url of urls) {
results.push(await fetch(url).then(r => r.json()));
}
// PARALLEL: fire all, await together (fast)
const results = await Promise.all(
urls.map(u => fetch(u).then(r => r.json()))
);
// THROTTLED: limit concurrency with a pool
async function mapLimit(items, limit, fn) {
const ret = [];
const executing = [];
for (const item of items) {
const p = Promise.resolve().then(() => fn(item));
ret.push(p);
if (executing.push(p) >= limit) {
await Promise.race(executing);
executing.splice(executing.findIndex(x => x === p), 1);
}
}
return Promise.all(ret);
}错误处理
try/catch/finally
try/catch 处理同步错误和(配合 await)异步拒绝。finally 无论 return/throw 都运行 —— 用于关闭句柄等清理。通过检查 err.code 捕获特定错误,并重新抛出你不认识的错误,让意外 bug 浮现而非被吞掉。
function parse(str) {
try {
return JSON.parse(str);
} catch (err) {
console.error('parse failed:', err.message);
return null;
} finally {
console.log('cleanup always runs');
}
}
// async errors need async catch
async function load() {
try {
const data = await fetchData();
return data;
} catch (err) {
if (err.code === 'ENOENT') return defaultData();
throw err; // re-throw unknown errors
}
}错误优先回调
在错误优先回调中,始终先检查 err 并提前返回 —— err 存在时绝不使用 data。包装回调 API 时,将错误转发给你自己的回调而非抛出(回调内的抛出无法被调用者捕获)。用 try/catch 包裹可能抛出的同步代码(如 JSON.parse)。
const fs = require('fs');
// ALWAYS check err first, return early
fs.readFile('missing.txt', 'utf8', (err, data) => {
if (err) {
console.error('could not read:', err.message);
return; // don't use data
}
console.log(data);
});
// propagate the error to your own caller
function readConfig(cb) {
fs.readFile('config.json', 'utf8', (err, raw) => {
if (err) return cb(err);
try { cb(null, JSON.parse(raw)); }
catch (e) { cb(e); }
});
}未捕获异常
uncaughtException 在同步异常逃逸所有处理器时触发 —— 进程现已损坏;记录日志、关闭服务器并退出(让进程管理器重启你)。unhandledRejection 在被遗忘的 promise 上触发;自 Node 15 起这些会使进程崩溃,因 此始终添加 .catch() 或在 try/catch 中 await。
// last-resort handler for sync exceptions
process.on('uncaughtException', err => {
console.error('UNCAUGHT:', err);
// the process is in an unknown state — restart it
server.close(() => process.exit(1));
});
// rejected promises nobody awaited
process.on('unhandledRejection', (reason, promise) => {
console.error('UNHANDLED REJECTION:', reason);
});
// Node 15+: unhandled rejections terminate the process
// always add .catch() or wrap in try/await自定义错误类
继承 Error 以添加结构化字段(code、field、status),并用 instanceof 区分错误类型。始终将 this.name 设为类名(旧 target 下不自动),并调用 super(message)。自定义错误让 catch 块可读:基于 instanceof 分支而非脆弱的字符串匹配。
// subclass Error to add context
class ValidationError extends Error {
constructor(field, message) {
super(message);
this.name = 'ValidationError';
this.field = field;
this.code = 'VALIDATION_FAILED';
}
}
function validate(email) {
if (!email.includes('@')) {
throw new ValidationError('email', 'invalid email');
}
}
// branch on error type with instanceof
try {
validate('no-at-sign');
} catch (err) {
if (err instanceof ValidationError) {
console.log(err.field, err.message);
} else {
throw err;
}
}错误属性与 cause
每个 Error 都有 message、name 和 stack。添加 code 属性以实现稳定的机器可读处理(Node 自身错误使用 ENOENT、EACCES 等代码)。ES2022 的 cause 选项(Node 16.9+)链式底层错误,让你包装低层失败同时保留原始堆栈便于调试。
// built-in Error props
const e = new Error('something broke');
e.message; // 'something broke'
e.stack; // stack trace string
e.name; // 'Error'
// add a code for machine-readable handling
const err = new Error('file missing');
err.code = 'ENOENT';
err.errno = -2;
// Error.cause (Node 16.9+) — chain underlying errors
try {
JSON.parse(bad);
} catch (original) {
throw new Error('config parse failed', { cause: original });
}
// inspect the cause
console.log(err.cause); // the original SyntaxErrorDomains 与清理
用 'beforeExit' 做异步清理(可推迟退出),'exit' 仅做同步工作。处理 SIGTERM(容器关闭)和 SIGINT(Ctrl-C)以在退出前排空连接 —— 添加强制退出超时,使卡住的连接不会困住进程。Domains 已废弃;改用 try/catch 和 async 错误处理。
// modern cleanup: 'beforeExit' for async, 'exit' for sync
process.on('beforeExit', async () => {
await closeDbConnections();
});
process.on('exit', code => {
// ONLY sync code allowed here — no async, no promises
console.log('exiting with', code);
});
// graceful signal handling
const shutdown = sig => {
console.log('got', sig);
server.close(() => process.exit(0));
setTimeout(() => process.exit(1), 5000).unref(); // force exit
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));调试
console 方法
console 不止 log:table 渲染对象数组,time/timeEnd 测量时长,dir 以可配置深度转储对象,trace 打印堆栈,count 计数。使用 console.error(stderr)以便在生产管道中分离日志和错误。
console.log('basic'); // stdout
console.error('oops'); // stderr
console.warn('careful'); // stderr
// formatted table
console.table([{ id: 1, n: 'A' }, { id: 2, n: 'B' }]);
// inspect an object in depth
console.dir(obj, { depth: null, colors: true });
// timing a block
console.time('loop');
for (let i = 0; i < 1e6; i++) {}
console.timeEnd('loop'); // loop: 3.2ms
// stack trace
console.trace('where am I');
// count calls
for (let i = 0; i < 3; i++) console.count('hit');node inspect 与 DevTools
--inspect 启动 V8 inspector,使 Chrome DevTools(chrome://inspect)或 VS Code 可附加。--inspect-brk 在第一行暂停,以便在启动代码运行前设置断点。经典 'node inspect' 是终端调试器,在无浏览器的 SSH 环境中有用。
# open Chrome DevTools debugger
node --inspect app.js
# visit chrome://inspect, click "inspect"
# pause on first line (break before anything runs)
node --inspect-brk app.js
# wait for a debugger client to attach
node --inspect-wait app.js
# the legacy CLI debugger (still works, no browser)
node inspect app.js
# > cont, step, next, breakpoints, repldebugger 语句
debugger 语句仅在附加调试器(DevTools 或 VS Code)时暂停执行 —— 否则是空操作,留在代码中安全。配合 --inspect-brk 在启动时停止。对于临时检查,在循环或条件中放入 debugger; 而非设置 UI 断点。
function findBug(arr) {
let sum = 0;
for (const n of arr) {
debugger; // pauses only when a debugger is attached
sum += n;
}
return sum;
}
// run with --inspect-brk to hit the breakpoint
// node --inspect-brk app.js
// conditional breakpoints in DevTools/VSCode:
// if (n > 100) { debugger; }VS Code 调试
VS Code 的 Node 调试器(F5)读取 .vscode/launch.json。"request": "launch" 内置 inspector 运行程序;"attach" 连接到已用 --inspect 启动的进程。skipFiles 在单步时隐藏 Node 内部。用 "restart" 标志或 nodemon 在文件变更时自动重启。
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug App",
"program": "${workspaceFolder}/app.js",
"skipFiles": ["<node_internals>/**"]
},
{
"type": "node",
"request": "attach",
"name": "Attach by PID",
"processId": "${command:PickProcess}"
}
]
}
// F5 to start; set breakpoints in the editor
// "restart": true, "nodemon" for auto-restart on change警告与堆栈跟踪
--trace-warnings 打印进程警告(内存泄漏、废弃 API)的堆栈。--trace-deprecation 显示遗留 API 用法;--throw-deprecation 将其变为硬错误以用于更严格的 CI。--cpu-prof 写入可在 Chrome DevTools 中打开的性能分析文件以查找热点函数。调高 Error.stackTraceLimit 以获得更深的异步堆栈。
# show stack traces for process warnings
node --trace-warnings app.js
# deprecation warnings (use --throw-deprecation to make them errors)
node --trace-deprecation app.js
# capture a stack trace manually
const e = new Error('here');
console.log(e.stack);
# show async stack traces (Node 12+ enabled by default)
Error.stackTraceLimit = 50; // deeper traces
# profile CPU usage
node --cpu-prof app.js # writes CPU.*.cpuprofile
# then load the file in Chrome DevTools加密(Crypto)
哈希(createHash)
createHash 产生单向摘要。SHA-256 是校验和与签名的现代默认;安全场景避免 MD5/SHA-1(易碰撞)。可增量喂数据并无缓冲地流式处理大文件。digest('hex') 返回十六进制字符串;'base64' 也常见。哈希不是密码存储 —— 请用 pbkdf2/scrypt。
const crypto = require('crypto');
// common hashes: sha256, sha512, md5 (insecure!), sha1
const hash = crypto.createHash('sha256');
hash.update('hello');
hash.update(' world'); // can chain updates
console.log(hash.digest('hex'));
// b94d... (64 hex chars)
// one-shot hashing
const h = crypto.createHash('sha256').update('data').digest('hex');
console.log(h);
// file hash (streaming)
const fs = require('fs');
const fhash = crypto.createHash('sha256');
fs.createReadStream('big.iso').on('data', c => fhash.update(c))
.on('end', () => console.log(fhash.digest('hex')));HMAC(createHmac)
HMAC 用共享密钥签名数据,使接收方能验证完整性和真实性。它是 JWT 签名和 webhook 验证(GitHub、Stripe)的基础。始终用 timingSafeEqual 比较签名 —— 普通 === 会通过攻击者可利用的时序差异泄露信息。
const crypto = require('crypto');
// HMAC = keyed hash (authenticity + integrity)
const secret = process.env.SECRET_KEY;
const hmac = crypto.createHmac('sha256', secret);
hmac.update('payload to sign');
const signature = hmac.digest('hex');
// verify a webhook signature (constant-time compare)
function verify(payload, receivedSig) {
const expected = crypto.createHmac('sha256', secret)
.update(payload).digest('hex');
// timingSafeEqual prevents timing attacks
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(receivedSig)
);
}随机字节
crypto.randomBytes 产生密码学安全的随机数据 —— 用于令牌、ID 和密钥。异步形式避免在生成大量字节时阻塞事件循环。crypto.randomInt 给出无偏随机整数,randomUUID() 生成 RFC-4122 v4 UUID。绝不要用 Math.random() 处理任何安全相关内容。
const crypto = require('crypto');
// async (preferred — doesn't block the event loop)
crypto.randomBytes(16, (err, buf) => {
const token = buf.toString('hex'); // 32-char hex
});
// sync (blocks; fine for small sizes)
const id = crypto.randomBytes(8).toString('hex');
// random integer in [min, max)
const n = crypto.randomInt(1, 101); // 1..100
// random UUID (Node 14.17+)
const uuid = crypto.randomUUID();
// '1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed'
// use crypto, NEVER Math.random() for secrets!加密(createCipheriv)
AES-256-GCM 是推荐的对称加密 —— 它是经过认证的,可检测篡改。IV(nonce)在给定密钥下每次加密必须唯一(切勿复用);与密文一起存储。setAuthTag 在解密时验证完整性。绝不用已废弃的 createCipher(它不安全地派生密钥)—— 始终用 createCipheriv 配合正确密钥。
const crypto = require('crypto');
const algorithm = 'aes-256-gcm';
// AES-256-GCM (authenticated encryption) ----------------
const key = crypto.randomBytes(32); // 256-bit key
const iv = crypto.randomBytes(12); // 96-bit nonce
function encrypt(text) {
const cipher = crypto.createCipheriv(algorithm, key, iv);
const enc = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag(); // GCM auth tag
return { enc, tag };
}
function decrypt(enc, tag) {
const decipher = crypto.createDecipheriv(algorithm, key, iv);
decipher.setAuthTag(tag); // verify integrity
return Buffer.concat([decipher.update(enc), decipher.final()]).toString('utf8');
}密码哈希(pbkdf2、scrypt)
用慢速、带盐的 KDF 哈希密码 —— 绝不用普通哈希。pbkdf2(随硬件提升调高迭代次数)和 scrypt(内存硬、抗 GPU)内置于 Node。将迭代次数、盐和哈希一起存储,以便验证并日后升级参数。用 timingSafeEqual 检查哈希而不泄露时序信息。
const crypto = require('crypto');
// PBKDF2: tuned with iterations
function hashPbkdf2(password) {
const salt = crypto.randomBytes(16);
const iterations = 100000;
const hash = crypto.pbkdf2Sync(password, salt, iterations, 64, 'sha256');
return `${iterations}.${salt.toString('hex')}.${hash.toString('hex')}`;
}
// scrypt: memory-hard, resistant to GPU/ASIC attacks
function hashScrypt(password) {
const salt = crypto.randomBytes(16);
const hash = crypto.scryptSync(password, salt, 64, { N: 16384 });
return salt.toString('hex') + '.' + hash.toString('hex');
}
// verify: re-hash with the same salt and compare
function verify(password, stored) {
const [iter, salt, hash] = stored.split('.');
const computed = crypto.pbkdf2Sync(password, Buffer.from(salt, 'hex'),
+iter, 64, 'sha256');
return crypto.timingSafeEqual(Buffer.from(hash, 'hex'), computed);
}计时安全比较
timingSafeEqual 以常数时间比较两个缓冲区,防止攻击者通过响应延迟逐字节推断密钥的时序攻击。两个缓冲区必须等长(先检查,但长度检查本身可能泄露长度 —— 对于密钥,先对两边哈希)。在比较令牌、签名或 API 密钥时使用它。
const crypto = require('crypto');
// NORMAL compare leaks via timing (returns early on first diff)
// 'secret' === userInput // BAD for secrets
// timingSafeEqual takes equal-length Buffers
function safeEqual(a, b) {
const bufA = Buffer.from(a);
const bufB = Buffer.from(b);
if (bufA.length !== bufB.length) return false;
return crypto.timingSafeEqual(bufA, bufB);
}
// common use: API key check
function auth(req) {
const provided = req.headers['x-api-key'] || '';
return safeEqual(provided, process.env.API_KEY);
}OS 模块
系统信息
os 暴露宿主环境。platform/arch/type 标识 OS 以便做可移植性检查。os.EOL 给出平台的行尾序列 —— 在写平台正确的文本文件时使用。hostname 是机器的网络名,适合记录哪个实例处理了请求。
const os = require('os');
os.platform(); // 'linux', 'darwin', 'win32'
os.arch(); // 'x64', 'arm64'
os.type(); // 'Linux', 'Darwin', 'Windows_NT'
os.release(); // kernel version
os.hostname(); // machine hostname
os.uptime(); // seconds since boot
os.EOL; // '\n' (POSIX) or '\r\n' (Windows)
// detect platform portably
const isWin = os.platform() === 'win32';
const isMac = os.platform() === 'darwin';CPU 信息
os.cpus() 返回逻辑核心(含超线程)数组,其长度是 cluster.fork 的正确并发提示。每个条目有 model、speed 和 times(user/sys/idle)。loadavg 报告系统负载(仅 Linux/macOS)—— 值高于 cpus().length 表示饱和。
const os = require('os');
// logical core count (threads, not physical)
os.cpus().length; // 8
// detailed per-core info
os.cpus().forEach((cpu, i) => {
console.log(`core ${i}: ${cpu.model} @ ${cpu.speed}MHz`);
});
// 1/5/15-minute load averages (NOT on Windows)
os.loadavg(); // [0.45, 0.32, 0.28]
// set process scheduling priority
os.setPriority(os.constants.priority.PRIORITY_BELOW_NORMAL);内存
os.totalmem/freemem 报告系统内存,而 process.memoryUsage 报告本进程占用。rss 是 OS 级内存;heapUsed 是活动 JS 对象。监控 heapUsed 以发现泄漏。要将字节转为 MB,除以 1024*1024(1048576)。堆接近上限时调高 --max-old-space-size。
const os = require('os');
// system memory (bytes)
os.totalmem(); // e.g. 17179869184
os.freemem(); // currently free bytes
// process memory usage (bytes)
const m = process.memoryUsage();
m.rss; // resident set size (total RAM held)
m.heapTotal; // V8 heap allocated
m.heapUsed; // V8 heap actually used
m.external; // C++ objects bound to JS
m.arrayBuffers; // memory in ArrayBuffers
// human-readable
console.log(`heap: ${(m.heapUsed / 1048576).toFixed(1)} MB`);网络接口
os.networkInterfaces() 列出每个网络接口及其地址、MAC 和 family(IPv4/IPv6)。'internal' 标志标记回环。它是可移植地发现机器 LAN IP 以绑定服务器或日志的方式。每个接口可有多个地址(多宿主主机)。
const os = require('os');
const nets = os.networkInterfaces();
for (const name of Object.keys(nets)) {
for (const net of nets[name]) {
console.log(`${name} ${net.family}: ${net.address} (${net.mac})`);
}
}
// en0 IPv4: 192.168.1.10 (aa:bb:cc:...)
// lo IPv4: 127.0.0.1 (:::...)
// find this machine's LAN IPv4
function getLanIp() {
for (const nets of Object.values(os.networkInterfaces())) {
for (const n of nets) {
if (n.family === 'IPv4' && !n.internal) return n.address;
}
}
return '127.0.0.1';
}用户与路径
os.homedir 返回用户主目录,os.tmpdir 返回系统临时目录 —— 两者都平台感知,优先于硬编码路径。os.userInfo 给出当前用户身份。os.constants 集中了数值信号/errno/优先级值,使你不必硬编码像 SIGTERM 的 15 这样的魔术数字。
const os = require('os');
os.homedir(); // /home/alice or C:\Users\alice
os.tmpdir(); // /tmp or C:\Users\...\Temp
os.userInfo(); // { username, uid, gid, homedir, shell }
os.constants; // signal/errno/priority constants
// errno constants map
os.constants.errno.ENOENT; // -2
// signal constants
os.constants.signals.SIGTERM; // 15
// priority constants
os.constants.priority.PRIORITY_HIGH;
// build a portable temp path
const path = require('path');
const tmpFile = path.join(os.tmpdir(), 'app-' + Date.now() + '.log');进程(Process)
process.argv
process.argv 保存命令行参数:argv[0] 是 node 二进制,argv[1] 是脚本,其余是用户参数。切掉前两个即可获得用户参数。对玩具脚本以外的任何内容,使用 minimist、yargs 或 commander 等解析器 —— 它们处理标志、类型、默认值和帮助文本。
// argv[0] = node path, argv[1] = script path, argv[2+] = args
// node app.js --port 3000 users.csv
console.log(process.argv);
// ['/usr/bin/node', '/app/app.js', '--port', '3000', 'users.csv']
const args = process.argv.slice(2);
console.log(args); // ['--port', '3000', 'users.csv']
// minimal flag parsing
function parseArgs(arr) {
const out = { _: [] };
for (let i = 0; i < arr.length; i++) {
if (arr[i].startsWith('--')) out[arr[i].slice(2)] = arr[++i];
else out._.push(arr[i]);
}
return out;
}
// for real CLI use commander, yargs, or minimistprocess.env
process.env 以字符串形式保存环境变量。约定上 PORT、NODE_ENV、DATABASE_URL 和 API_KEY 来自环境,使同一代码在不同环境运行。用 dotenv 包在开发时加载 .env 文件。绝不要提交密钥 —— 通过宿主/CI 环境注入。
// read environment variables
const port = process.env.PORT || 3000;
const dbUrl = process.env.DATABASE_URL;
const nodeEnv = process.env.NODE_ENV || 'development';
const isProd = process.env.NODE_ENV === 'production';
// set an env var (affects child processes too)
process.env.MY_VAR = 'value';
// load .env files (install: npm i dotenv)
// require('dotenv').config();
// enumerate everything
for (const [k, v] of Object.entries(process.env)) {
console.log(k, '=', v);
}process.exit 与退出码
process.exit(n) 立即终止,可能在 I/O 缓冲冲刷前 —— 优先设置 process.exitCode 并让事件循环自然排空。退出 0 表示成功;非零向 shell 和 CI 表示失败。约定 128+N 编码被信号 N 终止(因此 SIGTERM 为 143) 。
// exit immediately with a status code
process.exit(0); // success
process.exit(1); // general failure
// exit code is also settable
process.exitCode = 2; // used if the loop ends naturally
// prefer setting exitCode + returning over process.exit()
// so async work can finish
// common conventions:
// 0 success
// 1 general error
// 2 misuse (bad args)
// 124 timeout (from the 'timeout' command)
// 128+N killed by signal N (e.g. 143 = SIGTERM)stdin / stdout / stderr
process.stdout 和 process.stderr 是可写流 —— write() 不加换行(不同于 console.log)。stdin 是可读流,可用 'data' 事件或 async 迭代(最干净的现代模式)消费。用 stderr 输出诊断,以便在管道中与真实输出分离。
// stdout/stderr are writable streams
process.stdout.write('no newline');
process.stdout.write('line\n');
console.log === process.stdout.write.bind(...); // roughly
// stdin is a readable stream
process.stdin.setEncoding('utf8');
process.stdin.on('data', chunk => console.log('got:', chunk));
// read all of stdin (async iteration)
async function readStdin() {
let data = '';
for await (const chunk of process.stdin) data += chunk;
return data;
}
// ask a yes/no question
process.stdin.resume();
process.stdin.once('data', input => process.exit(input[0] === 'y'.charCodeAt(0) ? 0 : 1));进程事件(信号)
'exit' 在事件循环排空后触发 —— 仅同步代码可运行。'beforeExit' 可运行异步工作并可能推迟退出。处理 SIGINT(Ctrl-C)和 SIGTERM(docker stop、kill)以优雅关闭服务器和数据库连接。始终设置强制退出超时,使卡住的连接不会永远困住进程。
// lifecycle events
process.on('exit', code => {
// sync only here — no async, no event loop
console.log('bye, code', code);
});
process.on('beforeExit', () => {
// async OK; can postpone exit
});
// signal handlers (Unix + Windows for SIGINT/SIGTERM)
process.on('SIGINT', () => { // Ctrl-C
console.log('graceful shutdown...');
server.close(() => process.exit(0));
});
process.on('SIGTERM', () => { // kill / docker stop
server.close(() => process.exit(0));
});
process.on('SIGHUP', () => reloadConfig());memoryUsage 与 uptime
process 暴露身份(pid、ppid、platform、version)和运行时统计(uptime、memoryUsage、cpuUsage)。cwd() 是工作目录 —— chdir 改变它,影响相对路径解析。memoryUsage 和 cpuUsage 是健康检查端点和自动扩缩指标的基础。
// process info
process.pid; // OS process ID
process.ppid; // parent PID
process.platform; // 'linux', 'darwin', 'win32'
process.version; // 'v20.11.0'
process.versions; // { v8, openssl, modules, ... }
process.cwd(); // current working directory
process.uptime(); // seconds since process started
// change directory
process.chdir('/tmp');
// memory snapshot
const m = process.memoryUsage();
console.log(m.rss, m.heapUsed);
// CPU usage of this process
const cpu = process.cpuUsage();
console.log(cpu.user, cpu.system); // microseconds网络(net)
TCP 服务器
net.createServer 创建原始 TCP 服务器。每个连接调用回调并传入 Socket(一个 Duplex 流)。用 'data' 事件读取,用 socket.write 写入。务必处理 socket 的 'error',否则进程崩溃。TCP 是 HTTP 之下的基础 —— 适用于自定义协议、游戏和代理。
const net = require('net');
const server = net.createServer(socket => {
console.log('client connected', socket.remoteAddress);
socket.write('welcome\n');
socket.on('data', data => {
socket.write('echo: ' + data); // echo back
});
socket.on('end', () => console.log('client left'));
socket.on('error', err => console.error('socket err', err));
});
server.listen(5000, () => {
console.log('TCP server on port 5000');
});
// test with: nc localhost 5000 or telnet localhost 5000TCP 客户端
net.connect(别名 createConnection)打开 TCP 客户端 socket。连接时触发回调;'data' 事件送达传入字节。设置编码以接收字符串而非 Buffer。与任何流一样,处理 'error' 以免崩溃。socket 在 'end' 时自动关闭;调用 client.end() 半关闭本端。
const net = require('net');
// connect to a TCP server
const client = net.connect(5000, 'localhost', () => {
console.log('connected');
client.write('hello server\n');
});
client.setEncoding('utf8');
client.on('data', data => console.log('server said:', data));
client.on('end', () => console.log('disconnected'));
client.on('error', err => console.error(err));
// or with an options object + timeout
const c = net.createConnection({
port: 5000,
host: 'localhost',
timeout: 5000
});Socket 事件与数据
TCP 是字节流,不是消息协议 —— 一个 'data' 事件不等于一条消息;数据可能被拆分或合并。你必须自己分帧消息(长度前缀、分隔符或解析器)。setKeepAlive 检测死对等端,setTimeout 杀死空闲连接,'drain' 表示写缓冲区已排空。
const net = require('net');
net.createServer(socket => {
socket.setEncoding('utf8');
socket.setKeepAlive(true, 30000); // enable TCP keepalive
socket.setTimeout(60000); // idle timeout
socket.on('data', chunk => {
// TCP is a STREAM: chunks may split or merge messages.
// Frame your protocol (length prefix, delimiter, etc.)
console.log('received', chunk.length, 'bytes');
});
socket.on('timeout', () => socket.end());
socket.on('close', hadError => console.log('closed', hadError));
socket.on('drain', () => console.log('buffer drained'));
}).listen(5000);写入与半关闭
socket.write 遵守背压(满时返回 false —— 等待 'drain')。end() 半关闭:它冲刷待写数据后发 FIN,同时仍读取对端。destroy() 立即杀死 socket。用 remoteAddress/remotePort 记录日志和访问控制。pause/resume 节流快速发送方。
const net = require('net');
net.createServer(socket => {
// write() returns false when the buffer is full (backpressure)
const ok = socket.write(bigPayload);
if (!ok) socket.once('drain', writeMore);
// half-close: send FIN, keep reading
socket.end(); // writes any pending data, then closes our side
// socket.end('final message\n'); // write + close
// fully destroy the socket immediately
socket.destroy();
// pause/resume reading
socket.pause();
socket.resume();
// address info
socket.remoteAddress; // '192.168.1.5'
socket.remotePort; // 54321
}).listen(5000);服务器事件与 IPC
net.Server 发出 'connection'(每客户端)、'listening'、'error'(处理 EADDRINUSE)和 'close'。listen 可接收路径用于 Unix socket(或 Windows 命名管道)—— 同一主机上进程间快速、安全的本地 IPC。server.close() 停止接受新连接并等待现有连接完成。
const net = require('net');
const server = net.createServer();
server.on('connection', socket => {}); // same as createServer cb
server.on('listening', () => console.log('bound'));
server.on('error', err => {
if (err.code === 'EADDRINUSE') console.error('port taken');
});
server.on('close', () => console.log('server closed'));
server.listen(5000);
// Unix domain socket / named pipe (fast local IPC)
server.listen('/tmp/app.sock');
// Windows: server.listen('\\\\.\\pipe\\app');
// stop accepting, close idle connections
server.close(() => console.log('all done'));
// get bound address
server.address(); // { port: 5000, family: 'IPv4', address: '::' }定时器与工具
定时器(setTimeout、setInterval)
setTimeout 在延迟后运行一次,setInterval 重复运行。setImmediate 在同一循环回合的 I/O 事件后运行。queueMicrotask 更早,在当前操作之后立即运行。一个循环回合内的顺序是:先微任务,然后定时器,然后 I/O,然后 setImmediate —— 在安排后续工作时很有用。
// run once after a delay (ms)
const t = setTimeout(() => console.log('hi'), 1000);
// run repeatedly every interval
const i = setInterval(() => console.log('tick'), 2000);
// run right after the current event loop turn
setImmediate(() => console.log('immediate'));
// run before the next event loop turn (microtask)
queueMicrotask(() => console.log('microtask'));
// order inside one loop:
// microtasks -> timers -> I/O -> check (setImmediate)清除定时器与 ref
每个定时器都有对应的清除函数。unref() 将定时器标记为 '不保持进程存活' —— 如果只剩 unref 的定时器,Node 会退出(适合不应困住进程的周期性清理)。ref() 反转它。延迟之后的额外参数会转发给回调,避免额外的箭头函数。
const t = setTimeout(() => {}, 1000);
clearTimeout(t); // cancel
const i = setInterval(() => {}, 1000);
clearInterval(i);
const imm = setImmediate(() => {});
clearImmediate(imm);
// keep the event loop alive?
// timers do by default; unref() lets the process exit if
// only this timer is pending
const timer = setInterval(() => {}, 1000);
timer.unref(); // won't keep Node alive
timer.ref(); // keep alive again
// pass args to the callback
setTimeout((a, b) => console.log(a, b), 500, 'x', 'y');util.inspect 与 format
util.format 用 printf 风格占位符(%s %d %j %o %O %%)构建字符串。util.inspect 渲染任何对象 —— 传 { depth: null, colors: true } 获得完整彩色转储,在日志中有用。util.deprecate 包装函数,使其在被调用时发出 DeprecationWarning(用 --trace-deprecation 显示)。
const util = require('util');
// format like printf
util.format('%s:%d', 'port', 3000); // 'port:3000'
util.format('%j', { a: 1 }); // JSON
// deep inspect an object (what console.dir uses)
util.inspect(obj, { depth: null, colors: true, compact: false });
// pretty-print a function (its source)
util.inspect(function f() { return 1; });
// deprecate an API with a warning
const oldFn = util.deprecate(
() => doThingOldWay(),
'oldFn() is deprecated, use newFn() instead'
);util.promisify
util.promisify 将错误优先回调函数包装为返回 Promise 的函数 —— 从遗留回调 API 到 async/await 的桥梁。许多核心模块现已提供原生 promise 版本(fs/promises)更可取,但 promisify 仍可拯救第三方回调 API。函数可定义 [util.promisify.custom] 以控制转换。
const util = require('util');
const fs = require('fs');
// turn an error-first callback fn into a promise fn
const readFile = util.promisify(fs.readFile);
const text = await readFile('data.txt', 'utf8');
// works on custom functions too
function delay(ms, cb) { setTimeout(() => cb(null, ms), ms); }
const delayP = util.promisify(delay);
await delayP(100);
// custom promisify symbol (controls the promise behavior)
// const { promisify } = util;
// obj[util.promisify.custom] = () => Promise.resolve(42);
// many core modules ship promise versions directly:
// require('fs/promises'), require('dns/promises')util 类型检查与 callbackify
util.types 提供可靠的类型检查(isPromise、isAsyncFunction、isRegExp、isMap),比 instanceof 更好地应对跨 realm 和子类边界情况。util.callbackify 反转 promisify —— 在你必须将基于 promise 的函数暴露给回调风格 API 时有用。util.MIMEType(Node 19+)解析和操作 MIME 类型。
const util = require('util');
// type checks
util.isPromise(Promise.resolve()); // true
util.isFunction(() => {}); // true
util.types.isMap(new Map()); // true
util.types.isAsyncFunction(async () => {}); // true
util.types.isRegExp(/x/); // true
// reverse of promisify: promise -> error-first callback
const readFileCb = util.callbackify(require('fs/promises').readFile);
readFileCb('x.txt', 'utf8', (err, data) => {
if (err) console.error(err);
else console.log(data);
});
// MIME type utilities (Node 19+)
util.MIMEType.parse('text/html; charset=utf-8');nextTick 与 setImmediate
process.nextTick 在当前同步操作之后、I/O 之前运行 —— 因此递归调用 nextTick 会无限饿死 I/O(循环永不前进)。setImmediate 在 I/O 之后的 'check' 阶段运行。要让事件循环让出并让待处理 I/O settle,优先用 setImmediate;nextTick 留给必须在任何 I/O 回调前发生的清理。
// process.nextTick: runs BEFORE I/O, after the current op
process.nextTick(() => console.log('tick'));
// setImmediate: runs AFTER I/O events, in the 'check' phase
setImmediate(() => console.log('immediate'));
// setTimeout(0): runs in the timers phase, after nextTick
setTimeout(() => console.log('timeout'), 0);
// typical order in one loop turn:
// 1. current sync code
// 2. nextTick callbacks (can starve I/O!)
// 3. timers
// 4. I/O callbacks
// 5. setImmediate
// prefer setImmediate to yield to I/O; use nextTick for
// immediate post-sync cleanup相关 Node.js 代码片段
Copy-paste ready code for common tasks.
fs Module
Read, write, and watch files with promises and callbacks.
HTTP Server
Build an HTTP server with the http module and routing.
Streams
Pipe, transform, and consume streams efficiently.
EventEmitter
Emit and listen for custom events.
path Module
Join, resolve, and parse file paths cross-platform.
Buffers
Work with binary data using Buffer and TypedArrays.
Child Process
Spawn, exec, and fork external processes.
Async Patterns
Run promises in parallel, sequentially, and with limits.
这篇内容对您有帮助吗?