Administrator
发布于 2026-04-30 / 5 阅读
0
0

首页的最终方案

大厂一般不是“页面自己决定怎么加载”,而是做成一套:

配置化页面系统 + 组件注册系统 + 任务调度系统 + 缓存降级系统 + 埋点监控系统

首页大致是这样:

App启动
 ↓
预拉取基础配置
 ↓
进入首页
 ↓
读取本地首页配置缓存
 ↓
快速渲染骨架/旧数据
 ↓
请求最新首页布局配置
 ↓
生成组件加载任务队列
 ↓
首屏组件优先加载
 ↓
非首屏组件按优先级/距离/空闲状态预加载
 ↓
组件独立成功/失败/重试
 ↓
曝光/点击/转化埋点

一、大厂首页不会这样写

不会是:

HomePage initState() {
  loadBanner();
  loadMenu();
  loadGoods();
  loadCoupon();
}

也不会是:

组件曝光了才开始请求

因为这会导致:

首屏慢
滑到哪里卡到哪里
请求失控
没有统一优先级
无法灰度
无法配置
无法降级

二、大厂更像这样

1. 首页布局接口

先请求一个“页面结构配置”。

{
  "pageId": "home_main",
  "version": "2026.04.30.001",
  "style": {
    "backgroundColor": "#F7F7F7",
    "navigationBar": "transparent"
  },
  "components": [
    {
      "id": "banner_001",
      "type": "banner",
      "sort": 1,
      "loadPolicy": {
        "mode": "immediate",
        "priority": 100,
        "cache": "staleWhileRevalidate"
      }
    },
    {
      "id": "menus_001",
      "type": "menus",
      "sort": 2,
      "loadPolicy": {
        "mode": "firstScreen",
        "priority": 90
      }
    },
    {
      "id": "recommend_001",
      "type": "goodsList",
      "sort": 10,
      "loadPolicy": {
        "mode": "preload",
        "priority": 40,
        "preloadDistance": 1600
      }
    }
  ]
}

这个接口只负责告诉客户端:

有什么组件
什么顺序
什么样式
怎么加载
数据源在哪里
失败怎么降级
缓存怎么用

三、客户端核心模块

大厂首页一般会拆成这些东西:

HomeLayoutManager
HomeComponentRegistry
HomeLoadPlanner
HomeLoadScheduler
HomeComponentLoader
HomeCacheManager
HomeExposureTracker

分别负责:

HomeLayoutManager      首页配置
HomeComponentRegistry  组件注册
HomeLoadPlanner        生成加载计划
HomeLoadScheduler      调度加载任务
HomeComponentLoader    真实加载组件数据
HomeCacheManager       缓存与降级
HomeExposureTracker    曝光与点击埋点

四、真正的加载逻辑

不是组件自己随便请求,而是生成一个任务队列:

[
  banner_001       priority 100 immediate
  menus_001        priority 90 firstScreen
  coupon_001       priority 80 firstScreen
  flash_sale_001   priority 70 afterFirstScreen
  recommend_001    priority 40 preload 1600px
  brand_001        priority 20 preload 800px
]

然后调度器执行:

1. immediate 立刻执行
2. firstScreen 首屏并发执行
3. afterFirstScreen 首屏完成后执行
4. preload 根据滚动距离提前执行
5. lazy 曝光兜底执行
6. idle 在用户停止滑动/空闲时执行

五、核心伪代码

class HomeLoadScheduler {
  HomeLoadScheduler({
    required this.loader,
    required this.maxConcurrent,
  });

  final HomeComponentLoader loader;
  final int maxConcurrent;

  final Set<String> _loaded = {};
  final Set<String> _loading = {};

  Future<void> runInitial(List<ComponentTask> tasks) async {
    final initialTasks = tasks.where((task) {
      return task.mode == LoadMode.immediate ||
          task.mode == LoadMode.firstScreen;
    }).toList();

    initialTasks.sort((a, b) => b.priority.compareTo(a.priority));

    await _runWithLimit(initialTasks);
  }

  void onScroll({
    required double scrollOffset,
    required double viewportHeight,
    required List<ComponentTask> tasks,
  }) {
    final preloadTasks = tasks.where((task) {
      if (_loaded.contains(task.componentId)) return false;
      if (_loading.contains(task.componentId)) return false;

      final distance = task.topOffset - scrollOffset - viewportHeight;
      return distance <= task.preloadDistance;
    }).toList();

    preloadTasks.sort((a, b) => b.priority.compareTo(a.priority));

    _runWithLimit(preloadTasks);
  }

  Future<void> _runWithLimit(List<ComponentTask> tasks) async {
    for (final task in tasks.take(maxConcurrent)) {
      _load(task);
    }
  }

  Future<void> _load(ComponentTask task) async {
    _loading.add(task.componentId);

    try {
      await loader.load(task.componentId);
      _loaded.add(task.componentId);
    } finally {
      _loading.remove(task.componentId);
    }
  }
}

六、组件本身只负责展示

组件不负责决定什么时候请求。

class HomeComponentRenderer extends StatelessWidget {
  const HomeComponentRenderer({
    super.key,
    required this.component,
    required this.state,
  });

  final HomeComponent component;
  final ComponentState state;

  @override
  Widget build(BuildContext context) {
    return switch (state.status) {
      ComponentStatus.loading => component.skeleton,
      ComponentStatus.success => component.view,
      ComponentStatus.failed => component.errorView,
      ComponentStatus.empty => component.emptyView,
    };
  }
}

七、组件注册系统

首页不应该写死:

if (type == 'banner') return BannerView();
if (type == 'menus') return MenusView();

而是:

final registry = HomeComponentRegistry()
  ..register('banner', BannerComponentBuilder())
  ..register('menus', MenusComponentBuilder())
  ..register('goodsList', GoodsListComponentBuilder())
  ..register('coupon', CouponComponentBuilder());

渲染时:

registry.build(component, state);

这样以后加一个组件:

直播组件
秒杀组件
新人专享
排行榜
品牌专区

只需要新增 Builder,然后注册进去。


八、缓存策略

大厂首页一般会有这些策略:

cacheFirst              先缓存,没缓存再请求
networkFirst            先网络,失败用缓存
staleWhileRevalidate    先显示旧缓存,同时后台刷新
cacheOnly               只用缓存
networkOnly             只走网络

首页最常用的是:

staleWhileRevalidate

也就是:

先显示上一次首页
同时请求最新配置和最新数据
请求成功后局部刷新

这样首页不会白屏。


九、降级策略

组件失败不应该导致首页失败。

Banner 失败:显示默认 Banner 或隐藏
Menus 失败:显示缓存菜单
GoodsList 失败:显示重试按钮
Coupon 失败:直接隐藏
FlashSale 失败:隐藏整个模块

配置里可以写:

"fallback": {
  "type": "hide",
  "useCache": true,
  "retry": 2
}

十、最终大厂形态

首页不是一个页面
首页是一个页面容器

组件不是写死 UI
组件是可注册插件

请求不是页面发起
请求是任务调度器发起

曝光不是加载策略
曝光只是触发器之一

缓存不是可选功能
缓存是首页体验的基础设施

失败不是整个页面失败
失败是组件级降级

你的项目建议实现顺序

第一版别直接做太复杂,按这个顺序:

1. HomeLayoutConfig
2. HomeComponentModel
3. HomeComponentRegistry
4. HomeComponentRenderer
5. HomeLoadPlanner
6. HomeComponentLoader
7. HomeLoadScheduler
8. ComponentState 独立管理
9. 缓存 staleWhileRevalidate
10. 曝光/点击埋点

你这个项目的首页架构最终应该是:

Config Driven Home
+ Pluggable Component
+ TaskFlow Scheduler
+ Component Level Cache
+ Component Level Fallback

这就是比较接近大厂的做法。


评论