CSAI Node.js 知识点与复习小站

掌握 Node.js 核心概念,提升后端开发技能

事件循环(Event Loop)

Node.js 是单线程非阻塞 I/O 模型。事件循环负责调度异步任务的执行,主要包含以下关键阶段:

  • Timers:执行 setTimeoutsetInterval 的回调。
  • Pending Callbacks:执行延迟到下一个循环迭代的 I/O 回调。
  • Poll:检索新的 I/O 事件,执行与 I/O 相关的回调。
  • Check:执行 setImmediate() 的回调。
  • Close Callbacks:关闭回调(如 socket.on('close'))。

注意:process.nextTick() 和 Promise 微任务会在当前阶段切换前优先清空执行。

模块化:CommonJS vs ES Modules

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)

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)。

异步编程三阶段演进

① 回调函数(Callback)

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 层 —— 著名的"回调地狱"
    });
});

② Promise

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 / await(现代推荐)

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);
    }
}
建议:现代 Node.js 代码统一用 async/await,错误用 try/catch

process 对象与全局变量

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

API作用
console日志(log / warn / error / table / time)
setTimeout / setInterval定时器
setImmediate在 I/O 后立即执行
process.nextTick当前操作结束后立刻执行
queueMicrotask把回调加入微任务队列
URL / URLSearchParamsURL 解析(无需引入包)
Buffer二进制数据缓冲
global全局命名空间(类似 window)

fs 文件系统模块

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,避免阻塞主线程。

path 路径处理

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';

http 模块:手写一个 Web 服务

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');
});
request 常用属性:url / method / headers;POST 请求体通过 data 事件流式接收。

Express 入门

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 的核心——请求 → 中间件链 → 响应。每个中间件能访问 reqresnext

// 日志中间件
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 与 package.json

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        // 卸载

package.json 关键字段

{
  "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固定
*最新

错误处理 ⭐⭐⭐

三种核心模式

① error-first callback(Node 传统)

fs.readFile('a.txt', (err, data) => {
    if (err) return console.error('读取失败', err);
    console.log(data);
});

② try / catch(同步 + async/await)

try {
    JSON.parse('{坏数据');
} catch (err) {
    console.error(err.message);
}

③ 自定义 Error + 中间件

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

EventEmitter 事件机制

Node.js 大量内置模块基于事件:httpstreamnetprocess……

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
内存泄漏陷阱:如果监听器数量持续增长(>10)Node 会打印 MaxListenersExceededWarning,记得及时 removeListener 或用 AbortController

子进程 child_process

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 三件套

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 Inspector(VS Code 断点)

# 启动调试
node --inspect-brk app.js        # 默认 9229 端口

# VS Code 中:
# 1. 点左侧调试图标
# 2. "create launch.json" → Node.js
# 3. 直接 F5 / 加断点
线上调试慎用:--inspect 会暴露调试端口,建议仅在开发环境开启。

URL 与查询字符串

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'

测试基础(Node 内置 assert)

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 18+ 内置 node:test,无需装第三方测试框架就能跑单元测试。

部署与生产实践

工具作用
PM2进程守护(崩溃自动重启、日志、负载均衡)
nodemon开发热重启(改代码自动 reload)
dotenv.env 读取环境变量
helmetExpress 安全中间件
cors跨域处理
compressiongzip 压缩响应
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() 用户输入,会执行任意代码。
  • SQL 注入:用参数化查询或 ORM;NoSQL 注入:校验输入类型。
  • XSS:模板引擎默认转义;前端 textContent 替代 innerHTML
  • CSRF:用 token;现代框架(Next/Nuxt)内置。
  • helmet:HTTP 头加固。
  • rate-limit:防爆破。
  • bcrypt:密码哈希(不要自己写)。
  • jwt:token;存敏感信息放服务端,token 只放 ID。
性能:--inspect + Chrome DevTools 找热点;缓存(node-cache / Redis);数据库加索引;用 stream 处理大文件。

自我检测练习题

Q1: 下列代码的输出顺序是什么?为什么?
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 → 2
解析:
  1. 同步代码优先执行:打印 15
  2. 主线程同步代码结束后,清空微任务队列:process.nextTick 优先级高于普通 Promise,因此先打印 4,再打印 3
  3. 最后在 Timers 阶段执行定时器回调,打印 2
Q2: 在 Node.js 中,fs.readFilefs.createReadStream 的主要区别是什么?在大文件处理时应该选择哪一个?
答案:
- fs.readFile 会将整个文件内容一次性加载到内存中,成功后再调用回调函数。
- fs.createReadStream 以流(Stream)的形式分块读取文件,逐块处理数据。
- 选择:处理大文件时应优先使用 fs.createReadStream,以避免耗尽系统内存。
Q3: 如何用内置的 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/');
});
Q4: process.nextTicksetImmediate 的执行顺序如何?
  • A. setImmediate 先于 process.nextTick
  • B. process.nextTick 先于 setImmediate,且在每个阶段切换前都会清空
  • C. 二者永远交替执行
  • D. 完全无法预测
答案:B。process.nextTick 的回调拥有最高优先级——在当前操作完成、阶段切换前就会清空整个 nextTick 队列。 setImmediate 则在事件循环的 Check 阶段执行。
Q5: CommonJS 中,模块会被加载几次?
  • A. 每次 require 都重新执行
  • B. 仅第一次加载
  • C. 第一次 require 时加载并缓存,后续直接读取缓存
  • D. 永不加载
答案:C。CommonJS 模块首次加载会执行模块代码,结果放入 require.cache,下次 require 拿到同一对象。 想重新加载可用 delete require.cache[require.resolve('./m')](慎用)。
Q6: 如何把 Buffer 转成 UTF-8 字符串?反之?
  • A. buf.toString('utf8') 和 Buffer.from(str, 'utf8')
  • B. JSON.parse 和 JSON.stringify
  • C. buf.text() 和 String.fromBuffer()
  • D. 没有内置 API
答案:A。
Buffer.from('hi', 'utf8').toString('utf8')   // 'hi'
Buffer 默认就是 UTF-8,所以 .toString() 也行;显式带编码参数更稳。
Q7: path.join('/a', '/c', 'd')path.resolve('/a', '/c', 'd') 的结果分别是?
  • A. 相同,都是 '/a/c/d'
  • B. join 得到 '/c/d',resolve 得到 '/a/c/d'
  • C. join 报错,resolve 正常
  • D. join 得到 '/a/c/d',resolve 得到 '/c/d'
答案:D。
  • join 只是把片段用平台分隔符拼起来,读绝对路径。
  • resolve 从右往左拼接,遇到绝对路径就把它当作新起点
Q8: Express 中间件不调用 next() 会发生什么?
  • A. 抛错并崩溃
  • B. 下一个自动执行
  • C. 请求挂起,客户端超时
  • D. 直接跳到错误处理
答案:C。除非中间件调用了 res.send()/res.end() 主动结束响应, 否则必须显式 next() 把控制权交给下一个中间件,否则请求将永远挂起
Q9: 什么是 error-first callback(错误优先回调)?
  • A. 第一个参数是数据
  • B. 第一个参数是 err,成功时为 null
  • C. 用 try/catch 替代
  • D. Node.js 8 后被废弃
答案:B。Node.js 异步 API 的标准约定:callback(err, data),错误在前。 仍然推荐使用,但现代异步代码更推荐 Promise / async-await
Q10: __dirnameprocess.cwd() 的区别?
  • A. __dirname 是当前文件所在目录,process.cwd() 是启动进程时的工作目录
  • B. 两者都返回当前工作目录
  • C. __dirname 在 ESM 中可用,process.cwd() 不能
  • D. process.cwd() 是文件路径
答案:A。
  • __dirname当前文件所在的绝对目录(CommonJS 才有)。
  • process.cwd()启动 node 命令时所在目录。
ESM 中用 import.meta.url + fileURLToPath 来得到 __dirname 等价路径。
Q11: npm install --save 在 npm 5+ 还有必要加吗?
  • A. 必须加,否则不入 package.json
  • B. 完全等价
  • C. 只对 devDependencies 有用
  • D. 默认就是 --save,无需显式声明;想放 devDependencies 用 -D
答案:D。npm 5+(2017 年)起,npm install xxx 默认就写入 dependencies。 想放 devDependencies(仅开发用,如 jest、nodemon)才需要 -D--save-dev
Q12: EventEmitter 用得不当会产生什么常见陷阱?
  • A. 单线程崩溃
  • B. 事件名拼写错误
  • C. 监听器持续累加导致内存泄漏(MaxListenersExceededWarning)
  • D. 事件循环停止
答案:C。常见场景:长连接、定时器、缓存里持续添加 on('data', ...)。 解法:监听完成后 removeListener,或用 AbortController 控制生命周期。

速查手册

模块化

  • require()
  • module.exports
  • __dirname
  • __filename
  • import.meta.url(ESM)

文件系统

  • fs.readFile / writeFile
  • fs.readdir / mkdir
  • fs.watch
  • createReadStream
  • fs.promises

路径 / URL

  • path.join
  • path.resolve
  • path.basename
  • new URL()
  • URLSearchParams

HTTP

  • http.createServer
  • https.request
  • req.url / method
  • res.writeHead
  • res.end

异步

  • setTimeout / setInterval
  • setImmediate
  • process.nextTick
  • queueMicrotask
  • Promise / async-await

进程 / 系统

  • process.argv / env
  • process.exit
  • os.cpus / totalmem
  • child_process.spawn
  • cluster

Express 常用

  • app.use
  • app.get/post/put/del
  • req.body / params / query
  • res.json / status
  • next(err)

常用第三方包

  • express — Web 框架
  • axios — HTTP 客户端
  • mongoose — MongoDB
  • jsonwebtoken — JWT
  • nodemon / pm2
学习路线:基础语法 → fs/path/http → npm → Express → 数据库 → 鉴权 → 部署。
推荐实战项目:① Todo API ② 博客 URL ④ 实时聊天(WebSocket)③ 简易电商后端 ⑤ 文件上传服务。
@media print{ .nav-tabs,#progress-bar,#to-top{display:none!important} header{padding:10px 0!important;border-bottom:1px solid #ccc!important} .card{break-inside:avoid;box-shadow:none!important;border:1px solid #ccc!important} pre{background:#f5f5f5!important;color:#000!important} }