# 什么是 BFF
BFF 全称是 Backend For Frontend (服务于前端的后端),也就是服务器设计 API 时会考虑前端的使用,并在服务端直接进行业务逻辑的处理,又称为用户体验适配器。BFF 只是一种逻辑分层,而非一种技术,虽然 BFF 是一个新名词,但它的理念由来已久。
# BFF 解决了什么问题
在实际项目中,经常会有某个页面需要向多个不同的服务发送请求,然后将请求回来的数据用来渲染页面不同的组件的情况,即一个页面同时存在多个请求的场景。假设每次访问一个页面都需要发送 3 个请求,同时为了保障 Android,iOS,以及 Web 端的不同需求,需要为不同的平台写不同的 API 接口,而每当值发生一些变化时,需要 Android,iOS,Web 做出修改。这样的代价显然是相当大的,也相当麻烦。
因此,我们就需要 BFF 作为中间层,在这个中间层上做一些业务逻辑处理。当有了 BFF 这一层之后,我们就不需要考虑系统后端的迁移了,后端发生的变化都可以在 BFF 层做一些响应修改。像上面所说的场景,加入了 BFF 层之后,原本每次访问发送 3 个请求,现在就变成一个请求了。
# BFF 的好处
为特定前端定制 API 接口
聚合多个后端服务的数据
处理认证授权、日志记录等横切关注点
提供更好的错误处理和缓存策略
减少前端的业务逻辑复杂度
什么是横切关注点
横切关注点(Cross-cutting Concerns)指的是那些贯穿整个应用多个模块的功能,比如认证、授权、日志、缓存等。
# 如何正确使用 BFF
- 多端应用
我们在设计 API 时会考虑到不同设备的需求,也就是为不同的设备提供不同的 API,虽然它们可能是实现相同的功能,但因为不同设备的特殊性,它们对服务端的 API 访问也各有其特点,需要区别处理。
- 服务聚合
随着微服务的兴起,原本在同一个进程内运行的业务流程被拆分到了不同的服务中。这在增加业务灵活性的同时,也让前端的调用变得更复杂。BFF 的出现为前端应用提供了一个对业务服务调用的聚合点,它屏蔽了复杂的服务调用链,让前端可以聚焦在所需要的数据上,而不用关注底层提供这些数据的服务。
- 非必要,莫新增
我们在看到 BFF 带来的各种好处的同时,也要注意到它所带来的代码重复和工作量增加方面的问题。如果与已有 BFF 功能类似,且展现数据的要求也相近的话,一定要谨慎对待新增 BFF 的行为。因此,建议非必要,莫新增。
# BFF 的实战应用
- 访问控制
例如,服务中的权限控制,将所有服务中的权限控制集中在 BFF 层,使下层服务更加纯粹和独立。
- 应用缓存
项目中时常存在一些需要缓存的临时数据,此时 BFF 作为业务的汇聚点,距离用户请求最近,遂将该缓存操作放在 BFF 层。
- 第三方入口
在业务中需要与第三交互时,将该交互放在 BFF 层,这样可以只暴露必要信息给第三方,从而便于控制第三方的访问。
# SOLID 设计原则
SOLID 是面向对象设计的五大基本原则。
# S - 单一职责原则 (Single Responsibility Principle)
一个组件只做一件事
❌ 错误:一个组件做太多事
// 这个组件又要显示用户,又要获取数据,又要处理样式
const UserComponent = () => {
const [user, setUser] = useState(null);
// 获取数据
useEffect(() => {
fetch('/api/user').then(res => res.json()).then(setUser);
}, []);
// 处理样式
const getStatusColor = (status) => {
return status === 'active' ? 'green' : 'red';
};
// 显示界面
return (
<div style={{ color: getStatusColor(user?.status) }}>
{user?.name}
</div>
);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
✅ 正确:拆分成多个组件,各司其职
// 1. 获取数据的 Hook
const useUser = () => {
const [user, setUser] = useState(null);
useEffect(() => {
fetch('/api/user').then(res => res.json()).then(setUser);
}, []);
return user;
};
// 2. 处理样式的工具
const getStatusColor = (status) => {
return status === 'active' ? 'green' : 'red';
};
// 3. 只负责显示的组件
const UserDisplay = ({ user }) => (
<div style={{ color: getStatusColor(user.status) }}>
{user.name}
</div>
);
// 4. 组合使用
const UserComponent = () => {
const user = useUser();
return user ? <UserDisplay user={user} /> : <div>加载中...</div>;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# O - 开闭原则 (Open/Closed Principle)
组件应该对扩展开放,对修改关闭
❌ 错误:每次新增功能都要改原组件
const Button = ({ type, children }) => {
// 每次新增按钮类型都要改这里
if (type === 'save') {
return <button style={{ background: 'blue' }}>{children}</button>;
}
if (type === 'delete') {
return <button style={{ background: 'red' }}>{children}</button>;
}
// 要新增 'edit' 类型?又要改这里...
};
2
3
4
5
6
7
8
9
10
✅ 正确:通过配置扩展,不修改原组件
const Button = ({ style, children, ...props }) => (
<button style={style} {...props}>
{children}
</button>
);
// 扩展:定义不同样式,不用改原组件
const SaveButton = ({ children, ...props }) => (
<Button style={{ background: 'blue' }} {...props}>
{children}
</Button>
);
const DeleteButton = ({ children, ...props }) => (
<Button style={{ background: 'red' }} {...props}>
{children}
</Button>
);
// 新增功能很简单,不用改原组件
const EditButton = ({ children, ...props }) => (
<Button style={{ background: 'green' }} {...props}>
{children}
</Button>
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# L - 里氏替换原则 (Liskov Substitution Principle)
子组件应该能够替换其父组件而不破坏程序功能
❌ 错误:子组件改变了使用方式
// 普通输入框
const Input = ({ value, onChange }) => (
<input value={value} onChange={onChange} />
);
// 数字输入框 - 改变了使用方式!
const NumberInput = ({ value, onChange }) => (
<input
value={value}
onChange={(e) => onChange(Number(e.target.value))} // 这里改变了 onChange 的参数类型
/>
);
// 使用时会出问题
const Form = ({ InputComponent }) => {
const [value, setValue] = useState('');
return (
<InputComponent
value={value}
onChange={setValue} // Input 传字符串,NumberInput 传数字,不一致!
/>
);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
✅ 正确:保持一致的使用方式
// 基础输入框
const Input = ({ value, onChange }) => (
<input value={value} onChange={onChange} />
);
// 数字输入框 - 保持相同的接口
const NumberInput = ({ value, onChange }) => (
<input
type="number"
value={value}
onChange={onChange} // 保持相同的参数类型
/>
);
// 现在可以安全替换
const Form = ({ InputComponent }) => {
const [value, setValue] = useState('');
return (
<InputComponent
value={value}
onChange={(e) => setValue(e.target.value)} // 两个组件都能正常工作
/>
);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# I - 接口隔离原则 (Interface Segregation Principle)
不应该强迫组件依赖它们不使用的接口
❌ 错误:传递整个大对象
const user = {
id: 1,
name: 'John',
email: 'john@example.com',
age: 25,
address: '北京市朝阳区',
phone: '13800138000',
preferences: {...},
permissions: {...}
};
// 只要显示名字,却要传整个用户对象
const UserName = ({ user }) => <span>{user.name}</span>;
// 只要显示头像,也要传整个用户对象
const UserAvatar = ({ user }) => <img src={user.avatar} />;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
✅ 正确:只传需要的数据
// 只传需要的 name
const UserName = ({ name }) => <span>{name}</span>;
// 只传需要的 avatar 和 name
const UserAvatar = ({ avatar, name }) => <img src={avatar} alt={name} />;
// 使用时按需传递
const UserCard = ({ user }) => (
<div>
<UserAvatar avatar={user.avatar} name={user.name} />
<UserName name={user.name} />
</div>
);
2
3
4
5
6
7
8
9
10
11
12
13
# D - 依赖反转原则 (Dependency Inversion Principle)
高层模块不应该依赖低层模块,两者都应该依赖抽象
❌ 错误:直接依赖具体的 API
const UserList = () => {
const [users, setUsers] = useState([]);
useEffect(() => {
// 直接写死了 fetch,难以测试和替换
fetch('/api/users')
.then(res => res.json())
.then(setUsers);
}, []);
return (
<div>
{users.map(user => <div key={user.id}>{user.name}</div>)}
</div>
);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
✅ 正确:通过参数传入具体实现
// 组件不关心数据怎么来的
const UserList = ({ getUsers }) => {
const [users, setUsers] = useState([]);
useEffect(() => {
getUsers().then(setUsers);
}, [getUsers]);
return (
<div>
{users.map(user => <div key={user.id}>{user.name}</div>)}
</div>
);
};
// 使用时传入具体实现
const App = () => {
// 真实环境用真实 API
const realGetUsers = () => fetch('/api/users').then(res => res.json());
// 测试环境用假数据
const mockGetUsers = () => Promise.resolve([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' }
]);
const getUsers = process.env.NODE_ENV === 'test' ? mockGetUsers : realGetUsers;
return <UserList getUsers={getUsers} />;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 控制反转 IoC
控制反转是一种设计思想:把对象创建和依赖管理的控制权从组件本身转移给外部容器。
传统方式:我需要什么,我自己创建什么
控制反转:我需要什么,别人给我什么
# IoC 的好处
降低耦合度:组件不依赖具体实现
提高可测试性:可以注入 mock 对象
// 测试时注入 mock 服务
const mockServices = {
userService: {
getUsers: () => Promise.resolve([{ id: 1, name: 'Test' }])
}
};
render(
<ServiceProvider services={mockServices}>
<UserList />
</ServiceProvider>
);
2
3
4
5
6
7
8
9
10
11
12
- 增强可配置性:外部控制所有依赖
// 可以轻松切换不同环境的配置
const devConfig = { apiBase: 'http://localhost:3000' };
const prodConfig = { apiBase: 'https://api.prod.com' };
<ConfigProvider config={isDev ? devConfig : prodConfig}>
<App />
</ConfigProvider>
2
3
4
5
6
7
- 便于维护:依赖变更不影响组件
// 新增服务不需要修改组件
const enhancedServices = {
...originalServices,
analyticsService: new AnalyticsService()
};
<ServiceProvider services={enhancedServices}>
<App />
</ServiceProvider>
2
3
4
5
6
7
8
9
# 自定义 IoC
使用 InversifyJS、Acorn 和 Reflect(JavaScript 的反射 API)可以实现一个自定义的 IoC 容器。
1. InversifyJS
作用:InversifyJS (opens new window) 是一个现成的 IoC 容器,提供依赖注入功能,通过装饰器(@injectable、@inject)和绑定机制管理依赖。
在自定义 IoC 中的用途:可以作为参考或直接复用其容器逻辑(Container 类、绑定机制等),避免重复造轮子。如果要完全自定义,InversifyJS 的源码可以提供灵感(如依赖解析、生命周期管理)。
局限:它是现成工具,直接使用可能限制定制化程度,但适合快速验证 IoC 概念。
2. Acorn
作用:Acorn (opens new window) 是一个 JavaScript 解析器,能将代码解析为抽象语法树(AST),允许分析类、函数、装饰器等结构。
在自定义 IoC 中的用途:通过解析源代码,提取类定义、构造函数参数或自定义装饰器信息,用于自动注册依赖或推断依赖关系。
优势:支持静态代码分析,能处理动态注入场景(如根据文件结构自动注册服务)。
3. Reflect
作用:Reflect (opens new window) 是 ECMAScript 的内置 API,提供运行时元数据操作(如 Reflect.getMetadata),常与 reflect-metadata (opens new window) 库结合使用,用于存储和读取类/方法的元数据(如依赖类型)。
在自定义 IoC 中的用途:通过元数据存储依赖信息,动态解析构造函数参数,实现依赖注入的自动化。
优势:与 TypeScript 配合良好,支持装饰器元数据的动态注入。
实现思路
1. 依赖注册
使用 Acorn 解析 JavaScript/TypeScript 代码,识别类和装饰器(如 @injectable),提取构造函数参数。
使用 Reflect 和 reflect-metadata 存储类或方法的元数据(如依赖的类型标识符)。
2. 容器实现
仿照 InversifyJS 的 Container,实现一个简单的容器类,支持绑定(bind)和解析(resolve)。
支持生命周期管理(如单例、瞬时)。
3. 依赖解析
使用 Reflect 获取构造函数的元数据,解析依赖链。
动态创建实例并注入依赖。
4. 自动加载(可选)
- 使用 Acorn 扫描指定目录的代码文件,自动注册所有带有特定装饰器的类。
# 依赖注入 DI
依赖注入是一种依赖管理模式:不在组件内部创建依赖,而是从外部传入依赖,让组件专注于业务逻辑。
依赖注入是控制反转的一种实现方式。
# DI 的好处
更容易测试、复用、维护、扩展。
- 更好的测试性
// 测试时可以注入 mock 服务
const mockUserService = {
getUsers: jest.fn().mockResolvedValue([
{ id: 1, name: 'Test User' }
])
};
render(<UserList userService={mockUserService} />);
2
3
4
5
6
7
8
- 更高的灵活性
// 开发环境用 mock 数据
const devService = { getUsers: () => Promise.resolve(mockData) };
// 生产环境用真实 API
const prodService = { getUsers: () => fetch('/api/users').then(r => r.json()) };
const service = isDevelopment ? devService : prodService;
2
3
4
5
6
7
- 更低的耦合度
// 组件不知道数据来源,只知道接口
const UserList = ({ dataSource }) => {
// dataSource 可以是 API、localStorage、IndexedDB 等任何实现
};
2
3
4
# Awilix 和 Awilix-Koa
Awilix (opens new window) 是一个使用 TypeScript 编写的 JavaScript/Node 依赖注入容器库。它允许开发者通过 DI 模式构建可组合、可测试的软件,而无需特殊的注解或装饰器,从而将核心应用代码与 DI 机制解耦。
Awilix-Koa (opens new window) 是 Awilix 的官方 Koa 2 集成包,提供中间件、路由器和作用域管理工具,帮助在 Koa 应用中无缝使用 Awilix。它主要解决 Web 应用中的依赖注入问题,特别是每个 HTTP 请求创建一个独立的依赖作用域(scopePerRequest),避免状态共享。
# InversifyJS
InversifyJS (opens new window) 是一个轻量级、强大的依赖注入(Dependency Injection, DI)和控制反转(Inversion of Control, IoC)容器,专为 TypeScript 和 JavaScript 应用设计。它通过类构造函数识别和注入依赖,支持 SOLID 原则和最佳 OOP 实践,帮助开发者构建模块化、可测试的代码。
InversifyJS 是框架无关的,可与 Express、Hapi、React 等集成,支持 ES5/ES6+,并编译为纯 JavaScript 代码,可在浏览器、Node.js 或任何支持 ECMAScript 2022+ 的环境中运行。
核心优势:
装饰器支持:使用 @injectable 和 @inject 装饰器简化注入,无需手动管理依赖。
绑定类型:支持构造函数注入、标签(tags)、命名绑定和多重注入。
生命周期管理:单例、瞬时等模式。
测试友好:易于 mock 依赖,提高单元测试效率。
适用场景:
大型 Node.js/前端应用,如 API 服务、微服务或复杂组件架构。
# Awilix 和 InversifyJS 的区别
1. 依赖注入实现不同
InversifyJS:
使用装饰器显式声明依赖,依赖 TypeScript 的元数据反射(reflect-metadata)。
构造函数注入需明确指定依赖的标识符(如字符串或符号)。
import { Container, injectable, inject } from 'inversify';
import 'reflect-metadata';
@injectable()
class Katana {
hit() {
return 'Cutting with Katana!';
}
}
@injectable()
class Ninja {
constructor(@inject('Katana') private katana: Katana) {}
fight() {
return this.katana.hit();
}
}
const container = new Container();
container.bind<Katana>('Katana').to(Katana).inSingletonScope();
container.bind<Ninja>('Ninja').to(Ninja);
const ninja = container.get<Ninja>('Ninja');
console.log(ninja.fight()); // 输出: Cutting with Katana!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Awilix:
无需装饰器,通过容器注册函数(如 asClass、asFunction)定义依赖。
支持代理模式(Proxy),自动解析构造函数参数,减少配置。
const awilix = require('awilix');
class Katana {
hit() {
return 'Cutting with Katana!';
}
}
class Ninja {
constructor({ katana }) { // 自动注入
this.katana = katana;
}
fight() {
return this.katana.hit();
}
}
const container = awilix.createContainer();
container.register({
katana: awilix.asClass(Katana).singleton(),
ninja: awilix.asClass(Ninja).singleton()
});
const ninja = container.resolve('ninja');
console.log(ninja.fight()); // 输出: Cutting with Katana!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2. 如何选择
如果你使用 TypeScript,追求类型安全和企业级架构,选择 InversifyJS。
如果你偏好 JavaScript 或轻量级开发,追求简单性和性能,选择 Awilix。
如果项目基于 Koa 或 Express,Awilix 的内置集成会更省力。
如果需要跨框架或复杂注入场景,InversifyJS 更灵活。
# NestJS
NestJS (opens new window) 内置了一个强大的依赖注入(Dependency Injection, DI)系统,基于 TypeScript 和 装饰器,深受 Angular 和 InversifyJS 的启发。NestJS 的 DI 系统是其核心特性之一,用于管理模块、服务、控制器等组件的依赖关系,简化代码组织,提高可测试性和模块化程度。
1. NestJS 依赖注入系统
基于装饰器:NestJS 使用 TypeScript 的装饰器(如 @Injectable、@Inject)来标记可注入的服务或自定义提供者。
模块化架构:依赖注入基于模块(@Module),每个模块可以定义提供者(providers)、控制器(controllers)和其他依赖。
自动解析:NestJS 利用 TypeScript 的元数据(reflect-metadata)自动解析构造函数参数的类型,无需显式指定依赖标识符(除非使用自定义提供者)。
作用域管理:
单例(Singleton):默认作用域,同一实例在整个应用中共享。
瞬时(Transient):每次解析生成新实例。
请求作用域(Request-Scoped):为每个 HTTP 请求创建新实例,适合 Web 应用。
内置容器:NestJS 内置 IoC 容器,管理所有依赖的注册和解析,无需额外引入第三方 DI 库。
框架集成:与 Express 和 Fastify 无缝集成,支持 HTTP、WebSocket、微服务等场景。
# NestJS DI、InversifyJS 和 Awilix 的区别
NestJS DI:适合构建复杂的全栈应用或微服务,特别是有模块化需求(如 REST API、GraphQL、WebSocket)。如果你已经在用 NestJS,无需引入其他 DI 库。
InversifyJS:适合需要独立 DI 容器的场景,如非 Web 项目(CLI、React)或跨框架复用。
Awilix:适合快速开发的 Node.js 项目,特别是 Koa 或 Express 生态,注重简单性和性能。
# 面向切面编程 AOP
AOP 是一种编程思想:把横切关注点(Cross-cutting Concerns)从业务逻辑中分离出来,统一处理。
传统编程:每个函数都要写日志、错误处理、性能监控等
AOP 编程:把这些通用逻辑提取出来,自动 "织入" 到需要的地方
常见的横切关注点:
🪵 日志记录:记录函数调用、参数、返回值
⚠️ 错误处理:统一捕获和处理错误
⏱️ 性能监控:测量函数执行时间
🔐 权限验证:检查用户权限
💾 缓存管理:缓存函数结果
📊 数据埋点:用户行为统计
# AOP 的好处
提高代码复用性
减少代码重复
增强可维护性
保持业务逻辑纯净
# 使用装饰器实现 AOP
// 定义各种切面
const withLogging = (target, propertyName, descriptor) => {
const originalMethod = descriptor.value;
descriptor.value = async function(...args) {
console.log(`📝 调用 ${propertyName},参数:`, args);
const result = await originalMethod.apply(this, args);
console.log(`📝 ${propertyName} 返回:`, result);
return result;
};
return descriptor;
};
const withPerformance = (target, propertyName, descriptor) => {
const originalMethod = descriptor.value;
descriptor.value = async function(...args) {
const startTime = Date.now();
const result = await originalMethod.apply(this, args);
console.log(`⏱️ ${propertyName} 耗时: ${Date.now() - startTime}ms`);
return result;
};
return descriptor;
};
const withErrorHandling = (target, propertyName, descriptor) => {
const originalMethod = descriptor.value;
descriptor.value = async function(...args) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
console.error(`❌ ${propertyName} 执行失败:`, error);
throw error;
}
};
return descriptor;
};
// 使用装饰器 - 业务逻辑非常清爽
class UserService {
@withLogging
@withPerformance
@withErrorHandling
async getUsers() {
// 只关心业务逻辑
const response = await fetch('/api/users');
return response.json();
}
@withLogging
@withPerformance
@withErrorHandling
async createUser(userData) {
// 只关心业务逻辑
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(userData)
});
return response.json();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# 使用高阶组件 (HOC) 实现 AOP
// 日志记录的 HOC
const withLogging = (WrappedComponent, componentName) => {
return (props) => {
console.log(`🔍 渲染 ${componentName},props:`, props);
return <WrappedComponent {...props} />;
};
};
// 性能监控的 HOC
const withPerformance = (WrappedComponent, componentName) => {
return (props) => {
const renderStart = Date.now();
useEffect(() => {
console.log(`⚡ ${componentName} 渲染耗时: ${Date.now() - renderStart}ms`);
});
return <WrappedComponent {...props} />;
};
};
// 错误边界的 HOC
const withErrorBoundary = (WrappedComponent) => {
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('🚨 组件错误:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return <div>组件出错了</div>;
}
return <WrappedComponent {...this.props} />;
}
}
return ErrorBoundary;
};
// 业务组件 - 只关心业务逻辑
const UserList = ({ users }) => (
<div>
{users.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
// 使用多个切面增强组件
const EnhancedUserList = withErrorBoundary(
withPerformance(
withLogging(UserList, 'UserList'),
'UserList'
)
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# 使用自定义 Hook 实现 AOP
// 网络请求的 AOP Hook
const useApiCall = (apiFunc, options = {}) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const execute = useCallback(async (...args) => {
// 日志切面
console.log('🌐 开始 API 调用:', apiFunc.name, args);
// 性能切面
const startTime = Date.now();
try {
setLoading(true);
setError(null);
// 执行实际的 API 调用
const result = await apiFunc(...args);
setData(result);
// 性能日志
console.log(`⏱️ API 调用耗时: ${Date.now() - startTime}ms`);
// 成功日志
console.log('✅ API 调用成功:', result);
return result;
} catch (err) {
// 错误处理切面
console.error('❌ API 调用失败:', err);
setError(err);
throw err;
} finally {
setLoading(false);
}
}, [apiFunc]);
return {
data,
loading,
error,
execute
};
};
// 使用 AOP Hook
const UserManagement = () => {
const getUsersCall = useApiCall(
async () => {
const response = await fetch('/api/users');
return response.json();
}
);
const createUserCall = useApiCall(
async (userData) => {
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(userData)
});
return response.json();
}
);
return (
<div>
<button onClick={getUsersCall.execute}>
{getUsersCall.loading ? '加载中...' : '获取用户'}
</button>
{getUsersCall.error && (
<div>错误: {getUsersCall.error.message}</div>
)}
{getUsersCall.data && (
<div>用户数量: {getUsersCall.data.length}</div>
)}
</div>
);
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
← Node.js Koa BFF 实战 →
