93 lines
4.7 KiB
Markdown
93 lines
4.7 KiB
Markdown
---
|
||
id: backend.nodejs.express-nest
|
||
title: Node.js 后端(了解级):事件循环、Express 5 中间件与错误处理、NestJS 的模块化思路、做 BFF
|
||
type: lesson
|
||
domain: backend
|
||
stack: [nodejs]
|
||
tags: [Node.js, 事件循环, Express 5, 中间件, 错误处理, NestJS, 依赖注入, BFF, SSE]
|
||
level: basic
|
||
prerequisites: [frontend.typescript.type-system]
|
||
est_hours: 2.5
|
||
related: [frontend.ssr.nextjs-app-router, backend.nodejs.scenario.ai-bff, backend.nodejs.interview]
|
||
week: [7, 14]
|
||
order: 1
|
||
---
|
||
|
||
# Node.js 后端(了解级):事件循环、Express 5 中间件与错误处理、NestJS 的模块化思路、做 BFF
|
||
|
||
> 定位:**了解并能写**。你的主线后端是 Go(基础)+ Python(AI / Agent)。BOSS直聘 2026-10-23 的调研中有岗位提到 Node.js / Next.js,前端出身的候选人被问到 Node 的概率很高。示例基于 Node 24 + Express 5 实际运行通过;NestJS 部分是示意代码,未编译。以官方文档为准:https://nodejs.org/docs 、https://expressjs.com/ 、https://docs.nestjs.com/ 。
|
||
|
||
## 解释说明
|
||
|
||
### 事件循环(面试必问)
|
||
|
||
Node 单线程执行 JS,I/O 交给系统异步完成,回调放进队列。事件循环的主要阶段:timers(`setTimeout`)→ pending callbacks → poll(I/O)→ check(`setImmediate`)→ close callbacks;每个阶段之间先清空 `process.nextTick` 队列和 Promise 微任务队列。
|
||
|
||
**结论**:适合 I/O 密集(BFF、网关、实时推送);CPU 密集的计算会阻塞所有请求,要放到 `worker_threads` 或其他服务。
|
||
|
||
### Express 5
|
||
|
||
- **中间件**:`(req, res, next)` 按注册顺序执行,类似 Gin 的中间件链。
|
||
- **错误处理中间件**:4 个参数 `(err, req, res, next)`,放在最后。
|
||
- **Express 5 的变化**:async 处理函数抛出的错误 / 被拒绝的 Promise 会自动传给错误处理中间件(Express 4 需要手动 `next(err)`);路由路径语法也有调整(以迁移指南为准)。
|
||
|
||
### NestJS
|
||
|
||
在 Express(或 Fastify)之上提供 Angular 风格的结构:模块(Module)、控制器(Controller)、服务(Provider)、**依赖注入**、管道(Pipe,做校验)、守卫(Guard,做鉴权)、拦截器(Interceptor)。适合中大型团队统一规范。对照:Go 中你手写依赖注入,FastAPI 用 `Depends`。
|
||
|
||
### 为什么前端团队常用 Node 做 BFF
|
||
|
||
同一语言、类型可以共享;聚合多个后端接口、裁剪字段;统一鉴权、限流;流式转发 AI 回答。
|
||
|
||
## 案例
|
||
|
||
AI 客服的 BFF:校验登录、限流、为每个请求生成 request id,调用 Python AI 服务并把流式回答以 SSE 转发给浏览器;客户端断开时中止上游。
|
||
|
||
## 代码示例
|
||
|
||
[examples/express-bff](examples/express-bff/):`node --test app.test.ts` 4 个测试通过、`tsc` 通过,`node server.ts` 启动后 `curl -N` 观察到每 200ms 到达一条 SSE 事件。
|
||
|
||
```ts
|
||
// Express 5:async 路由中直接 throw,由统一错误处理中间件返回
|
||
app.get("/orders/:id", async (req, res) => {
|
||
if (!/^[A-Z]\d{3,}$/.test(req.params.id)) throw new HttpError(400, "BAD_ORDER_ID", "订单号格式不正确");
|
||
const order = await deps.findOrder(req.params.id);
|
||
if (!order) throw new HttpError(404, "ORDER_NOT_FOUND", "订单不存在");
|
||
res.json(order);
|
||
});
|
||
|
||
app.use((err: unknown, _req: Request, res: Response, _next: NextFunction) => {
|
||
if (err instanceof HttpError) { res.status(err.status).json({ code: err.code, message: err.message }); return; }
|
||
res.status(500).json({ code: "INTERNAL", message: "服务器内部错误" }); // 不暴露内部细节
|
||
});
|
||
```
|
||
|
||
NestJS 的写法(示意,未编译;需要装饰器相关的编译配置):
|
||
|
||
```ts
|
||
@Controller("orders")
|
||
export class OrdersController {
|
||
constructor(private readonly orders: OrdersService) {} // 依赖注入
|
||
|
||
@Get(":id")
|
||
@UseGuards(AuthGuard) // 鉴权
|
||
find(@Param("id") id: string) {
|
||
return this.orders.findOne(id); // 抛出 NotFoundException 自动转成 404
|
||
}
|
||
}
|
||
```
|
||
|
||
## 易错点
|
||
|
||
- 在请求处理中做大量同步计算(大 JSON 序列化、加密、图片处理),阻塞所有请求。
|
||
- Express 4 的项目里 async 错误没有 `next(err)`,请求挂起直到超时。
|
||
- SSE 响应头发出后出错,还试图设置状态码。
|
||
- 没有监听客户端断开,上游 AI 继续生成、浪费 token。
|
||
- 未处理的 Promise 拒绝导致进程退出:要有全局监控和进程守护。
|
||
|
||
## 面试常问
|
||
|
||
- 讲讲 Node 的事件循环,`process.nextTick`、Promise、`setTimeout`、`setImmediate` 的顺序?
|
||
- Express 中间件原理?错误处理怎么做?
|
||
- Node 适合什么场景,不适合什么场景?
|