ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

React Native鸿蒙开发:地址删除边界校验与退出登录二次确认实践

React Native鸿蒙开发:地址删除边界校验与退出登录二次确认实践 做跨平台客户端开发久了你就会发现一个定律越是不起眼的小功能越容易在真机联调时给你来一下狠的。地址管理就是个典型。增删改查在需求文档里就占三行但一旦落到代码里你要面对的是数据边界、交互确认、多端行为差异和一堆真机上的诡异现象。这个项目标题里的三件事——删除地址时增加边界校验、确保至少保留一个地址、退出登录采用二次确认每一件单拎出来都不复杂但它们组合在一起时恰恰是把一个能用的功能打磨成好用的关键。而当你把这些逻辑跑在 React Native 上又要同时适配鸿蒙系统的时候那才是真正考验功力的时候。这篇文章我就把这套需求的完整设计思路、代码实现和鸿蒙适配过程掰开揉碎讲一遍。1. 需求背景与整体设计思路拆解1.1 这个功能到底在解决什么问题先说地址管理的场景。电商、外卖、物流这类应用收货地址是交易闭环的地基。用户下单前必须选择或新增一个地址如果地址列表被删空了整个下单流程就会卡在请先添加收货地址这一步非常影响转化率。更恶心的是有些数据同步策略会在用户重新登录后把本地缓存拉取过来如果本地删空了下一次冷启动时用户看到的是一片空白甚至会出现地址存在却显示不出来的脏数据。所以在删除地址这条链路上做至少保留一个的校验不是为了限制用户操作而是保护数据完整性和后续业务可用性。这个校验是前置的也就是在删除动作真正执行之前先把数量边界卡住。如果用户只有一个地址那就明确告诉他删不了同时给他一个解释文案至少需要保留一个收货地址。用户体验上虽然多了一次阻挡但总比让他删完所有地址、再去下单时发现没地址可选要体面得多。这个思路本质上是集合非空约束在计算机科学里是很常见的状态不变量。比如银行卡列表至少要保留一张、团队成员至少要有一个管理员权限、服务器集群至少保留一台主节点业务规则不同但校验逻辑的骨架是一模一样的。把这一次的实现抽象出来你会发现它适用于任何删除操作不能把集合清空的场景。1.2 为什么至少保留一个地址必须放在删除链路里做前端校验这时候会有同学问这种边界校验不是后端也要做吗前端做到底有没有必要我的答案是三层防线一层都不能省。第一层是前端交互层。用户点击删除按钮的瞬间前端本地就能判断当前 list.length如果只剩一条直接拦截并提示。这一层响应最快体验最好不会让用户产生按了删除却没反应的困惑。第二层是后端接口层。前端拦截只是第一道闸门但接口本身必须也要做同样的校验。原因很简单客户端的请求可能是被 Hack 的、可能是旧版本客户端发出的、也可能是并发请求导致的状态覆盖。后端如果只做删除指定 id这一个动作而不校验删除后列表是否为空那么两条并发请求就可能同时删掉仅剩的两条地址返回 200数据库里一条都不剩。所以服务端必须保证删除后的数量 1。第三层是本地状态兜底层。删除接口调用成功后前端还要把本地 store 里的地址同步掉。但网络请求返回失败怎么办本地状态不能跟着乐观更新走必须回滚。也就是说删除接口失败的时候列表里那条地址要原封不动留在原地甚至要弹一个 Toast 告诉用户网络异常请重试。这层兜底决定了状态一致性做不好就会出现列表里看着删了刷新一下又回来了的灵异事件。顺便说一句前端边界校验的具体阈值要根据真实数据来定。如果你做的是多端同步场景地址列表在本地是有初始值的那判断条件就应该是list.length 1而不是list.length 1。因为有极少数情况本地数据没拉全临时显示 0 条或者 1 条但此时如果把这条唯一的地址删掉后端同步回来发现原来的地址还在但本地以为删了就会出现状态分裂。1.3 退出登录为什么必须二次确认退出登录这个动作我一直觉得是移动端交互设计里最容易被敷衍的一个环节。很多 App 直接在设置页给一个退出登录按钮点了就退连个确认都没有。但退出登录其实是彻底改变应用状态的破坏性操作它涉及三件事清除本地登录凭证、重置用户级数据缓存、跳转回登录页。一旦执行用户想要恢复原状态就得重新输入账号密码有些还涉及多因子验证成本很高。如果用户是无意中触发了这个按钮尤其是按钮位置在设置页底部、手势操作容易误触的场景下没有二次确认的结果就是用户莫名其妙被登出然后带着一肚子火重新登录。而二次确认的意义恰恰在于给用户一个紧急刹车的机会。弹窗里面明确告诉他退出后需要重新登录是否继续用户如果只是误触直接点取消就完事。但这里有个容易走偏的设计倾向就是给所有操作都套二次确认。像点赞、收藏这种高频低风险操作你要是也加弹窗用户会烦死。我的判断标准是看三个维度不可逆性退出的影响范围是否覆盖全 App、状态影响深度是否清数据、清缓存、恢复成本重新进入是否需要额外验证。三者只要有一个显著偏高就必须做二次确认。退出登录在这三个维度上都拉满了所以它天然是二次确认的典型场景。2. 核心交互设计删除校验与二次确认的状态模型2.1 地址数据模型与状态管理设计先说数据层。跨端开发里地址列表的状态一定不能直接散落在各个组件里的 useState 中因为它会被多个页面共享比如地址列表页、下单页、结算页都可能依赖它。项目里我用 Zustand 做全局状态管理它足够轻量在 React Native 和鸿蒙壳工程之间的桥接成本也很低。地址对象本身我定义为AddressItem字段包括 id、联系人姓名、电话、省市区、详细地址、经纬度、是否默认地址。这个模型在 RN、iOS、Android、鸿蒙四个端上都通用不需要任何平台特有字段。interface AddressItem { id: string; name: string; phone: string; province: string; city: string; district: string; detail: string; latitude?: number; longitude?: number; isDefault: boolean; } interface AddressStore { list: AddressItem[]; setList: (list: AddressItem[]) void; removeAddress: (id: string) Promiseboolean; resetAddress: () void; // 退出登录时清空 }状态管理的核心诉求有两个一是保证多个页面读到的地址数据始终一致二是在删除、编辑、新增操作后能同步通知所有订阅方刷新。用 Zustand 的话任何组件调用useAddressStore()拿到的都是同一个响应式状态某个页面删掉一条地址后下单页会立刻感知到列表变化。2.2 删除地址的完整校验流程删除地址的校验流程我把它画成一个决策序列每一步拦截掉一类非法操作。直白地说就是一个先拦数量、再确认意图、然后请求、最后同步的四步走。第一步数量边界校验。进入删除方法后立刻检查当前列表长度。如果不大于 1直接提示用户删除流程终止。这个提示文案我建议用收货地址至少需要保留一个而不是干巴巴的无法删除。人话版本是告诉用户为什么而不是只说不行。第二步确认删除意图。即使通过了数量校验也不能直接删。需要弹出一个确认框文案类似删除后不可恢复确定要删除这条地址吗按钮用取消和删除其中删除按钮要走危险操作的红色样式在 RN 里对应style: destructive。这一步的意义在于给误触一个撤销机会。第三步后端请求。确认后调用删除接口。这里要注意的一点是如果用户在确认弹窗出现之后、点击删除按钮之前他可能已经切换到别的页面组件卸载了、页面 onBlur 了但删除回调仍然会执行。所以删除请求必须绑定到 store 方法而不是绑定到页面组件实例。第四步本地状态同步。接口返回成功后更新 list失败则回滚并提示。回滚不是把删除的元素再塞回去而是压根不要从本地 list 里过滤掉它。最稳妥的写法是接口成功后再过滤失败就保持原样并把错误信息展示给用户。这里还有一个隐蔽的边界条件需要处理如果要删除的地址是默认地址而列表里还有其他地址那删除后默认地址会悬空。我在实际项目里是这样处理的删除默认地址前弹窗文案额外追加一句删除该默认地址后系统将自动为你选择一条地址作为默认后端返回删除成功后前端立即把 list 中剩余的第一条地址设置为 isDefault true。这样做用户体验是最顺滑的否则用户删完默认地址后下次下单时默认地址变成了一个不存在的 id那是真正的灾难。2.3 退出登录二次确认的交互落地退出登录的交互模式在 React Native 里最直接的做法是使用内置的Alert.alert弹出对话框。很多团队在这里会踩一个坑把退出登录的确认按钮和取消按钮的顺序放反。按照移动端惯例取消按钮应该放在左侧危险操作按钮放在右侧而且危险操作按钮的颜色必须区别于主按钮。在 RN 的 Alert 里这个差异就是通过style: destructive来实现的。const confirmLogout () { Alert.alert(退出登录, 退出后需要重新输入账号密码登录确定要继续退出吗, [ { text: 取消, style: cancel }, { text: 退出, style: destructive, onPress: doLogout }, ]); };另外退出登录的 onPress 里不要做太多同步工作。实际的退出逻辑包括清理 token、清理地址缓存、重置全局状态、路由重置跳转登录页这些任务的耗时可能在几十毫秒到几百毫秒之间。所以弹窗确认之后最好有一步 loading 过渡避免用户看到屏幕停顿怀疑是不是 App 卡死了。比较稳妥的做法是确认退出后先展示一个全屏 loading 或者对话框 loading然后在异步任务全部执行完成后再跳转登录页。路由重置用 navigation 的reset方法不然用户按系统的返回键会回到登录前的页面又看到残留的登录态页面这在鸿蒙上尤其容易出问题因为鸿蒙的返回手势默认会保留页面栈。3. React Native 鸿蒙端的关键代码实现3.1 代码工程结构RN 业务工程与鸿蒙壳工程先说明一下当前 RN 跑鸿蒙的主流姿势。以我实际使用过的方案为例业务侧保持 React Native 工程不变用 TypeScript 写业务逻辑鸿蒙侧用 DevEco Studio 建一个壳工程通过社区的react-native-harmony或react-native-oh/react-native包把 RN 实例嵌入到 ArkTS 的容器页面中。大概的工程结构是这样的HarmonyShell/ # DevEco Studio 鸿蒙壳工程 AppScope/ entry/ src/main/ets/ # ArkTS 层入口 pages/ RNPage.ets # 承载 RN 组件的容器 entryability/ EntryAbility.ets # 启动时初始化 RN 环境 oh-package.json5 rn-address-app/ # React Native 业务工程 src/ store/addressStore.ts pages/AddressList.tsx pages/Login.tsx index.js # RN 入口注册业务组件 metro.config.js实际运行时鸿蒙壳工程会加载 RN 打包出来的业务 bundle然后通过运行时桥接把 React 组件渲染到原生页面上。业务层写一套 JS/TS 逻辑通过 Platform API 区分平台差异就能做到 Android、iOS、HarmonyOS 三端共享代码。但需要说明的是鸿蒙的 RN 适配目前对新架构Fabric支持还不完整我实测下来必须关闭新架构使用旧架构才能稳定跑通。3.2 删除地址边界校验核心代码下面这段代码是从项目里抽出来的核心逻辑Zustand 的 store 方法实现了数量校验 意图确认 请求 本地同步四步流程。// store/addressStore.ts import { create } from zustand; import type { AddressItem } from ../types/address; import { request } from ../utils/request; interface AddressStore { list: AddressItem[]; setList: (list: AddressItem[]) void; removeAddressById: (id: string) Promiseboolean; resetAddress: () void; } export const useAddressStore createAddressStore((set, get) ({ list: [], setList: (list) set({ list }), // 删除方法的完整实现注意它不是页面组件方法而是全局 store 方法 removeAddressById: async (id) { const { list } get(); // 第一层数量边界校验 if (list.length 1) { Alert.alert(无法删除, 收货地址至少需要保留一个请先添加其他地址); return false; } // 第二层意图确认这里不直接用 Alert 是因为它本身是 UI 逻辑 // 但是在 store 层直接调用 Alert 也没问题RN 的 Alert 是全局单例 const confirmed await new Promiseboolean((resolve) { Alert.alert(删除地址, 删除后不可恢复确定要删除这条地址吗, [ { text: 取消, style: cancel, onPress: () resolve(false) }, { text: 删除, style: destructive, onPress: () resolve(true), }, ]); }); if (!confirmed) return false; // 记录删除目标的默认地址状态方便后面重设默认地址 const target list.find((item) item.id id); if (!target) return false; // 第三层后端请求。这里的 request 封装里做了超时处理 // 实测鸿蒙端网络超时时间设置比 Android 要短建议单独调大。 try { await request.post(/api/address/delete, { id }); // 第四层本地同步 set((state) { const next state.list.filter((item) item.id ! id); // 如果删除的是默认地址自动设置剩余第一条为默认 if (target.isDefault next.length 0) { next[0] { ...next[0], isDefault: true }; } return { list: next }; }); return true; } catch (err) { // 回滚什么都不改让原列表保持原状只提示错误 Alert.alert(删除失败, 网络异常请稍后重试); return false; } }, resetAddress: () set({ list: [] }), }));这段代码里有个容易忽略的设计点我把删除方法放在 store 层意味着它可以在任何页面被调用而不会因为页面卸载导致回调失效。同时请求失败以后 return false调用方可以根据返回值决定是否更新依赖该地址的其他 UI 状态。比如下单页如果发现删除的是当前选中地址就可以重新 pick 一条默认地址。3.3 退出登录二次确认代码实现退出登录我封装成了一个独立的封装函数不只是调用 store 里一个清空方法。因为退出登录按标题要求是二次确认的交互模式但确认之后的实际退出动作很可能还要清掉登录态 token、清掉本地缓存的地址数据、跳转登录页等等。// utils/logout.ts import Alert from react-native; import { useAddressStore } from ../store/addressStore; import { authStorage } from ../utils/authStorage; import { navigationRef } from ../navigation; export const confirmLogout () { Alert.alert(退出登录, 退出后需要重新输入账号密码登录确定要退出吗, [ { text: 取消, style: cancel }, { text: 退出, style: destructive, onPress: () doLogout() }, ]); }; const doLogout () { // 第一步清登录凭证。这里如果用 MMKV要格外注意异步写入和同步读取的时机 authStorage.clear(); // 第二步清全局用户级数据包括地址列表、购物车、历史记录等 useAddressStore.getState().resetAddress(); // 第三步重置路由栈避免用户按返回键回到已退出页面 navigationRef.reset({ index: 0, routes: [{ name: Login }], }); };这里的navigationRef是 React Navigation 的 NavigationContainer 反射引用它在鸿蒙上的表现我实测比较稳定。需要注意一点鸿蒙端如果用了react-native-oh/react-native的适配部分导航库的原生依赖可能没有完全跟随新架构适配所以在鸿蒙上跑 React Navigation 时要留意依赖版本尽量选混编入口纯 JS 的方案而不是依赖原生 tab 容器的方案。退出登录二次确认的替代交互是自定义一个半屏弹窗组件这个好处是可以控制样式、圆角、遮罩层透明度在一套视觉体系里保持和 Android/iOS 一致。需求里要求的是二次确认的交互模式用 RN 内置 Alert 已经满足了但如果产品要求弹窗里还有同步清除本地缓存之类的说明文字那建议直接上自定义 Dialog 组件。关键点是 Dialog 必须在最顶层渲染并且要处理好返回键逻辑。3.4 鸿蒙端的适配差异点把同一套代码从 Android/iOS 迁移到鸿蒙上跑最烦的不是业务逻辑而是平台差异。我列几个在这次实践中遇到的要点。Alert 的行为差异。在 Android 上Alert.alert默认最多支持三个按钮取消按钮顺序可由开发者控制鸿蒙上基于 ArkUI 实现的 Alert 框在按钮数量、按钮颜色表现上会有细微差异个别版本中destructive按钮不会显示红色。真机测试时如果发现颜色不对不要怀疑代码这是适配层的问题。解决方式是封装一个自定义showConfirm方法在鸿蒙平台直接渲染自定义 Dialog在 Android/iOS 上走原生 Alert。存储方案差异。地址列表和登录态的本地缓存我一开始用的 react-native-mmkv在鸿蒙上需要安装适配版本的包并且初始化代码要显式调用MMKV.initialize()。如果初始化时序不对App 冷启动时读缓存会拿到 null导致地址列表闪烁一下再渲染出来。建议把存储初始化放进 App 入口最早的生命周期里执行并在鸿蒙壳工程的EntryAbility里做并行初始化。平台判断逻辑。能用Platform.OS harmony判断时尽量把它收敛到一个独立的 platform.ts 文件里而不是散落在各处。因为 RN 鸿蒙适配层的 Platform 字段在不同版本里可能返回harmony或ohos统一收敛后需要改动时只改一个文件。4. 实操过程与踩坑记录4.1 RN 接入鸿蒙的工程搭建要点如果你是从零开始接入先确定好鸿蒙侧的 RN 容器工程需要用什么版本。当前社区里react-native-oh/react-native是有配套脚手架的比如react-native-oh/cli可以直接用来初始化鸿蒙壳工程和 RN 工程。初始化完成后RN 业务工程负责打包 JS bundle鸿蒙壳工程通过加载这个 bundle 把 JavaScript 渲染到 ArkUI 上。工程搭建过程中最值得注意的问题有两个。第一是版本锁定。鸿蒙适配层对 RN 版本非常敏感react-native 主版本号一旦不匹配编译时会出现一堆未定义符号、找不到头文件的错误。我的建议是首次搭建时严格跟着插件 README 里示例的版本组合来锁定 package.json不要擅自升级 RN 版本。第二是打包产物路径。鸿蒙壳工程加载 bundle 时路径写错是最常见的启动失败原因。比如 debug 模式和 release 模式的 bundle 路径往往不同Configure 里配置错误的话App 启动后就是一个纯白屏没有任何报错提示排查起来特别费劲。构建完成后用 DevEco Studio 直接构建 hap 包然后通过 hdc鸿蒙的调试工具类似 adb安装到模拟器或真机执行。这一步不需要额外操作只要在 DevEco 里配置好签名证书即可。4.2 启动白屏问题排查React Native 项目在鸿蒙上启动白屏是出现频率最高的运行时问题热词里也经常有人搜。这里的白屏原因和 Android 上常见的原因不太一样我分析下来大体是三类。第一类是 bundle 加载超时或路径错误。鸿蒙壳工程在启动时如果迟迟拿不到业务 bundle整个 RN 容器就一直停在透明状态界面看起来就是白屏。排查方法是看系统日志有没有出现Unable to load script之类的关键字以及确认 bundle 文件有没有被正确打到 hap 包内并从正确的相对路径加载。第二类是容器页和 RN 实例的生命周期衔接问题。鸿蒙侧容器页虽然已经创建但 RN 环境还没有 ready这时 JS 代码也没有被解释执行。出现这种情况我一般会建议先检查容器页有没有主动调用RNInstance.start()这类初始化方法或者是否因为同步阻塞导致初始化代码迟迟没有执行。实践中发现如果 ArkTS 层执行了耗时的同步 I/O 操作RN 环境初始化会被拖到很晚视觉上就是白屏时间格外长。第三类是 JS 层首屏渲染被阻塞。React Native 进入鸿蒙后如果首屏组件树非常重、依赖了过多同步操作或原生模块调用未就绪JS 线程就会卡住。这个和 Android 的低端机白屏现象类似。解决方案是把首屏组件做轻量化处理延迟渲染非关键区块并确保所有原生模块调用都在componentDidMount之后进行。总体来说启动白屏最重要的排查原则是先在 ArkTS 壳工程日志确认 RN 环境初始化状态再进 JS 侧确认 bundle 是否加载成功最后才怀疑业务代码性能问题。按这个顺序排查能省很多时间。4.3 鸿蒙端网络请求错误与调试这次实施过程中最诡异的一个问题是同样的删除地址请求Android 上跑通鸿蒙上却报2300056错误。这个错误码并不是 RN 层报出来的而是鸿蒙底层网络模块抛出的错误常见情况是网络安全配置和 Android 不一样。Android 的网络安全配置允许开发者在network_security_config.xml里灵活配置信任范围而鸿蒙对网络安全更严格尤其是 API 9 及以上版本对纯 HTTP 明文请求和自签名证书有默认限制。如果你的 App 在开发环境使用了 HTTP 或自签名证书Android 正常但鸿蒙请求报错是很正常的。排查手段是抓包。鸿蒙端抓包和 Android 差不多但需要先完成证书安装步骤。用 Charles 这类工具时要确保根证书已经安装到鸿蒙系统的用户信任凭据中并且目标请求走的代理模式正确。另外鸿蒙上 Android 常用的adb reverse或全局代理设置不完全适用推荐直接走hdc端口转发把鸿蒙设备的流量代理到本地开发机上的监听端口。请求配好后如果确认是证书问题可以在鸿蒙的网络安全配置中临时信任开发证书或者把请求切到 HTTPS 并配置正确的 CA。注意这只是开发调试时的临时手段生产环境一定要用合规的证书链。4.4 鸿蒙模拟器与真机调试技巧热词里经常有人搜鸿蒙模拟器怎么用、无线调试怎么开简单说一下实际操作。鸿蒙模拟器资源占用比 Android 模拟器可观得多启动一次可能要一两分钟而且模拟器对 RN 桥接的性能表现和真机有区别。我一般是先跑模拟器做大部分业务功能自测最后在真机焦点验证 Alert 样式、网络请求、导航栈这些和系统能力强相关的功能点因为模拟器在渲染效果和原生能力模拟上不如真机准确。无线调试的开启步骤是在鸿蒙设备上打开开发者选项里的无线调试开关记录设备 IP 和端口然后用hdc tconn ip:port建立连接。连接成功后的用法和 USB 连接基本一致hap 包安装、日志抓取都能在无线模式下完成。真机调试还有一个建议鸿蒙设备的系统日志输出比 Android 更零散建议在 DevEco Studio 里直接用 HiLog 工具过滤进程关键字用你自己的应用包名或者ReactNativeJS能很快定位业务 JS 层的 console 日志。RN 层 console.log 在鸿蒙上的输出默认不一定走系统日志需要在初始化代码里把 console 重定向到鸿蒙日志系统否则你会发现在 DevEco 里根本看不到 JS 侧的日志输出。5. 常见问题排查速查表下面是一份按本次实践整理的排查速查表覆盖了需求实现过程中大家容易卡壳的问题。现象可能原因排查方向与解决方案App 启动白屏无报错bundle 路径配置错误检查鸿蒙壳工程加载 bundle 的相对路径及产物是否打入 hap 包App 启动白屏但系统日志有报错RN 版本与鸿蒙适配层不匹配锁版本组合参照社区插件的示例依赖锁定 package.json删除地址后列表没有更新接口失败但本地没有回滚检查删除请求返回码删除成功后再过滤本地 list删除最后一个地址仍执行了删除前端边界校验缺失在调用删除接口前判断list.length 1拦截并提示删除默认地址后出现空默认值未处理默认地址转移删除后立刻设置剩余第一条为 isDefault退出登录后按返回键回到已退出页面路由栈没有重置使用 navigation reset 替代 navigation pushAlert 按钮顺序或样式和设计稿不一致鸿蒙 Alert 适配层样式差异统一封装自定义 Dialog 组件跨平台保持一致鸿蒙端请求报 2300056网络安全配置/证书校验不通过检查 HTTPS 配置、证书可信链使用抓包工具复现地址列表从本地缓存恢复时闪烁MMKV 初始化时序不正确将 MMKV.initialize 提前到 App 生命周期最前React Native 包在鸿蒙上找不到原生模块使用了新架构不支持的模块切换到旧架构或者使用适配鸿蒙的社区库版本结束前一点个人经验最后聊两句我在实际落地中的体会。边界校验这类功能不要觉得不就是加个 if 判断嘛就掉以轻心。真正的坑永远在边界条件的组合上只有一个地址、默认地址被删、并发删除、网络失败回滚、退出登录后缓存残留每一个单点拿出来都简单但合在一起就会咬人。写完代码之后我强烈建议把地址列表的删除场景整理成一个测试用例清单每个 case 都在三端上跑一遍尤其是鸿蒙端不然你永远不知道自己是在救火还是在挖坑。二次确认的交互也要克制。它只应该出现在真正不可逆、影响范围大的操作上。退出登录算一个删除地址算半个——删除地址本身也破坏性但它能通过至少保留一个和删除确认两层缓冲控制住风险。交互设计不是做得越多越好而是该挡住的时候挡得住、该放行的时候不拖泥带水。你在真机上把这两个逻辑跑顺了这套方案就可以直接复用到其他相似需求上比如清空缓存、解绑设备、注销账号思路是通的。
返回列表