前端架构学习-2:典型分层中业务层与数据层的区别
前端应用的典型分层为:
-
表现层(Presentation Layer):组件树/Virtual DOM管理
-
业务逻辑层(Domain Layer):Redux中间件/Service封装
-
数据层(Data Layer):SWR/React Query数据获取策略
笔者对分层架构中各层的职责划分不太清楚,尤其是业务逻辑层和数据层之间的界限。在此之前实际开发中都是将数据获取和业务逻辑混在一起,导致层次不清晰。
当我查询各分层的职责时:业务逻辑层应该处理应用的核心逻辑,比如数据转换、验证、流程控制等,而数据层则专注于数据的获取和存储,包括与API的交互、缓存管理。即使两者都涉及数据操作,但职责不同:业务逻辑层处理如何处理数据,数据层处理如何获取数据。比如,Redux中间件可能处理业务逻辑中的异步操作,如处理订单时的库存检查,而Service封装可能聚合多个数据源,进行数据转换。而SWR/React Query则专注于高效获取数据,处理缓存和重试,属于数据层的技术细节。
一、核心差异:职责划分的本质
1. 数据层(Data Layer)
-
核心职责:
"如何获取数据" —— 聚焦于数据来源的技术实现细节-
数据获取方式(REST/GraphQL/WebSocket)
-
缓存策略(SWR的stale-while-revalidate)
-
请求重试机制(指数退避策略)
-
网络错误处理(401跳登录/503降级)
-
-
技术特征:
-
与业务无关:同一套数据层可服务不同业务模块
-
技术细节封装:隐藏fetch/XHR具体实现
-
性能优化:自动缓存/预加载/请求合并
-
-
代码示例:
// 数据层封装(纯技术实现) const apiClient = { get: (url) => axios.get(url).catch(handleNetworkError), post: (url, data) => axios.post(url, data) }; // React Query配置(数据策略) const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 5 * 60 * 1000, // 缓存策略 retry: 2 // 重试机制 } } });
2. 业务逻辑层(Business Logic Layer)
-
核心职责:
"如何处理数据" —— 聚焦于业务规则和流程控制-
数据转换(API数据 → 前端模型)
-
业务验证(库存检查/权限校验)
-
流程编排(下单 → 支付 → 通知的时序控制)
-
领域逻辑(购物车优惠计算)
-
-
技术特征:
-
强业务耦合:直接对应产品需求文档中的业务规则
-
可测试性:可脱离UI进行单元测试
-
技术无关:不关心数据来源(可替换为Mock数据)
-
-
代码示例:
// 业务服务类(核心逻辑) class OrderService { constructor(paymentService, notificationService) { // 依赖注入其他业务服务 this.payment = paymentService; this.notification = notificationService; } async placeOrder(cartItems) { // 业务验证 if (!this.validateStock(cartItems)) { throw new Error('库存不足'); } // 数据转换 const order = this.createOrderDTO(cartItems); // 流程编排 const paymentResult = await this.payment.process(order); await this.notification.sendOrderConfirmation(order); return { order, paymentResult }; } }
二、为什么需要分离?
1. 关注点不同导致的变更频率差异
-
数据层变更:
当后端API升级(REST → GraphQL)或更换HTTP库(axios → fetch)时,只需修改数据层,业务逻辑层无感知。 -
业务层变更:
当产品需求调整(如新增优惠券校验规则),只需修改业务逻辑层,数据层无需变动。
2. 典型场景对比
| 场景 | 数据层处理 | 业务逻辑层处理 |
|---|---|---|
| 获取用户列表 | 实现分页参数拼接/缓存更新策略 | 过滤VIP用户/计算用户活跃度 |
| 提交订单 | 处理请求超时重试 | 验证库存/计算总价/生成订单流水号 |
3. 错误处理差异
// 数据层错误(技术性错误)
async function fetchUser(id) {
try {
return await axios.get(`/users/${id}`);
} catch (error) {
if (error.response?.status === 404) {
throw new DataLayerError('用户不存在'); // 技术语义错误
}
throw error;
}
}
// 业务逻辑层错误(业务语义错误)
async function upgradeUserLevel(userId) {
const user = await fetchUser(userId);
if (user.level === 'VIP') {
throw new BusinessError('已是最高等级'); // 业务语义错误
}
return await apiClient.patch(`/users/${userId}`, { level: 'VIP' });
}
三、Service封装与数据获取的关系
1. 常见误区:混合写法
// ❌ 混合数据获取与业务逻辑
class ProblematicService {
async getVIPOrders() {
// 数据获取细节暴露(属于数据层)
const response = await axios.get('/orders', {
params: { status: 'paid' },
headers: { 'X-Cache': 'no-store' }
});
// 业务逻辑(属于业务层)
return response.data.filter(order =>
order.user.level === 'VIP' &&
order.total > 1000
);
}
}
2. 正确分层实践
// ✅ 数据层(仅关注获取)
const orderApi = {
getOrders: (params) =>
axios.get('/orders', { params })
.then(res => res.data)
};
// ✅ 业务逻辑层(仅关注处理)
class OrderService {
constructor(apiClient) {
this.api = apiClient;
}
async getVIPOrders() {
const allOrders = await this.api.getOrders({ status: 'paid' });
return allOrders.filter(order =>
order.user.level === 'VIP' &&
order.total > 1000
);
}
}
// 使用层
const orderService = new OrderService(orderApi);
const vipOrders = await orderService.getVIPOrders();
四、为什么SWR/React Query属于数据层?
1. 技术特性分析
-
缓存策略:
stale-while-revalidate模式实现 -
请求去重:同一时刻相同key的请求自动合并
-
自动重试:网络错误时的重试机制
-
预加载:
preloadQuery等优化手段
这些都属于 "如何高效获取数据" 的技术细节,不涉及具体业务规则。
2. 使用示例
// 数据层封装(技术细节)
function useUserData(userId) {
return useQuery(['user', userId], () =>
fetch(`/api/users/${userId}`).then(res => res.json()),
{
staleTime: 1000 * 60 * 5, // 缓存5分钟
retryDelay: attempt => Math.min(attempt * 1000, 30 * 1000) // 重试策略
}
);
}
// 业务逻辑层(业务规则)
function useVIPUsers() {
const { data: allUsers } = useUserData();
return useMemo(() => {
return allUsers?.filter(user =>
user.level === 'VIP' &&
user.lastActive > Date.now() - 30 * 86400 * 1000
) ?? [];
}, [allUsers]);
}
// 表现层
function VIPUserList() {
const vipUsers = useVIPUsers();
return (
<ul>
{vipUsers.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
五、分层带来的实际收益
1. 可测试性对比
// 测试业务逻辑层(无需真实API)
test('should filter VIP users correctly', () => {
// 准备Mock数据
const mockApi = {
getOrders: jest.fn().mockResolvedValue([
{ id: 1, user: { level: 'VIP' }, total: 2000 },
{ id: 2, user: { level: 'normal' }, total: 500 }
])
};
const service = new OrderService(mockApi);
const result = await service.getVIPOrders();
expect(result).toEqual([{ id: 1, ... }]); // 纯业务逻辑测试
});
// 测试数据层(验证缓存策略)
test('should apply staleTime correctly', async () => {
const { result, rerender } = renderHook(
() => useUserData('123'),
{ wrapper: QueryClientProvider }
);
await waitFor(() => expect(result.current.isSuccess).toBe(true));
expect(queryClient.getQueryData(['user', '123'])).toBeDefined(); // 验证缓存
});
2. 架构演进案例
场景:从REST迁移到GraphQL
-
数据层改动:
// 修改前(REST) const getUser = id => axios.get(`/users/${id}`); // 修改后(GraphQL) const getUser = id => client.query({ query: gql` query ($id: ID!) { user(id: $id) { id name level } } `, variables: { id } }); -
业务逻辑层:零改动,因为接口契约(输入输出)保持一致
六、常见分层误区与修正
| 错误模式 | 问题 | 修正方案 |
|---|---|---|
| 在业务Service中直接使用axios | 业务代码与技术实现耦合 | 通过依赖注入抽象数据访问接口 |
| 在数据层进行数据过滤 | 污染缓存数据 | 保持数据层返回原始数据,在业务层过滤 |
| 业务层处理HTTP状态码 | 混淆技术错误与业务错误 | 在数据层统一转换技术错误 |
| 表现层直接调用数据层 | 破坏分层架构 | 强制通过业务层桥接 |
七、如何确定代码属于哪一层?
使用决策树进行判断:
-
这段代码是否与 产品需求文档 中的业务规则直接相关?
-
是 → 业务逻辑层
-
否 → 进入下一问题
-
-
这段代码是否处理 数据如何获取/存储 的技术细节?
-
是 → 数据层
-
否 → 可能属于基础设施层或公用工具层
-
-
这段代码是否管理 用户交互状态?
-
是 → 表现层
-
否 → 需要重新审视代码职责
-
通过这种清晰的职责划分,可以带来以下收益:
-
独立演进能力:数据层可替换请求库而不影响业务逻辑
-
可复用性:同一数据层服务多个业务模块
-
可测试性:业务逻辑可脱离UI和数据源进行测试
-
团队协作:前端与后端基于数据层接口进行契约开发
建议在实际项目中通过依赖注入明确层间关系,例如使用TypeScript接口约束数据层契约,确保分层架构的严格执行。

浙公网安备 33010602011771号