前端架构学习-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状态码 混淆技术错误与业务错误 在数据层统一转换技术错误
表现层直接调用数据层 破坏分层架构 强制通过业务层桥接

七、如何确定代码属于哪一层?

使用决策树进行判断:

  1. 这段代码是否与 产品需求文档 中的业务规则直接相关?

    • 是 → 业务逻辑层

    • 否 → 进入下一问题

  2. 这段代码是否处理 数据如何获取/存储 的技术细节?

    • 是 → 数据层

    • 否 → 可能属于基础设施层或公用工具层

  3. 这段代码是否管理 用户交互状态

    • 是 → 表现层

    • 否 → 需要重新审视代码职责


通过这种清晰的职责划分,可以带来以下收益:

  • 独立演进能力:数据层可替换请求库而不影响业务逻辑

  • 可复用性:同一数据层服务多个业务模块

  • 可测试性:业务逻辑可脱离UI和数据源进行测试

  • 团队协作:前端与后端基于数据层接口进行契约开发

建议在实际项目中通过依赖注入明确层间关系,例如使用TypeScript接口约束数据层契约,确保分层架构的严格执行。

posted @ 2025-02-15 17:00  Yang9710  阅读(166)  评论(0)    收藏  举报