基于 Umi + React + Midway 的 BFF 最佳实践
通过上篇《React前端如何理解、学习和落地 BFF 的完整路径》文章,结合技术栈(Umi + React + @midwayjs/koa),实现一个 企业级、安全可控、可独立部署的 BFF 落地路径。
🎯 什么是 BFF?为什么需要它?
🤔 传统架构的问题
假设你开发一个电商中台,需要显示首页数据:
传统方式:前端直接调用多个微服务
前端 → 用户服务(获取用户信息)
→ 商品服务(获取推荐商品)
→ 订单服务(获取最近订单)
→ 广告服务(获取广告)
问题:
- ❌ 前端需要发起 4 个请求,页面加载慢
- ❌ 每个服务的认证方式可能不同(JWT、API Key、时间戳等)
- ❌ 每个服务返回的数据结构不一致,前端需要分别处理
- ❌ 前端需要了解所有微服务的接口细节
- ❌ 后端接口变更时,前端需要同步修改多处代码
✅ BFF 架构的优势
前端 → BFF(Backend For Frontend)→ 用户服务
(聚合数据) → 商品服务
→ 订单服务
→ 广告服务
优势:
- ✅ 前端只需 1 个请求
- ✅ 统一认证(前端只需向 BFF 发送 JWT)
- ✅ 统一数据格式(BFF 返回前端需要的结构)
- ✅ 前端无需关心下游服务细节
- ✅ 接口变更时,只需修改 BFF,前端无感知
🎨 本文要实现的功能
我们将创建一个完整的 BFF 项目,包含:
- 前端页面:显示用户信息和推荐内容
- BFF 服务:聚合用户服务和内容服务的数据
- 安全认证:前端向 BFF 发送 JWT,BFF 向微服务发送时间戳签名
- 自动生成接口: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/:定义前端可以访问的 APIbff/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-Timestamp和X-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 层进行适配,前端无感知
- 使用契约测试确保接口兼容性
🎯 下一步学习建议
- 学习 Midway 框架:深入了解依赖注入、装饰器等高级特性
- 学习 OpenAPI 规范:掌握更复杂的 API 文档编写
- 学习缓存策略:在 BFF 层添加 Redis 缓存
- 学习监控告警:为 BFF 添加日志、监控、告警功能
- 学习安全加固:添加限流、防刷、SQL 注入防护等
恭喜!你已经成功搭建了一个完整的 BFF 架构项目。
这套架构已在多个大型项目中验证,兼顾开发效率与架构清晰性。
更多推荐


所有评论(0)