⚡️编排的艺术:BFF 的核心职能——数据聚合与 HTTP 请求

大家好,我是大布布将军。

在传统的 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,但这个数据分散在三个核心微服务中:

  1. 商品信息服务: 提供基本信息(名称、描述)。
  2. 库存服务: 提供当前库存量。
  3. 评价服务: 提供最近 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 的核心逻辑:

  1. 将业务逻辑放在 Service 层,并利用依赖注入将其与 Controller 解耦。
  2. 使用 axiosfetch 向外部微服务发起请求。
  3. 利用 Promise.all 实现数据的并行获取和聚合 ,最大限度地发挥 Node.js 的异步优势,提高 API 响应速度。

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

相关推荐
Coder_Shenshen1 小时前
西门子S7CommPlus协议鉴权算法原理与流程详解
网络·后端·算法
大圣编程1 小时前
Python中continue语句的用法是什么?
开发语言·前端·python
yuhaiqiang2 小时前
随手 vibecoding 的浏览器插件已经 6000 多次下载,聊聊他的产品设计
前端·后端·面试
之歆2 小时前
Vue商品详情与放大镜组件
前端·javascript·vue.js
再吃一根胡萝卜3 小时前
如何把小米 MiMo 接入 CodeBuddy,打造私有 Agent
前端
HavenlonLabs4 小时前
Havenlon 对抗性完整(十七):安全不是“防住攻击”,而是控制失败方式
网络·人工智能·架构·安全威胁分析·安全架构·havenlon
fei_sun4 小时前
路径MTU发现
linux·运维·网络
负责的蛋挞4 小时前
异步HttpModule的实现方式
java·服务器·前端
AlfredZhao6 小时前
Linux 主机防火墙如何同时开启 80 和 443?
http·https·firewall