大家好,我是大布布将军。
在传统的 Web 开发中,前端只管展示,后端只管提供数据。但在 BFF 模式中,我们的 Node.js 服务肩负了更重的使命:它是一个**"数据中介"和 "服务编舞者" 。它不拥有核心业务数据(比如用户账号、库存数量),而是负责从各种核心后端服务**那里"取货"并进行"重新包装"。
本篇,我们将学习如何将业务逻辑剥离到 Service 层 ,并掌握 Node.js 中最常用的 HTTP 请求库。
1. 业务逻辑分离:引入 Service 层
在 NestJS 或任何规范的后端框架中,我们遵循 MVC (Model-View-Controller) 或类似的分层结构。
- Controller (控制器): 仅负责接收 HTTP 请求、调用 Service、并返回 HTTP 响应。它不应包含复杂的业务逻辑。
- Service (服务层): 包含所有的业务逻辑、数据处理、以及与外部服务(数据库、其他微服务)的交互。这是我们 BFF 逻辑的核心所在。
这种分离是通过 NestJS 内置的依赖注入(Dependency Injection, DI) 实现的。
TypeScript
// 📁 src/user/user.service.ts
import { Injectable } from '@nestjs/common';
import { User, ApiResponse } from '../interfaces';
@Injectable() // ① 声明这是一个可注入的服务
export class UserService {
// 假设这是从数据库获取用户列表的方法
getUsersFromDB(): User[] {
// 实际场景:调用数据库 ORM 或其他核心微服务
console.log('正在从核心数据库服务获取用户列表...');
return [{
id: 1,
username: '测试用户',
email: 'test@example.com',
role: 'customer'
}];
}
}
现在,我们在 Controller 中安全地调用它:
TypeScript
// 📁 src/user/user.controller.ts
import { Controller, Get } from '@nestjs/common';
import { UserService } from './user.service'; // 导入 Service
@Controller('users')
export class UserController {
// ② 依赖注入:通过构造函数,NestJS 会自动创建并注入 UserService 实例
constructor(private readonly userService: UserService) {}
@Get('list')
getUsersList(): ApiResponse<User[]> {
const users = this.userService.getUsersFromDB(); // ③ 调用 Service 逻辑
return {
code: 200,
message: '列表获取成功',
data: users
};
}
}
2. BFF 的核心:数据聚合与外部 HTTP 请求
BFF 的主要任务是聚合数据。这意味着我们的 Service 层需要向其他微服务 发起 HTTP 请求。在 Node.js 生态中,最主流的 HTTP 客户端是 axios 或 Node.js 原生的 fetch 。我们以更常用的 axios 为例。
场景:一个商品的详情页聚合
假设前端请求一个商品详情页 /product/detail?id=123,但这个数据分散在三个核心微服务中:
- 商品信息服务: 提供基本信息(名称、描述)。
- 库存服务: 提供当前库存量。
- 评价服务: 提供最近 5 条用户评价。
BFF Service 必须并行调用这三个服务,然后将数据整合。
TypeScript
// 📁 src/product/product.service.ts
import { Injectable } from '@nestjs/common';
import axios from 'axios';
import { ProductDetailDTO } from '../interfaces'; // 假设这是最终返回给前端的结构
@Injectable()
export class ProductService {
// 定义核心服务的基地址
private readonly PRODUCT_CORE_URL = 'http://core-product-service:3001';
private readonly INVENTORY_CORE_URL = 'http://core-inventory-service:3002';
private readonly REVIEW_CORE_URL = 'http://core-review-service:3003';
async getProductDetail(productId: number): Promise<ProductDetailDTO> {
// ① 使用 Promise.all 并行发起三个独立的请求
const [productRes, inventoryRes, reviewRes] = await Promise.all([
axios.get(`${this.PRODUCT_CORE_URL}/products/${productId}`),
axios.get(`${this.INVENTORY_CORE_URL}/inventory/${productId}`),
axios.get(`${this.REVIEW_CORE_URL}/reviews/latest/${productId}`)
]);
// ② 数据编排/转换
const productInfo = productRes.data;
const inventory = inventoryRes.data.quantity;
const latestReviews = reviewRes.data.reviews.map(r => ({
author: r.user,
content: r.text
}));
// ③ 构建最终的、为前端定制的 DTO (Data Transfer Object)
return {
name: productInfo.name,
description: productInfo.desc,
currentStock: inventory,
price: productInfo.price,
latestReviews: latestReviews,
// 注意:核心服务可能返回 userId,但 BFF 隐藏了它,只返回前端需要的 user 别名
};
}
}
3. Node.js 异步并发的威力
请注意上面代码中的 await Promise.all([...]) 。
这是利用 Node.js 非阻塞 I/O 机制实现高性能的关键。当 axios 发起请求时,Node.js 主线程不会等待(非阻塞 )。它将这三个请求都扔给底层去处理,然后继续接收其他用户的请求。只有当所有请求都完成后,Promise.all 才会 resolve,我们才进行下一步的编排。
- 如果使用同步请求: 总耗时 = 服务 A 耗时 + 服务 B 耗时 + 服务 C 耗时。
- 使用异步并发: 总耗时 ≈ 三个服务中最慢那个的耗时。
这种对 I/O 效率的提升,是 BFF 层必须掌握的核心技能。
总结
至此,我们完成了 BFF 的核心逻辑:
- 将业务逻辑放在 Service 层,并利用依赖注入将其与 Controller 解耦。
- 使用
axios或fetch向外部微服务发起请求。 - 利用
Promise.all实现数据的并行获取和聚合 ,最大限度地发挥 Node.js 的异步优势,提高 API 响应速度。

下一篇,我们将进入后端数据持久化的世界------学习 关系型数据库(SQL)基础 ,以及如何在 Node.js 中使用 ORM (对象关系映射) 来操作数据。