目前仅讲设计过程,源码待补充...
引言
在移动互联网时代,为了覆盖更广泛的用户群体并降低开发成本,跨端开发已成为主流。然而,如何在不同技术栈(如小程序、H5、React Native)之间保证 UI 的一致性、交互的统一性以及卓越的性能,是每个开发团队都必须面对的核心挑战。一套设计精良的跨端组件架构,是解决这一系列问题的关键。本文将以设计一个相册组件为例,回顾作者在开发跨端组件过程中的所思所想。
一、明确需求和目标
1. "多端"具体指的是哪些平台?
微信小程序、H5、React Native
2. 组件的核心功能有哪些?
- 不仅支持图片展示,还需要支持视频播放
- 支持左右滑动,需要考虑手势监听
- 有tab作为指示器,需要和相册主体联动
3. 对性能和加载速度的预期如何?
- 用于页面的首屏组件渲染,需要做到首屏秒出
- 滑动不卡顿,不黑帧
- 需要贴近原生性能
- 支持懒加载,加载失败占位
4. 是否有特定的设计或交互要求?
- 相册帧有高度变化,并不是固定的
- 视频特定条件下(如wifi)会自动播放
- 滑动到底部支持继续滑动跳转至二级页面
5. 是否有团队的技术栈偏好或限制?
偏向使用React和Taro,比较熟悉,上手快
二. 架构设计
1. 技术选型
| 维度 | 推荐技术/框架 | 决策依据 |
|---|---|---|
| 多端框架 | Taro + React | 满足多端(小程序、H5、RN)和技术栈偏好。Taro 的条件编译是实现三端原生组件隔离的关键。 |
| 轮播组件 (Swiper) | 三端原生组件 + Taro 条件编译 | 贴近原生性能。小程序使用 <Swiper>,本质是小程序的swiper组件,H5 使用 <View> + CSS translate3d 灵活度高,RN 使用 <ViewPager>,原生性能好。 |
| 媒体控制 | 统一封装 MediaView | 抽象图片/视频的加载、播放和暂停逻辑,实现离屏暂停和播放互斥。 |
| 性能优化 | LQIP/Base64 + 精确预加载 | 解决“首屏秒出”和“滑动不黑帧”。首帧采用低质量占位图。预加载 的高清资源。 |
| 手势与交互 | 原生组件事件监听 + 事件冒泡控制 | 监听原生组件的滑动事件,在边界(最后一张)控制事件冒泡,触发“左滑进入看详情”的自定义事件。 |
2. 挑战与设计策略
| 核心挑战 | 设计策略(解决方案) | 关键实现要点 |
|---|---|---|
| 首屏秒出 | 渲染/加载分离 + 预加载 | OptimizedImage 仅在 shouldLoad 为 true 时渲染真实 <img> 或 <Image> 标签。首图数据中包含 Base64/LQIP,确保第一时间渲染占位图。 |
| 相册帧高度变化 | 动态样式计算与组件封装 | Carousel 监听当前 activeIndex。子组件 CarouselItem 根据索引应用不同的 CSS/Style 高度。由于高度不固定,需要确保轮播组件能正确适应,避免抖动。 |
| 视频特定条件自动播放 | 网络状态检测 + 生命周期控制 | 组件初始化时,使用 Taro API getNetworkType 判断是否为 Wi-Fi。如果满足条件且当前帧激活,则调用 <Video> 组件的 play() 方法。 |
| 滑动到底部跳转二级页 | 边界手势事件监听 | 监听轮播组件的滑动结束事件。如果判断当前索引为列表末尾,且滑动方向为继续向前(左滑/上滑) ,则触发一个 onReachedEndAndSwiped 自定义事件,由外部容器处理路由跳转。 |
| Tab与主体联动 | 单向数据流 (Props) | MultiAlbum 维护 activeTab 状态,传递给 TabSelector 和 Carousel。TabSelector 触发改变后,更新 MultiGallery 状态,驱动 Carousel 刷新数据(Key 变化)。 |
3. 组件架构设计
3.1 架构设计图
3.2 核心设计理念
3.2.1 容器/展示模式(Container/Presenter Pattern)
将组件的数据管理和逻辑处理与UI渲染和展示分离,容器MultiGallery负责状态管理、数据获取和复杂逻辑,而展示组件TabSelector 和Carousel只负责接受Props并渲染UI。可以使得展示组件,如TabSelector有更高地复用性,因为它不依赖任何内部状态或业务逻辑。
3.2.2 策略模式(Strategy Pattern)
封装算法,使得算法的变化可以独立于使用它的组件。Carousel是UI组件,它感知抽象的控制属性,如shouldLoad、isActive,渲染策略被封装在OptimizedImage和OptimizedVideo中,Carousel不需要知道图片是如何实现懒加载的,也不需要知道视频是如何实现互斥播放的,它只需要依赖shouldLoad这一抽象接口。我们可以随时修改或者替换OptimizedImage的懒加载算法,而无需修改核心的Carousel逻辑。
3.3.3 适配器模式 (Adapter Pattern)
Carousel 期望使用一个统一的接口,如 <GallerySwiper activeIndex={N} onSlideChange={fn} />,适配器(三个平台文件)将这个统一接口适配到各平台的原生组件上,实现了跨端兼容性。Carousel 只需要编写一次代码,就能与不同平台的原生组件无缝协作。
4. 目录结构
MultiGallery/
├── package.json
├── tsconfig.json
├── .babelrc
├── README.md
└── src/
├── components/
│ ├── MultiGallery.tsx # 1. 应用层:容器主入口
│ ├── TabSelector.tsx # 1. 应用层:Tab组件
│ ├── Carousel.tsx # 1. 应用层:轮播图组件
│ └── core/ # 2. 核心逻辑层
│ ├── GallerySwiper/ # 3. 跨端适配层
│ │ ├── index.tsx # 适配入口
│ │ ├── index.weapp.tsx # 小程序实现
│ │ ├── index.h5.tsx # H5实现
│ │ ├── index.rn.tsx # React Native实现
│ ├── OptimizedImage/ # 4. 媒体抽象层:图片优化
│ │ ├── OptimizedImage.tsx # 图片组件(LQIP/Base64等)
│ │ ├── index.scss
│ │ └── index.ts
│ └── OptimizedVideo/ # 4. 媒体抽象层:视频优化
│ ├── OptimizedVideo.tsx # 视频组件(Wi-Fi播放等)
│ ├── index.scss
│ └── index.ts
├── utils/
│ └── index.ts # 工具函数
├── hooks/
│ ├── useCarouselState.ts # 轮播状态管理hook
├── constants/
│ ├── index.ts # 常量
└── index.tsx # 组件导出入口
三、核心内容 -- 多端GallerySwiper的实现
待补充代码
四、一些难点与挑战
1. 复杂的交互
- 手势监听:高度动态变化
- 手势监听:滑动到底部加载下一页
- 视频自动播放/暂停、互斥
2. 多端兼容及性能优化
- 懒加载
- 图片组件优化:加载性能
- 视频组件优化:播放性能