引言:Node.js 究竟是什么?
Node.js 不是一门新的语言,也不是一个 JavaScript 框架,而是一个基于 Chrome V8 引擎的 JavaScript 运行时环境。它最大的革命性在于:让 JavaScript 跑在了服务器端。
它的核心设计哲学是:单线程 + 非阻塞 I/O + 事件驱动。这使得它极其适合处理高并发、I/O 密集型的应用场景(如 API 服务、实时通信)。
一、 核心架构基石(必读底层原理)
1. V8 引擎与 libuv
- V8 引擎:负责解析和执行 JavaScript 代码(将 ES6+ 编译为机器码)。
- libuv:这是一个跨平台的异步 I/O 库,它是 Node.js 的事件循环和线程池(Thread Pool)的真正实现者。Node.js 的异步能力并非 V8 提供,而是 libuv 赋予的。
2. 事件循环
事件循环是 Node.js 实现非阻塞的关键。它的执行顺序是分阶段的,每个阶段都有一个先进先出的回调队列:
| 阶段 | 说明 |
|---|---|
| Timers(定时器) | 执行setTimeout和setInterval中到期的回调。 |
| Pending Callbacks | 执行上一轮循环中被延迟到本轮执行的 I/O 回调。 |
| Idle / Prepare | 仅内部使用。 |
| Poll(轮询) | 核心阶段:检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有除了定时器、setImmediate、close以外的回调)。 |
| Check(检查) | 执行setImmediate的回调。 |
| Close Callbacks | 执行socket.on('close')等关闭事件回调。 |
微任务(Microtask)执行时机:在每个阶段切换之间,事件循环会清空
process.nextTick队列(优先级最高)和 Promise 队列(then/catch/finally)。
3. 单线程与多线程的平衡
- 主线程单线程:意味着无需处理多线程并发下的锁和状态同步问题,避免了上下文切换开销。
- 异步 I/O 线程池:Node.js 默认会开启 4 个(可通过
UV_THREADPOOL_SIZE修改)子线程用于处理耗时的异步操作(如fs.readFile、crypto.pbkdf2、zlib压缩等),完成后将回调放入事件循环队列。
二、 全局对象与内置核心模块
1. 常用全局对象
- global:全局命名空间(类似浏览器的
window,但在模块中不直接暴露)。 - process:当前进程对象,用于获取环境变量(
process.env)、进程输入输出、以及监听进程事件(exit、uncaughtException)。 - Buffer:用于处理二进制数据流(文件操作和网络传输的基础)。
2. 核心内置模块详解
| 模块 | 核心用途 | 关键 API/知识点 |
|---|---|---|
| fs (文件系统) | 文件读写、目录操作 | 提供同步/异步(回调/Promise)API。流式读取(createReadStream)优于readFile处理大文件。 |
| path | 路径处理 | path.join、path.resolve(绝对路径)、path.parse。 |
| http / https | 构建 Web 服务器和客户端 | http.createServer;处理请求对象(req)和响应对象(res)。 |
| events | 事件触发器 | Node.js 中最核心的发布-订阅模式实现,大多数内置类都继承自EventEmitter。 |
| os | 操作系统信息 | os.cpus()(获取 CPU 核心数)、os.totalmem()、os.type()。 |
| crypto | 加密与哈希 | 用于 MD5/SHA 哈希、HMAC、以及 RSA/AES 等加解密(常用于密码存储和 JWT 签名)。 |
| util | 工具函数 | util.promisify(将回调函数转为 Promise)极其实用。 |
| child_process / worker_threads | 多进程 / 多线程 | spawn/fork创建子进程;Worker创建真正的线程用于处理 CPU 密集型任务。 |
三、 异步编程进化史
Node.js 天生异步,处理异步的能力体现了开发者的水平。
现代标准:Promise + async/await
- 通过new Promise()将回调包装,利用then/catch链式调用。
-终极形态:async/await(语法糖),让异步代码看起来像同步代码,彻底解决回调地狱。
-注意:await只能在async函数中使用,且必须包裹try...catch或全局捕获unhandledRejection事件来捕获异常。并发控制
Promise.all(全部成功才返回)、Promise.race(谁先完成返回谁)、Promise.allSettled(全部完成不管成功失败)。
四、 核心后端框架与数据库交互
1. 主流框架选型
- Express/Koa(传统 MVC):轻量级,中间件机制灵活,生态丰富(NestJS、Egg.js 均基于此封装)。
- NestJS(现代分层架构):使用 TypeScript,借鉴 Angular 的模块化、依赖注入(IoC)、装饰器模式,更适合大型企业级项目。
2. 数据库连接与 ORM/ODM
- SQL 数据库(MySQL/PostgreSQL):使用
Sequelize或TypeORM。- 注意:连接池配置(
connectionLimit)和防止 SQL 注入(参数化查询)。
- 注意:连接池配置(
- NoSQL 数据库(MongoDB):使用
Mongoose。- 核心概念:Schema 定义、Model 操作、以及聚合管道(Aggregation Pipeline)。
- Redis(缓存/消息中间件):使用
ioredis或node-redis。常用于 Session 存储、接口缓存、分布式锁。
五、 安全防护体系
| 安全威胁 | Node.js 防护措施 |
|---|---|
| SQL/NoSQL 注入 | 使用 ORM 参数化查询,或对输入做严格类型校验(如class-validator)。 |
| XSS(跨站脚本攻击) | 输出渲染时进行转义(escape),或使用 CSP(内容安全策略)。 |
| CSRF(跨站请求伪造) | 使用csrf令牌同步机制或SameSiteCookie 属性。 |
| 敏感信息泄露 | 使用dotenv管理.env环境变量,切勿将密码/密钥提交到代码仓库。 |
| 请求泛滥/DoS | 使用express-rate-limit进行接口限流;使用helmet设置安全响应头。 |
| 依赖包漏洞 | 定期运行npm audit或yarn audit修复已知漏洞。 |
六、 内存管理与性能优化
1. 内存泄漏排查
- Node.js 内存上限默认约 1.4GB(64位系统)。超出将导致
JavaScript heap out of memory。 - 常见内存泄漏场景:
- 全局变量缓存过多(使用 LRU Cache 替代 Map)。
- 事件监听器未移除(
emitter.setMaxListeners警告)。 - 闭包引用未释放。
- 排查工具:使用
node --inspect开启 Chrome DevTools 进行内存快照对比,或使用clinic工具集。
2. CPU 密集型任务处理(突破单线程瓶颈)
Node.js 不适合直接做大量 CPU 计算(如图像处理、大数据排序),解决方案:
- 方案一:使用
worker_threads创建多线程,将计算任务分配给子线程执行,主线程负责调度。 - 方案二:使用
child_process.fork或cluster模块,利用多核 CPU 启动多个进程。
3. 常用优化技巧
- 使用
stream(流)处理大文件,而非一次性读入内存。 - 使用
gzip/deflate压缩静态资源传输。 - Nginx 前置:将静态文件交给 Nginx 处理,Node 专注动态逻辑。
七、 工程化与部署运维
1. 包管理
npmvsyarnvspnpm(pnpm 通过硬链接节省磁盘空间,并解决幽灵依赖问题)。package.json中的dependenciesvsdevDependencies。- 语义化版本控制(Semver):
^1.2.3(兼容补丁和次版本)、~1.2.3(仅兼容补丁)。
2. 进程守护与管理
- PM2:生产环境进程管理器。
- 支持
cluster mode(自动利用多核)。 - 自带日志管理、内存监控、以及
pm2 start ecosystem.config.js的配置管理。
- 支持
3. Docker 容器化部署
- 编写精简的
Dockerfile(使用多阶段构建,选择node:alpine基础镜像减小体积)。 - 注意容器内的
NODE_ENV=production优化依赖安装。
4. 日志规范
- 使用
winston或pino(高性能)进行日志分级(info, warn, error)。 - 必须接入 ELK(Elasticsearch, Logstash, Kibana)或第三方日志平台以便排查线上问题。
八、 常见题目总结
setTimeout(fn, 0)和setImmediate(fn)谁先执行?- 在
I/O循环外(主模块),执行顺序取决于Timer阶段和Check阶段的随机性(受 V8 启动性能影响)。 - 关键结论:在
fs.readFile的回调中,setImmediate永远先于setTimeout(fn, 0)执行。
- 在
process.nextTick比Promise.then快吗?- 是的。
nextTick在事件循环的每个阶段结束后立即执行,而Promise.then属于微任务,在nextTick之后执行。
- 是的。
- 如何实现一个简单的 EventEmitter?
- 本质是维护一个
{ eventName: [handler1, handler2] }的映射表,实现on、emit、once和off方法。
- 本质是维护一个