掌握 Node.js 核心概念,提升后端开发技能
Node.js 是单线程非阻塞 I/O 模型。事件循环负责调度异步任务的执行,主要包含以下关键阶段:
setTimeout 和 setInterval 的回调。setImmediate() 的回调。socket.on('close'))。注意:process.nextTick() 和 Promise 微任务会在当前阶段切换前优先清空执行。
Node.js 支持两种主流模块化规范:
// CommonJS (传统)
const fs = require('fs');
module.exports = { myFunction };
// ES Modules (现代,需要在 package.json 设置 "type": "module")
import fs from 'fs';
export default myFunction;
require() 不能加载 ESM 模块;动态 import() 可以从 CJS 文件加载 ESM。
Buffer:在内存中开辟固定大小的空间,用于处理二进制数据流。
Stream:按块(chunk)处理数据的抽象接口,适合处理大文件,避免内存溢出。
const fs = require('fs');
// 使用管道(Pipe)实现文件复制
const readStream = fs.createReadStream('input.txt');
const writeStream = fs.createWriteStream('output.txt');
readStream.pipe(writeStream);
四种流类型:Readable(可读)、Writable(可写)、Duplex(双工)、Transform(转换,常见如 zlib、crypto)。
fs.readFile('a.txt', (err, data) => {
if (err) return console.error(err);
fs.readFile('b.txt', (err2, data2) => {
if (err2) return console.error(err2);
// 嵌套 7-8 层 —— 著名的"回调地狱"
});
});
fetch('https://api.example.com/user/1')
.then(r => r.json())
.then(user => fetch(`/api/posts?uid=${user.id}`))
.then(r => r.json())
.then(posts => console.log(posts))
.catch(err => console.error(err));
async function loadPosts() {
try {
const r = await fetch('https://api.example.com/user/1');
const user = await r.json();
const r2 = await fetch(`/api/posts?uid=${user.id}`);
const posts = await r2.json();
return posts;
} catch (err) {
console.error(err);
}
}
async/await,错误用 try/catch。process 是 Node.js 进程对象,提供运行时信息与控制能力。
// 常用成员
process.argv // 命令行参数数组
process.env // 环境变量对象
process.cwd() // 当前工作目录
process.pid // 进程 ID
process.platform // 'linux' / 'darwin' / 'win32'
process.version // Node.js 版本
process.exit(code) // 退出进程
// 事件
process.on('exit', code => console.log('即将退出', code));
process.on('uncaughtException', err => console.error(err));
process.on('SIGINT', () => process.exit(0)); // Ctrl+C 优雅退出
// 旧 __dirname / __filename(CommonJS)
console.log(__dirname); // 当前文件所在目录
console.log(__filename); // 当前文件路径
| API | 作用 |
|---|---|
console | 日志(log / warn / error / table / time) |
setTimeout / setInterval | 定时器 |
setImmediate | 在 I/O 后立即执行 |
process.nextTick | 当前操作结束后立刻执行 |
queueMicrotask | 把回调加入微任务队列 |
URL / URLSearchParams | URL 解析(无需引入包) |
Buffer | 二进制数据缓冲 |
global | 全局命名空间(类似 window) |
const fs = require('fs');
const fsPromises = require('fs').promises;
// 同步(阻塞)
const data = fs.readFileSync('a.txt', 'utf8');
// 异步回调(Node 早期风格)
fs.readFile('a.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
});
// Promise(推荐)
async function read() {
const buf = await fsPromises.readFile('a.txt');
console.log(buf.toString());
}
// 写文件
await fsPromises.writeFile('b.txt', 'Hello World');
// 目录
await fsPromises.mkdir('sub', { recursive: true });
await fsPromises.readdir('.'); // 列出文件
await fsPromises.stat('a.txt'); // 元信息(大小、时间戳)
await fsPromises.unlink('b.txt'); // 删除
// 文件监听
fs.watch('a.txt', (eventType, filename) => {
console.log(eventType, filename);
});
fs/promises 异步 API,避免阻塞主线程。const path = require('path');
path.join('a', 'b', 'c.txt') // 'a/b/c.txt' 拼接
path.resolve('a', 'b') // 绝对路径(基于 cwd)
path.basename('/a/b/c.txt') // 'c.txt' 文件名
path.dirname('/a/b/c.txt') // '/a/b' 目录名
path.extname('a.txt') // '.txt' 后缀
path.parse('/a/b/c.txt') // { root, dir, base, ext, name }
path.sep // '/' (win32 为 '\\')
// path.join vs path.resolve
path.join('a', '/b', 'c') // 'a/b/c' 只拼接
path.resolve('a', '/b', 'c') // '/b/c' 遇到绝对路径就重置起点
// ES Modules 下需用
import path from 'node:path';
const http = require('http');
const server = http.createServer((req, res) => {
// req: IncomingMessage;res: ServerResponse
console.log(req.method, req.url);
// 简易路由
if (req.url === '/') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('<h1>首页</h1>');
} else if (req.url === '/api/time') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ now: Date.now() }));
} else {
res.writeHead(404);
res.end('Not Found');
}
});
server.listen(3000, () => {
console.log('http://localhost:3000');
});
url / method / headers;POST 请求体通过 data 事件流式接收。Node.js 原生 http 处理路由繁琐,Express 是事实标准的轻量框架。
// 安装:npm install express
const express = require('express');
const app = express();
app.use(express.json()); // 解析 JSON 请求体
// 路由
app.get('/', (req, res) => res.send('Hello'));
app.get('/api/users', (req, res) => res.json([{ id: 1, name: 'Alice' }]));
app.post('/api/users', (req, res) => {
console.log(req.body);
res.status(201).json({ id: Date.now(), ...req.body });
});
app.put('/api/users/:id', (req, res) => res.json({ ok: true }));
app.delete('/api/users/:id', (req, res) => res.status(204).end());
// 错误处理(4 参数)
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: err.message });
});
app.listen(3000, () => console.log('Listening on 3000'));
中间件是 Express 的核心——请求 → 中间件链 → 响应。每个中间件能访问 req、res、next。
// 日志中间件
app.use((req, res, next) => {
console.log(`${new Date().toISOString()} ${req.method} ${req.url}`);
next(); // 交给下一个,否则请求会卡住
});
// 鉴权中间件
function auth(req, res, next) {
if (req.headers.token === 'secret') next();
else res.status(401).json({ error: 'Unauthorized' });
}
app.get('/api/private', auth, (req, res) => res.json({ ok: true }));
// 错误处理中间件(必须 4 个参数)
app.use((err, req, res, next) => {
res.status(500).json({ error: err.message });
});
next(err) 会跳过中间件链直接到错误处理。npm init -y // 初始化 package.json
npm install express // 装到 dependencies
npm install -D nodemon // 装到 devDependencies
npm install -g pnpm // 全局安装
npm run dev // 执行 package.json 的 scripts
npm outdated // 检查过期
npm update express // 升级
npm uninstall lodash // 卸载
{
"name": "my-app",
"version": "1.0.0",
"type": "module", // "commonjs" 或 "module"
"main": "index.js", // 入口
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js",
"test": "jest",
"build": "tsc"
},
"dependencies": {
"express": "^4.18.2"
},
"devDependencies": {
"nodemon": "^3.0.0"
},
"engines": {
"node": ">=18"
}
}
| 版本号 | 含义 |
|---|---|
^1.2.3 | 兼容次版本(≥1.2.3 <2.0.0) |
~1.2.3 | 只允许补丁号变 |
1.2.3 | 固定 |
* | 最新 |
fs.readFile('a.txt', (err, data) => {
if (err) return console.error('读取失败', err);
console.log(data);
});
try {
JSON.parse('{坏数据');
} catch (err) {
console.error(err.message);
}
class HttpError extends Error {
constructor(status, message) {
super(message);
this.status = status;
}
}
// Express
app.use((err, req, res, next) => {
if (err instanceof HttpError)
return res.status(err.status).json({ error: err.message });
res.status(500).json({ error: 'Internal' });
});
await + try/catch,要么 .catch();未捕获的 Promise 拒绝会触发 unhandledRejection。Node.js 大量内置模块基于事件:http、stream、net、process……
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const e = new MyEmitter();
// 监听
e.on('hello', (name) => console.log(`hi, ${name}`));
e.once('once-event', () => console.log('只触发一次'));
// 触发
e.emit('hello', 'Alice'); // hi, Alice
e.emit('hello', 'Bob'); // hi, Bob
e.emit('once-event'); // 只触发一次
e.emit('once-event'); // 不会再次触发
// 一次性事件
e.removeAllListeners('hello');
e.listenerCount('hello'); // 0
removeListener 或用 AbortController。const { spawn, exec, execFile, fork } = require('child_process');
// spawn:流式(适合大输出)
const ls = spawn('ls', ['-lh', '/usr']);
ls.stdout.on('data', chunk => console.log(chunk.toString()));
// exec:缓冲整段输出(适合小命令)
exec('git status', (err, stdout) => {
if (err) return console.error(err);
console.log(stdout);
});
// fork:专门跑另一个 Node 脚本(带 IPC 通道)
const child = fork('./worker.js');
child.on('message', m => console.log('子进程消息:', m));
child.send({ type: 'start' });
// 同步版本(阻塞)
const { execSync } = require('child_process');
const out = execSync('whoami').toString();
spawn;小命令 → exec;Node 脚本通信 → fork。console.log('普通日志');
console.warn('警告');
console.error('错误');
console.table([{name:'A',age:18},{name:'B',age:20}]);
console.time('耗时');
// ... 业务代码 ...
console.timeEnd('耗时'); // 耗时: 12.345ms
console.trace('调用栈');
# 启动调试
node --inspect-brk app.js # 默认 9229 端口
# VS Code 中:
# 1. 点左侧调试图标
# 2. "create launch.json" → Node.js
# 3. 直接 F5 / 加断点
--inspect 会暴露调试端口,建议仅在开发环境开启。const u = new URL('https://example.com/path?a=1&b=2#hash');
u.protocol // 'https:'
u.host // 'example.com'
u.pathname // '/path'
u.search // '?a=1&b=2'
u.hash // '#hash'
u.searchParams.get('a') // '1'
u.searchParams.set('a', '99')
u.searchParams.toString() // 'a=99&b=2'
// querystring(旧 API,仍然好用)
const querystring = require('querystring');
querystring.parse('a=1&b=2'); // { a: '1', b: '2' }
querystring.stringify({ x: 1, y: 'hi' }); // 'x=1&y=hi'
const assert = require('node:assert/strict');
const { test } = require('node:test');
test('两数相加', () => {
assert.equal(2 + 2, 4);
assert.deepEqual({ a: 1 }, { a: 1 });
assert.throws(() => { throw new Error('x'); });
});
test('异步', async () => {
const r = await fetch('https://api.example.com/health');
assert.equal(r.status, 200);
});
// 运行
// node --test
node:test,无需装第三方测试框架就能跑单元测试。| 工具 | 作用 |
|---|---|
| PM2 | 进程守护(崩溃自动重启、日志、负载均衡) |
| nodemon | 开发热重启(改代码自动 reload) |
| dotenv | 从 .env 读取环境变量 |
| helmet | Express 安全中间件 |
| cors | 跨域处理 |
| compression | gzip 压缩响应 |
| winston / pino | 专业日志库 |
# 安装与使用
npm install -g pm2
pm2 start app.js --name myapp
pm2 list # 查看进程
pm2 logs # 实时日志
pm2 restart myapp
pm2 stop myapp
pm2 startup # 开机自启
# 用 ecosystem 文件
pm2 init # 生成 ecosystem.config.js
pm2 start ecosystem.config.js
eval() 用户输入,会执行任意代码。textContent 替代 innerHTML。--inspect + Chrome DevTools 找热点;缓存(node-cache / Redis);数据库加索引;用 stream 处理大文件。console.log('1');
setTimeout(() => {
console.log('2');
}, 0);
Promise.resolve().then(() => {
console.log('3');
});
process.nextTick(() => {
console.log('4');
});
console.log('5');
1 → 5 → 4 → 3 → 21 和 5。process.nextTick 优先级高于普通 Promise,因此先打印 4,再打印 3。2。fs.readFile 和 fs.createReadStream 的主要区别是什么?在大文件处理时应该选择哪一个?fs.readFile 会将整个文件内容一次性加载到内存中,成功后再调用回调函数。fs.createReadStream 以流(Stream)的形式分块读取文件,逐块处理数据。fs.createReadStream,以避免耗尽系统内存。
http 模块创建一个简单的 HTTP 服务器并返回 JSON 数据?请写出关键代码。const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: "Hello World", status: "success" }));
});
server.listen(3000, () => {
console.log('Server running at http://localhost:3000/');
});
process.nextTick 与 setImmediate 的执行顺序如何?process.nextTick 的回调拥有最高优先级——在当前操作完成、阶段切换前就会清空整个 nextTick 队列。
setImmediate 则在事件循环的 Check 阶段执行。
require.cache,下次 require 拿到同一对象。
想重新加载可用 delete require.cache[require.resolve('./m')](慎用)。
Buffer.from('hi', 'utf8').toString('utf8') // 'hi'
Buffer 默认就是 UTF-8,所以 .toString() 也行;显式带编码参数更稳。
path.join('/a', '/c', 'd') 和 path.resolve('/a', '/c', 'd') 的结果分别是?join 只是把片段用平台分隔符拼起来,不读绝对路径。resolve 从右往左拼接,遇到绝对路径就把它当作新起点。next() 会发生什么?res.send()/res.end() 主动结束响应,
否则必须显式 next() 把控制权交给下一个中间件,否则请求将永远挂起。
callback(err, data),错误在前。
仍然推荐使用,但现代异步代码更推荐 Promise / async-await。
__dirname 与 process.cwd() 的区别?__dirname:当前文件所在的绝对目录(CommonJS 才有)。process.cwd():启动 node 命令时所在目录。import.meta.url + fileURLToPath 来得到 __dirname 等价路径。
npm install --save 在 npm 5+ 还有必要加吗?npm install xxx 默认就写入 dependencies。
想放 devDependencies(仅开发用,如 jest、nodemon)才需要 -D 或 --save-dev。
on('data', ...)。
解法:监听完成后 removeListener,或用 AbortController 控制生命周期。
require()module.exports__dirname__filenameimport.meta.url(ESM)fs.readFile / writeFilefs.readdir / mkdirfs.watchcreateReadStreamfs.promisespath.joinpath.resolvepath.basenamenew URL()URLSearchParamshttp.createServerhttps.requestreq.url / methodres.writeHeadres.endsetTimeout / setIntervalsetImmediateprocess.nextTickqueueMicrotaskPromise / async-awaitprocess.argv / envprocess.exitos.cpus / totalmemchild_process.spawnclusterapp.useapp.get/post/put/delreq.body / params / queryres.json / statusnext(err)express — Web 框架axios — HTTP 客户端mongoose — MongoDBjsonwebtoken — JWTnodemon / pm2