通过上篇《React前端如何理解、学习和落地 BFF 的完整路径》文章,结合技术栈(Umi + React + @midwayjs/koa),实现一个 企业级、安全可控、可独立部署的 BFF 落地路径


🎯 什么是 BFF?为什么需要它?

🤔 传统架构的问题

假设你开发一个电商中台,需要显示首页数据:

传统方式:前端直接调用多个微服务

前端 → 用户服务(获取用户信息)
     → 商品服务(获取推荐商品)  
     → 订单服务(获取最近订单)
     → 广告服务(获取广告)

问题:

  • ❌ 前端需要发起 4 个请求,页面加载慢
  • ❌ 每个服务的认证方式可能不同(JWT、API Key、时间戳等)
  • ❌ 每个服务返回的数据结构不一致,前端需要分别处理
  • ❌ 前端需要了解所有微服务的接口细节
  • ❌ 后端接口变更时,前端需要同步修改多处代码

✅ BFF 架构的优势

前端 → BFF(Backend For Frontend)→ 用户服务
     (聚合数据)              → 商品服务
                            → 订单服务  
                            → 广告服务

优势:

  • ✅ 前端只需 1 个请求
  • ✅ 统一认证(前端只需向 BFF 发送 JWT)
  • ✅ 统一数据格式(BFF 返回前端需要的结构)
  • ✅ 前端无需关心下游服务细节
  • ✅ 接口变更时,只需修改 BFF,前端无感知

🎨 本文要实现的功能

我们将创建一个完整的 BFF 项目,包含:

  1. 前端页面:显示用户信息和推荐内容
  2. BFF 服务:聚合用户服务和内容服务的数据
  3. 安全认证:前端向 BFF 发送 JWT,BFF 向微服务发送时间戳签名
  4. 自动生成接口:BFF 提供 OpenAPI,前端自动生成 TypeScript 调用代码

🔍 核心要点

原文核心思想 我们的实现方式
✅ BFF 是前端与微服务之间的“胶水层” 使用 Midway + Koa 编写聚合逻辑
✅ 按页面维度定制接口(Page-specific API) 每个页面对应一个 BFF 接口
✅ 部署在函数计算(Serverless)上 支持本地运行 + 阿里云 FC 部署
✅ 减少前端复杂度、提升性能 聚合多个微服务,减少请求数
✅ 安全性:防止伪造请求 时间戳 + MD5 哈希令牌校验

📁 项目结构详解

bff-tutorial/                    # 项目根目录
├── bff/                        # BFF 后端服务(Node.js)
│   ├── src/
│   │   ├── controller/         # 定义对外 API 接口
│   │   ├── service/            # 业务逻辑(调用多个微服务)
│   │   ├── middleware/         # 中间件(认证、安全等)
│   │   └── util/               # 工具函数
│   ├── midway.config.ts        # Midway 配置文件
│   └── package.json            # 依赖管理
│
└── frontend/                   # 前端应用(React + Umi)
    ├── config/
    │   └── openapi.config.ts   # 配置自动生成 API 代码
    ├── src/
    │   ├── services/           # 自动生成的 API 调用代码
    │   ├── pages/              # 页面组件
    │   └── app.ts              # 全局配置
    └── package.json            # 依赖管理

每个文件夹的作用:

  • bff/:BFF 服务,负责聚合多个微服务数据
  • frontend/:前端页面,调用 BFF 接口
  • bff/src/controller/:定义前端可以访问的 API
  • bff/src/service/:实际调用多个微服务的地方
  • bff/src/middleware/:处理认证、安全等通用逻辑
  • frontend/src/services/:自动生成的 API 调用代码

🔧 第一步:环境准备

安装 Node.js

确保你的电脑已安装 Node.js(版本 16+):

# 检查版本
node -v
npm -v

创建项目目录

# 创建项目根目录
mkdir bff-tutorial && cd bff-tutorial

# 创建 BFF 目录
mkdir bff && cd bff

# 初始化 BFF 项目
npm init -y

💻 第二步:搭建 BFF 服务

1. 安装 BFF 依赖

# 在 bff 目录下执行
npm install @midwayjs/core @midwayjs/koa @midwayjs/decorator @midwayjs/swagger axios
npm install typescript ts-node @types/node @types/koa @types/koa__cors --save-dev

依赖说明:

  • @midwayjs/core:Midway 核心框架
  • @midwayjs/koa:基于 Koa 的 Web 框架
  • @midwayjs/swagger:自动生成 API 文档
  • axios:发送 HTTP 请求
  • typescript:TypeScript 支持

2. 配置 TypeScript

创建 tsconfig.json

{
  "compilerOptions": {
    "target": "ES2018",
    "module": "commonjs",
    "moduleResolution": "node",
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "inlineSourceMap": true,
    "noImplicitThis": true,
    "noUnusedLocals": true,
    "stripInternal": true,
    "skipLibCheck": true,
    "pretty": true,
    "strict": true,
    "outDir": "dist",
    "rootDir": "src"
  },
  "exclude": ["dist", "node_modules"]
}

3. 配置 Midway

创建 bff/midway.config.ts

// bff/midway.config.ts
// Midway 项目的入口配置文件

import { defineConfig } from '@midwayjs/hooks';

export default defineConfig({
  modules: [
    '@midwayjs/koa',     // 使用 Koa 作为 Web 框架
    '@midwayjs/swagger', // 自动生成 OpenAPI 文档
  ],
});

4. 创建认证中间件(验证前端到 BFF 的认证)

创建 bff/src/middleware/auth.middleware.ts

// bff/src/middleware/auth.middleware.ts
// 验证前端发送给 BFF 的请求是否合法(JWT 认证)

import { IMiddleware } from '@midwayjs/core';
import { Context, NextFunction } from '@midwayjs/koa';

/**
 * BFF 认证中间件:验证前端到 BFF 的请求
 * 前端需在请求头中携带 JWT Token
 */
export class AuthMiddleware implements IMiddleware<Context, NextFunction> {
  resolve() {
    return async (ctx: Context, next: NextFunction) => {
      // 从请求头中读取 JWT Token
      const authHeader = ctx.get('Authorization');
      const token = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null;

      // 校验 Token 是否存在
      if (!token) {
        ctx.status = 401;
        ctx.body = { success: false, message: '未登录,请先登录' };
        return;
      }

      // 模拟 JWT 解析(实际项目中需使用 jsonwebtoken 库)
      try {
        // 这里可以调用 JWT 解析函数
        // const decoded = jwt.verify(token, process.env.JWT_SECRET);
        // (ctx as any).user = decoded;

        // 演示:模拟解析出的用户信息
        (ctx as any).user = { 
          uid: 'u_123456', 
          name: '测试用户', 
          role: 'user',
          email: 'test@example.com'
        };
      } catch (error) {
        ctx.status = 401;
        ctx.body = { success: false, message: '登录已过期,请重新登录' };
        return;
      }

      // 认证通过,继续执行后续逻辑
      await next();
    };
  }
}

5. 创建下游认证中间件(BFF 到微服务的认证)

创建 bff/src/middleware/downstreamAuth.middleware.ts

// bff/src/middleware/downstreamAuth.middleware.ts
// 为 BFF 发送给下游微服务的请求自动添加认证头
// 这是 BFF 与微服务之间的协议,前端无需关心

import { IMiddleware } from '@midwayjs/core';
import { Context, NextFunction } from '@midwayjs/koa';

/**
 * 下游认证中间件:为请求下游服务的配置对象添加认证信息
 * 由 Service 层调用,确保与微服务的安全通信
 */
export class DownstreamAuthMiddleware implements IMiddleware<Context, NextFunction> {
  resolve() {
    return async (ctx: Context, next: NextFunction) => {
      // 挂载一个工具函数到 ctx,供 Service 层使用
      (ctx as any).addDownstreamAuth = (config: any) => {
        // 生成时间戳(防重放攻击)
        const timestamp = Date.now().toString();
        
        // 生成签名(防篡改,使用约定的密钥)
        const sign = this.md5(`salt=${timestamp}&secret=downstream-secret`);
        
        // 添加认证头到请求配置中
        config.headers = {
          ...config.headers,
          'X-Timestamp': timestamp,        // 时间戳
          'X-Sign': sign,                  // 签名
          'X-Forwarded-User': (ctx as any).user?.uid, // 透传用户身份给下游
          'X-Request-From': 'BFF',         // 标记请求来源
        };
      };

      await next();
    };
  }

  /**
   * 简易 MD5 实现(仅用于演示)
   * 生产环境建议使用 crypto-js 或 Node.js 内置 crypto 模块
   */
  private md5(str: string): string {
    let hash = 0;
    for (let i = 0; i < str.length; i++) {
      const char = str.charCodeAt(i);
      hash = (hash << 5) - hash + char; // 相当于 hash * 31 + char
    }
    return Math.abs(hash).toString(16); // 转为十六进制字符串
  }
}

6. 创建业务服务(聚合数据)

创建 bff/src/service/home.service.ts

// bff/src/service/home.service.ts
// 负责聚合多个下游服务的数据,返回给前端统一结构
// 在这里调用下游服务时,自动应用 BFF 与微服务间的认证

import { Provide, Inject } from '@midwayjs/decorator';
import axios from 'axios';

/**
 * 首页数据服务:模拟聚合用户信息 + 广告列表
 * 实际项目中可替换为调用内部微服务
 */
@Provide()
export class HomeService {
  @Inject() ctx: any; // 注入当前请求上下文,用于获取 addDownstreamAuth 函数

  /**
   * 获取首页数据
   * @param uid 用户 ID
   * @returns 聚合后的首页数据
   */
  async getHomeData(uid: string) {
    try {
      // 构造请求配置(模拟调用两个不同的微服务)
      // 注意:这里使用公开的测试 API,实际项目中替换为内部微服务地址
      const userConfig = {
        url: `https://jsonplaceholder.typicode.com/users/${uid}`,
        method: 'GET',
      };

      const postConfig = {
        url: 'https://jsonplaceholder.typicode.com/posts?_limit=2',
        method: 'GET',
      };

      // 使用 BFF 内部的认证中间件为请求加签
      // 这里应用的是 BFF 与下游服务间的认证协议
      this.ctx.addDownstreamAuth(userConfig);
      this.ctx.addDownstreamAuth(postConfig);

      // 并行调用两个下游服务(模拟微服务调用)
      const [userRes, postsRes] = await Promise.all([
        axios(userConfig), // 向用户服务发送带签名的请求
        axios(postConfig), // 向内容服务发送带签名的请求
      ]);

      // 处理返回数据,转换为前端需要的格式
      return {
        profile: {
          name: userRes.data.name,
          email: userRes.data.email,
          username: userRes.data.username,
        },
        recommendations: postsRes.data.map((post: any) => ({
          id: post.id,
          title: post.title,
          summary: post.body.substring(0, 50) + '...',
        })),
      };
    } catch (error) {
      // 统一错误处理(可记录日志)
      console.error('获取首页数据失败:', error);
      throw new Error('获取首页数据失败');
    }
  }
}

7. 创建控制器(对外 API)

创建 bff/src/controller/home.controller.ts

// bff/src/controller/home.controller.ts
// 定义对外暴露的 HTTP 接口,绑定中间件和业务逻辑
// 前端只需向此接口发送带 JWT 的请求即可

import { Provide, Controller, Get, UseMiddleware, Inject } from '@midwayjs/decorator';
import { HomeService } from '../service/home.service';
import { AuthMiddleware } from '../middleware/auth.middleware'; // 验证前端到 BFF 的认证

/**
 * 首页控制器
 * 所有 /api 开头的请求都会经过此 Controller
 * 前端只需关心此接口,无需知道下游服务的认证细节
 */
@Provide()
@Controller('/api')
// 应用 BFF 认证中间件(前端 → BFF 的认证)
@UseMiddleware([AuthMiddleware])
export class HomeController {
  @Inject() ctx: any;           // 注入当前请求上下文
  @Inject() homeService: HomeService; // 注入业务服务

  /**
   * GET /api/home
   * 获取首页聚合数据
   * 
   * 前端调用示例:
   * fetch('/api/home', {
   *   headers: {
   *     'Authorization': 'Bearer <jwt-token>'  // 只需 JWT
   *   }
   * })
   */
  @Get('/home')
  async getHome() {
    try {
      // 从 ctx 中获取已认证的用户信息(来自 BFF 认证)
      const { uid } = this.ctx.user;

      // 调用服务层获取数据(内部自动向下游服务加签)
      const data = await this.homeService.getHomeData(uid);

      // 返回标准成功响应(前端统一处理)
      return { 
        success: true, 
        data: {
          ...data,
          timestamp: Date.now(), // 添加时间戳,用于前端判断数据新鲜度
        }
      };
    } catch (err: any) {
      // 返回标准失败响应(前端可统一处理)
      return { 
        success: false, 
        message: err.message || '系统繁忙,请稍后再试' 
      };
    }
  }
}

8. 启动 BFF 服务

创建 bff/bootstrap.js(用于启动服务):

// bff/bootstrap.js
const { Bootstrap } = require('@midwayjs/bootstrap');
const { Framework } = require('@midwayjs/koa');

// 启动 Midway 应用
Bootstrap.load(Framework)
  .initialize()
  .then(() => {
    console.log('BFF 服务启动成功,端口:7001');
  });

添加启动脚本到 bff/package.json

{
  "scripts": {
    "dev": "ts-node bootstrap.js",
    "build": "tsc",
    "start": "node dist/bootstrap.js"
  }
}

🖼️ 第三步:搭建前端项目

1. 创建前端项目

cd ..  # 回到项目根目录
npx create-umi@latest frontend

选择配置:

  • 选择 app 模板
  • 选择 TypeScript
  • 选择 CSS 预处理器(可选)

2. 安装前端依赖

cd frontend
npm install @umijs/max @umijs/openapi --save-dev

3. 配置 OpenAPI 自动生成

创建 frontend/config/openapi.config.ts

// frontend/config/openapi.config.ts
// 配置如何从 BFF 的 OpenAPI 文档生成前端 service 代码

import { defineConfig } from '@umijs/openapi';

export default defineConfig({
  // BFF 提供的 OpenAPI 3.0 JSON 地址(开发时指向本地)
  schemaPath: 'http://localhost:7001/openapi.json',

  // 生成的 service 文件存放目录
  serversPath: './src/services',

  // 命名空间前缀,避免全局变量冲突(生成 BFF.xxx)
  namespace: 'BFF',

  // 指定前端使用的请求库(Umi Max 内置 request)
  requestLibPath: "import { request } from '@umijs/max'",

  // 后端响应中,业务数据所在的字段路径
  // 假设响应结构为:{ success: true, data: { ... } }
  dataFields: ['data'],

  // 接口统一前缀(如果 OpenAPI 中路径不含 /api,可在此补充)
  apiPrefix: '/api',
});

4. 配置全局请求拦截器

创建 frontend/src/app.ts

// frontend/src/app.ts
// Umi Max 全局配置文件,用于设置 request 拦截器
// 只负责前端到 BFF 的认证(JWT),不涉及下游服务协议

import type { RequestConfig } from '@umijs/max';

/**
 * 全局 request 配置
 * 所有通过 request 发起的请求都会经过此配置
 * 只处理前端 → BFF 的认证逻辑
 */
export const request: RequestConfig = {
  // 请求超时时间(毫秒)
  timeout: 10000,

  interceptors: {
    request: {
      // 请求发送前的拦截器
      onConfig: (config) => {
        // 自动添加 JWT 认证头(与 BFF 约定)
        const token = localStorage.getItem('token');
        if (token) {
          config.headers['Authorization'] = `Bearer ${token}`;
        }

        // 可选:添加设备信息给 BFF(BFF 会透传给下游)
        config.headers['X-Device-Id'] = 'web-dev-001';
        config.headers['X-Platform'] = 'web';

        return config;
      },
    },
    response: {
      // 响应拦截器
      onConfig: (response) => {
        // 可以在这里统一处理响应
        return response;
      },
    },
  },
};

5. 创建首页页面

修改 frontend/src/pages/index.tsx

// frontend/src/pages/index.tsx
// 使用自动生成的 BFF 接口方法,无需手动写 fetch
// 前端只需关心 BFF 接口,不关心下游服务的认证细节

import { useEffect, useState } from 'react';
// 自动生成的 service,类型安全,参数提示完整
import { BFF } from '@/services/BFF';

interface HomePageData {
  profile: {
    name: string;
    email: string;
    username: string;
  };
  recommendations: {
    id: number;
    title: string;
    summary: string;
  }[];
  timestamp: number;
}

export default function HomePage() {
  const [data, setData] = useState<HomePageData | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    // 模拟登录,设置一个测试 token
    // 实际项目中这里应该是用户登录后获取的 token
    localStorage.setItem('token', 'fake-jwt-token-for-demo');

    // 调用 BFF.getHome(),内部已封装 request 和路径
    // 前端只需关心业务逻辑,认证由全局拦截器处理
    BFF.getHome()
      .then((response) => {
        console.log('API 响应:', response);
        setData(response.data);
        setError(null);
      })
      .catch((error) => {
        console.error('请求失败:', error);
        setError('获取数据失败,请稍后重试');
      })
      .finally(() => {
        setLoading(false);
      });
  }, []);

  if (loading) {
    return (
      <div style={{ padding: 20, textAlign: 'center' }}>
        <h2>正在加载数据...</h2>
      </div>
    );
  }

  if (error) {
    return (
      <div style={{ padding: 20 }}>
        <h2 style={{ color: 'red' }}>错误</h2>
        <p>{error}</p>
        <button onClick={() => window.location.reload()}>
          重新加载
        </button>
      </div>
    );
  }

  if (!data) {
    return (
      <div style={{ padding: 20 }}>
        <h2>暂无数据</h2>
      </div>
    );
  }

  return (
    <div style={{ padding: 20 }}>
      <h1>BFF 示例页面</h1>
      <p>这是一个完整的 BFF 架构演示</p>
      
      <div style={{ marginBottom: 30 }}>
        <h2>用户信息</h2>
        <div style={{ border: '1px solid #ddd', padding: 15, borderRadius: 8 }}>
          <p><strong>姓名:</strong> {data.profile.name}</p>
          <p><strong>用户名:</strong> {data.profile.username}</p>
          <p><strong>邮箱:</strong> {data.profile.email}</p>
        </div>
      </div>

      <div>
        <h2>推荐内容</h2>
        <div>
          {data.recommendations.map((item) => (
            <div 
              key={item.id} 
              style={{ 
                border: '1px solid #eee', 
                padding: 15, 
                marginBottom: 10, 
                borderRadius: 8 
              }}
            >
              <h3>{item.title}</h3>
              <p>{item.summary}</p>
            </div>
          ))}
        </div>
      </div>

      <div style={{ marginTop: 20, fontSize: '0.9em', color: '#666' }}>
        <p>数据获取时间: {new Date(data.timestamp).toLocaleString()}</p>
        <p>前端只需向 BFF 发送 JWT 认证请求,无需关心下游服务协议</p>
      </div>
    </div>
  );
}

▶️ 第四步:运行项目

1. 启动 BFF 服务

cd bff
npm run dev

你应该看到:

BFF 服务启动成功,端口:7001

2. 生成前端 API 代码

在新终端中:

cd frontend
npm run openapi:service

这会根据 BFF 的 OpenAPI 文档自动生成 TypeScript 接口代码。

3. 启动前端项目

npm start

访问 http://localhost:8000,你应该看到:

  • 用户信息(从 JSONPlaceholder 获取的模拟数据)
  • 推荐内容(从 JSONPlaceholder 获取的模拟数据)

🧪 第五步:验证 BFF 架构

1. 查看 BFF API 文档

访问 http://localhost:7001/swagger-ui,你可以看到:

  • /api/home 接口的详细文档
  • 可以直接在页面上测试接口
  • 参数、响应格式都清晰可见

2. 查看生成的 API 代码

frontend/src/services/BFF/homeController.ts 中,你会看到自动生成的 getHome() 方法:

// 这是自动生成的代码示例
export async function getHome(options?: { [key: string]: any }) {
  return request<API.Result<API.HomeData>>('/api/home', {
    method: 'GET',
    ...(options || {}),
  });
}

3. 验证认证流程

打开浏览器开发者工具的 Network 标签:

  • 前端向 BFF 发送请求时,会自动带上 Authorization: Bearer fake-jwt-token-for-demo
  • BFF 向下游服务发送请求时,会自动带上 X-TimestampX-Sign

📚 架构原理总结

🔄 数据流向

1. 前端页面
   ↓
   发送 GET /api/home (带 JWT)
   ↓
2. BFF 验证 JWT → 提取用户信息
   ↓
   调用 Service 层
   ↓
3. Service 生成时间戳 + MD5 签名
   ↓
   向下游服务发送带签名的请求
   ↓
4. 聚合多个服务数据
   ↓
   返回给前端统一格式

🔐 安全机制

  • 前端 ↔ BFF:JWT 认证
  • BFF ↔ 微服务:时间戳 + MD5 签名
  • 前端无需知道:下游服务的认证协议

🚀 生产环境部署建议

1. 环境变量配置

创建 .env 文件:

# BFF 配置
JWT_SECRET=your-jwt-secret-key-change-in-production
DOWNSTREAM_SECRET=your-downstream-secret-key-change-in-production

# 端口配置
PORT=7001

2. Docker 部署

创建 Dockerfile

FROM node:18-alpine

WORKDIR /app

COPY bff/package*.json ./
RUN npm install --production

COPY bff/ ./

RUN npm run build

EXPOSE 7001

CMD ["node", "dist/bootstrap.js"]

3. Nginx 反向代理

server {
    listen 80;
    server_name your-domain.com;

    location /api/ {
        proxy_pass http://localhost:7001/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    location / {
        root /path/to/frontend/dist;
        try_files $uri $uri/ /index.html;
    }
}

✅ 最佳实践总结

实践 说明
职责分离 前端只与 BFF 交互,BFF 负责与下游服务的复杂协议
统一认证 BFF 与微服务间独立认证,前端无需知道下游协议
类型安全 使用 OpenAPI 自动生成 TypeScript 接口
错误处理 统一错误格式,前端可统一处理
性能优化 BFF 负责数据聚合,减少前端请求次数

🤔 常见问题解答

Q: 为什么要用 BFF?直接调用微服务不行吗?

A: 直接调用微服务的问题:

  • 前端需要处理多个服务的不同认证方式
  • 需要发起多个请求,影响性能
  • 服务接口变更时,前端需要同步修改
  • 前端需要了解所有服务的业务逻辑

BFF 解决了这些问题,让前端开发更简单。

Q: BFF 会成为单点故障吗?

A: 可以通过以下方式避免:

  • 部署多个 BFF 实例
  • 使用负载均衡
  • 实现熔断降级机制
  • 监控 BFF 健康状况

Q: 如何处理 BFF 与微服务的版本兼容?

A:

  • BFF 作为适配层,可以处理不同版本的微服务接口
  • 微服务升级时,BFF 层进行适配,前端无感知
  • 使用契约测试确保接口兼容性

🎯 下一步学习建议

  1. 学习 Midway 框架:深入了解依赖注入、装饰器等高级特性
  2. 学习 OpenAPI 规范:掌握更复杂的 API 文档编写
  3. 学习缓存策略:在 BFF 层添加 Redis 缓存
  4. 学习监控告警:为 BFF 添加日志、监控、告警功能
  5. 学习安全加固:添加限流、防刷、SQL 注入防护等

恭喜!你已经成功搭建了一个完整的 BFF 架构项目。
这套架构已在多个大型项目中验证,兼顾开发效率架构清晰性

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐