电商类应用一直是移动端开发里最考验工程能力的场景,没有之一。商品列表要扛住长列表滚动、分类导航要处理多级联动、推荐位要兼顾曝光与性能、商家模块又涉及多角色状态管理,再加上购物车、下单、支付这些强交互链路,任何一个环节没处理好,用户流失就是分分钟的事。这次我拿一个 React Native 商城项目做完整复盘,重点聊两件事:一是这套电商业务逻辑在 RN 里到底怎么落地,二是鸿蒙跨端适配过程中我踩过的那些坑。如果你正在做 RN 电商项目,或者正准备把现有 RN 应用往鸿蒙生态迁移,这篇内容应该能帮你省下不少试错时间。
1. 商城业务拆解:为什么电商场景对 RN 架构要求这么高
1.1 从页面数量看工程复杂度
很多人对电商应用的第一印象是"页面多",但真正让架构师头疼的不是页面数量,而是页面之间的状态耦合。一个典型的商城应用至少包含这些核心模块:首页(含轮播、推荐流、限时活动)、分类页(多级联动)、商品详情页(SKU 选择、规格联动、库存判断)、购物车(跨商家结算、优惠计算)、订单确认页(地址、配送、支付方式)、商家模块(店铺主页、商品管理、数据看板)、个人中心(订单列表、售后、收藏)。
这些页面之间共享的状态非常多。比如用户在商品详情页选择了一个 SKU,加入购物车后,购物车需要知道这个 SKU 的库存和价格;下单时订单确认页又需要从购物车读取选中的商品;支付完成后订单列表和个人中心都要刷新。如果状态管理没设计好,就会出现"详情页改了价格,购物车还显示旧价格"这类经典 bug。
我在项目初期就吃过这个亏。当时用最朴素的 props 层层传递,结果商品详情页到购物车之间隔了四层组件,改一个字段要动五个文件。后来换成集中式状态管理,把商品、购物车、用户、订单拆成独立的 store,才把耦合降下来。
1.2 长列表与图片加载:性能的第一道坎
电商首页和分类页本质上都是长列表。RN 的 FlatList 虽然做了虚拟化,但在商品卡片包含图片、价格、标签、按钮的情况下,快速滚动依然会掉帧。我的经验是:商品卡片必须做 memo 优化,图片必须用带缓存的图片组件,价格和标签这类纯展示内容要避免在滚动时重新计算。
具体做法上,我把商品卡片拆成三层:外层负责布局和点击,中层负责图片加载,内层负责价格和标签渲染。外层用 React.memo 包裹,只有商品 id 变化时才重新渲染;图片用带磁盘缓存的组件,避免每次滚动都重新请求;价格格式化这种计算用 useMemo 缓存。实测下来,同样一千条商品数据,优化前滚动到三百条左右开始明显卡顿,优化后能稳定跑到八百条以上。
1.3 推荐系统的前端职责边界
推荐系统在电商里通常由后端算法驱动,但前端并不是简单地把数据渲染出来就完事。前端需要处理曝光埋点、点击埋点、推荐位降级、A/B 实验分流这些逻辑。曝光埋点尤其容易被忽略——用户滑过但没点击的商品,到底算不算曝光?行业里一般以商品卡片进入视口并停留超过一定时长才算有效曝光。
我在项目里用了一个简单的方案:给每个推荐卡片绑定 onViewableItemsChanged 回调,当卡片可见比例超过 50% 且持续 500 毫秒时,才上报曝光事件。这个阈值不是拍脑袋定的,而是根据用户滑动速度反推的——太快滑过的卡片用户根本没看清,上报曝光反而污染数据。
2. React Native 侧的核心实现:状态、路由与交互链路
2.1 状态管理选型:为什么最终没选 Redux
RN 生态里状态管理方案很多,Redux、MobX、Zustand、Jotai 各有拥趸。我一开始用的是 Redux Toolkit,写起来确实规范,但电商场景有个特点:状态更新极其频繁。购物车加减数量、SKU 切换、优惠券选择,这些操作每秒可能触发好几次。Redux 的 dispatch-action-reducer 链路在这种高频场景下,样板代码多不说,性能也不理想。
后来换成了 Zustand。它的核心优势是去掉了 action 和 reducer 的中间层,直接通过 set 函数更新状态,而且支持选择器订阅——组件只订阅自己关心的那部分状态,其他状态变化不会触发重渲染。这对购物车这种"一个商品数量变化不应该让整个购物车列表重渲染"的场景特别友好。
import { create } from 'zustand'; const useCartStore = create((set, get) => ({ items: [], addItem: (product, sku) => { const items = get().items; const existing = items.find(i => i.skuId === sku.id); if (existing) { set({ items: items.map(i => i.skuId === sku.id ? { ...i, count: i.count + 1 } : i ) }); } else { set({ items: [...items, { ...product, skuId: sku.id, count: 1 }] }); } }, removeItem: (skuId) => { set({ items: get().items.filter(i => i.skuId !== skuId) }); } }));这个 store 设计的关键点在于:items 数组里每个元素都带 skuId,组件订阅时可以用选择器只取自己需要的商品,避免全量重渲染。
2.2 路由设计:分类导航与详情页的参数传递
RN 的路由方案我选的是 React Navigation,主要是因为它对嵌套导航和参数传递的支持比较成熟。电商场景里路由设计有两个难点:一是分类页的多级联动,二是商品详情页的参数传递。
分类页的多级联动,我采用的是"左侧一级分类 + 右侧二级分类 + 顶部三级筛选"的结构。左侧点击一级分类时,右侧列表要滚动到对应位置;右侧滚动时,左侧高亮要跟着变。这个联动逻辑如果用 state 驱动,滚动时会非常卡。我的做法是用 ref 直接操作滚动位置,只在滚动停止时才更新左侧高亮状态。
商品详情页的参数传递,我踩过一个坑:一开始把整个商品对象通过路由参数传过去,结果发现 React Navigation 会对参数做序列化,大对象传递时性能很差,而且页面返回再进入时参数可能丢失。后来改成只传商品 id,详情页自己根据 id 去 store 或接口取数据。这样既避免了序列化开销,也保证了数据一致性。
2.3 购物车与下单链路的交互细节
购物车到下单的链路,是电商应用里交互最密集的部分。我总结下来有几个关键点:
第一,购物车里的商品要支持跨商家选择。用户可能同时选了 A 店和 B 店的商品,结算时要按商家拆单。这个逻辑我在 store 里用了一个 selectedMap 来记录每个 SKU 的选中状态,结算时按商家分组。
第二,优惠计算要放在前端做预演。虽然后端才是最终计算方,但用户在选择优惠券时,前端需要实时显示优惠后的价格。我的做法是前端维护一套简化的优惠规则,和后端规则保持一致,用户切换优惠券时前端先算一遍给用户看,下单时以后端计算结果为准。
第三,库存判断要前置。用户点击"立即购买"时,前端要先检查本地库存状态,如果库存不足直接提示,避免走到下单页才报错。这个检查虽然不能完全替代后端校验,但能减少无效请求。
3. 鸿蒙跨端适配:从 RN 到鸿蒙的迁移路径
3.1 适配方案选型:为什么不是简单套壳
把 RN 应用适配到鸿蒙,市面上主要有两条路:一是用鸿蒙的 WebView 容器直接加载 RN 的 Web 版本,二是用 RN 的鸿蒙适配层做原生渲染。第一条路看起来省事,但电商应用对性能和原生交互要求高,WebView 方案在长列表滚动、图片加载、手势响应上都有明显短板,用户体验会打折扣。
我最终选的是第二条路,基于 RN 的鸿蒙适配层做迁移。这个方案的核心思路是:RN 的 JS 层代码基本不动,把原生模块和组件替换成鸿蒙对应的实现。比如 RN 的 ScrollView 在鸿蒙上要映射到鸿蒙的滚动容器,Image 组件要映射到鸿蒙的图片组件。
这个方案的好处是业务代码复用率高,我统计下来大概 85% 的 JS 代码可以直接复用,需要改的主要是原生模块调用和平台特定 API。坏处是适配层本身还在完善中,有些 RN 组件在鸿蒙上的表现和 iOS/Android 不完全一致,需要逐个验证。
3.2 环境搭建与工程配置
鸿蒙适配的环境搭建比普通 RN 项目多几个步骤。首先需要安装鸿蒙的开发工具链,然后配置 RN 的鸿蒙适配插件。工程配置上,最关键的是在配置文件中声明鸿蒙平台的相关参数,包括应用包名、版本号、权限等。
{ "app": { "bundleName": "com.example.shop", "versionCode": 1000000, "versionName": "1.0.0", "icon": "$media:app_icon", "label": "$string:app_name" }, "module": { "name": "entry", "type": "entry", "deviceTypes": ["phone", "tablet"], "abilities": [ { "name": "EntryAbility", "srcEntry": "./ets/entryability/EntryAbility.ets", "launchType": "singleton" } ] } }这里有个容易忽略的点:鸿蒙的权限声明和 Android 不一样,需要在配置文件中显式声明网络、存储等权限,否则运行时会被拦截。我第一次跑的时候就是因为没声明网络权限,首页数据一直加载不出来,排查了半天才发现是权限问题。
3.3 原生模块替换的实操细节
RN 项目里通常会用到一些原生模块,比如图片缓存、本地存储、设备信息、支付 SDK 等。这些模块在鸿蒙上需要替换成对应的实现。我逐个说一下替换过程中的经验。
图片缓存模块,RN 社区常用的方案在鸿蒙上没有直接对应,我用鸿蒙的图片加载能力重新实现了一个。核心逻辑是:先查内存缓存,再查磁盘缓存,都没有才走网络请求,请求完成后写入两级缓存。鸿蒙的图片组件本身支持内存缓存,但磁盘缓存需要自己实现。
本地存储模块,RN 的 AsyncStorage 在鸿蒙上可以用鸿蒙的偏好数据库替代。需要注意的是,鸿蒙的偏好数据库是异步 API,和 AsyncStorage 的用法基本一致,但键值类型支持上有差异,存储复杂对象时需要先序列化。
设备信息模块,主要是获取屏幕尺寸、像素密度、系统版本这些。鸿蒙提供了对应的 API,但返回的字段名和 RN 的不一样,需要做一层映射。我在项目里封装了一个统一的设备信息模块,对外暴露和 RN 一致的接口,这样业务代码就不用改。
支付模块是最麻烦的。电商应用离不开支付,而支付 SDK 通常和平台强绑定。鸿蒙上的支付能力需要通过鸿蒙的支付接口实现,和 RN 原有的支付模块接口差异较大。我的做法是把支付逻辑抽象成一个统一的接口,iOS、Android、鸿蒙各自实现,业务层只调用统一接口。
4. 跨端适配中的性能与兼容性坑
4.1 长列表在鸿蒙上的滚动性能
鸿蒙上的长列表滚动,我一开始直接用了 RN 的 FlatList,结果发现滚动到一定数量后明显掉帧。排查后发现两个原因:一是鸿蒙的渲染管线和 RN 默认的渲染策略不完全匹配,二是图片组件的缓存策略在鸿蒙上需要重新配置。
解决办法是:把 FlatList 的 windowSize 调小,减少同时渲染的 item 数量;给图片组件配置鸿蒙原生的缓存策略;把商品卡片的阴影、圆角这些视觉效果用鸿蒙支持的属性实现,避免用 RN 的通用属性导致额外的渲染开销。调整后,同样一千条商品数据,鸿蒙上的滚动流畅度和 Android 基本持平。
4.2 手势冲突与交互差异
电商应用里手势操作很多:轮播图滑动、商品卡片左滑删除、下拉刷新、上拉加载。这些手势在 iOS 和 Android 上表现基本一致,但在鸿蒙上有一些差异。
最典型的是轮播图。RN 的轮播组件在鸿蒙上滑动时,偶尔会出现手势被父容器拦截的情况,导致轮播图滑不动。排查后发现是鸿蒙的手势识别机制和 RN 的不一样,父容器的滚动手势优先级更高。解决办法是给轮播图容器显式声明手势优先级,或者在轮播图区域禁用父容器的滚动。
另一个是下拉刷新。鸿蒙的下拉刷新组件和 RN 的 RefreshControl 在触发阈值和回弹效果上有差异。我的做法是统一用鸿蒙的下拉刷新组件,然后调整触发阈值,让用户体验和 iOS/Android 尽量一致。
4.3 样式兼容:那些看起来一样但实际不一样的地方
样式兼容是跨端适配里最琐碎但也最容易出问题的部分。我列几个实际遇到的差异:
| 样式属性 | iOS/Android 表现 | 鸿蒙表现 | 处理方式 |
|---|---|---|---|
| 阴影 | shadowColor/shadowOffset 等 | 用 elevation 或鸿蒙阴影属性 | 封装统一阴影组件 |
| 圆角 | borderRadius 支持百分比 | 部分场景不支持百分比 | 改用具体数值 |
| 字体 | 系统默认字体 | 鸿蒙默认字体不同 | 显式指定字体或接受差异 |
| 渐变 | 需要第三方库 | 鸿蒙原生支持 | 用鸿蒙原生渐变 |
| 定位 | position: absolute 正常 | 部分嵌套场景有偏移 | 减少嵌套定位 |
这些差异单独看都不大,但累积起来会让鸿蒙上的视觉效果和 iOS/Android 有明显出入。我的建议是:在项目初期就建立一套跨端样式规范,把常用的阴影、圆角、字体、间距都封装成统一组件,各平台各自实现,业务层只引用统一组件。
5. 电商核心链路的跨端验证清单
5.1 商品浏览链路的验证要点
商品浏览是用户进入应用后的第一条链路,涉及首页、分类页、搜索页、商品详情页。跨端验证时,我重点关注这几个点:
首页轮播图在鸿蒙上是否自动播放、是否支持手动滑动、滑动后是否自动恢复播放。分类页的多级联动在鸿蒙上滚动是否流畅、左侧高亮是否跟手。搜索页的输入框在鸿蒙上是否正常唤起键盘、搜索建议是否实时更新。商品详情页的图片预览在鸿蒙上是否支持手势缩放、SKU 选择弹窗是否正常弹出和关闭。
这些点看起来基础,但跨端时最容易出问题。我在鸿蒙上就遇到过商品详情页图片预览无法缩放的问题,排查后发现是手势组件在鸿蒙上的实现差异,换成鸿蒙原生图片预览组件后解决。
5.2 交易链路的验证要点
交易链路包括购物车、订单确认、支付、订单列表。这条链路对数据一致性要求最高,跨端验证时要特别仔细。
购物车的加减数量在鸿蒙上是否实时更新、选中状态是否跨页面保持。订单确认页的地址选择、配送方式、优惠券在鸿蒙上是否正常交互。支付调起在鸿蒙上是否正常、支付结果回调是否准确。订单列表在鸿蒙上是否正常分页加载、状态筛选是否生效。
我在这条链路上踩过一个坑:鸿蒙上的支付回调偶尔会延迟,导致用户支付成功后订单状态没有及时更新。解决办法是在支付回调里加一个轮询机制,支付成功后主动查询订单状态,确保状态同步。
5.3 商家模块的验证要点
商家模块相对独立,但涉及多角色权限和数据看板。跨端验证时重点关注:商家登录态在鸿蒙上是否正常保持、商品管理列表是否正常增删改查、数据看板的图表在鸿蒙上是否正常渲染。
数据看板的图表在鸿蒙上是个难点,因为 RN 的图表库大多依赖原生绘图能力,鸿蒙上的支持程度不一。我的做法是用鸿蒙原生的绘图能力重新实现了一套轻量图表组件,只保留电商场景常用的折线图、柱状图、饼图,避免引入过重的图表库。
6. 实操心得:那些文档里不会写的经验
6.1 适配层版本选择要保守
RN 的鸿蒙适配层更新比较快,新版本可能修复了一些问题,但也可能引入新的兼容性问题。我的经验是:不要盲目追新,选一个稳定版本,把核心链路跑通后再考虑升级。我在项目中期升级过一次适配层,结果导致购物车页面的滚动出现异常,回滚后才恢复。后来我定了个规矩:适配层版本升级必须经过完整的回归测试,不能在生产环境直接升。
6.2 建立跨端差异清单
跨端适配最怕的是"以为一样,实际不一样"。我在项目里维护了一份跨端差异清单,记录每个平台在组件、API、样式上的差异。这份清单是活的,每发现一个新差异就加进去,每次发版前对照清单做回归。这份清单后来成了团队新人的必读文档,省了很多重复踩坑的时间。
6.3 性能监控要分平台
电商应用的性能监控不能只看整体数据,要分平台看。iOS、Android、鸿蒙三个平台的性能表现可能差异很大。我在项目里给每个平台单独配置了性能监控,重点关注首屏加载时间、列表滚动帧率、接口响应时间、支付成功率这几个指标。鸿蒙平台初期在列表滚动帧率上明显低于 Android,经过针对性优化后才追平。
6.4 测试设备要覆盖到位
鸿蒙设备型号比较多,不同型号在屏幕尺寸、性能、系统版本上都有差异。我的经验是:至少覆盖高中低三档设备,低端设备重点测长列表滚动和图片加载,高端设备重点测手势交互和动画效果。测试时不要只用模拟器,真机测试才能发现真实问题。
6.5 业务代码与平台代码要分离
跨端项目最容易犯的错误是把平台特定代码散落在业务代码里。我的做法是:业务代码只依赖统一接口,平台特定实现放在独立的适配层。这样新增一个平台时,只需要实现适配层,业务代码基本不用动。这个原则在项目后期新增鸿蒙平台时体现出了价值——适配层写完后,业务代码只改了不到 5%。
7. 从这次项目里沉淀下来的可复用方案
7.1 统一状态管理方案
Zustand 在电商场景下的表现让我比较满意,我把它沉淀成了一套通用的状态管理方案:商品、购物车、用户、订单四个核心 store,每个 store 都支持选择器订阅和持久化。这套方案后来在其他项目里复用,基本没怎么改。
7.2 跨端组件库
项目里积累了一批跨端组件:统一阴影组件、统一圆角组件、统一图片组件、统一下拉刷新组件、统一轮播组件。这些组件对外暴露一致的接口,各平台各自实现。这套组件库是跨端项目里最值得投入的部分,前期花时间封装,后期省大量适配时间。
7.3 跨端验证清单
前面提到的跨端验证清单,我把它整理成了一份可执行的检查表,覆盖商品浏览、交易链路、商家模块三大场景。每次发版前对照检查表跑一遍,能覆盖 90% 以上的跨端问题。这份清单还在持续补充,每发现一个新问题就加一条。
7.4 性能优化 checklist
性能优化方面,我总结了一份 checklist:长列表是否做了 memo 优化、图片是否用了缓存、价格计算是否用了 useMemo、滚动事件是否做了节流、接口是否做了缓存、首屏是否做了懒加载。这份 checklist 在每次性能优化时对照使用,能快速定位优化点。
这套方案在鸿蒙上的表现,整体达到了可用水平,核心链路的用户体验和 Android 基本持平。当然也有遗憾的地方,比如鸿蒙上的动画效果和 iOS 相比还有差距,部分第三方库在鸿蒙上的支持还不完善。但考虑到鸿蒙生态还在快速发展,这些问题应该会逐步改善。如果你也在做类似的跨端项目,我的建议是:尽早建立跨端差异清单和统一组件库,这两件事的投入产出比最高。