十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

React Native鸿蒙适配实践:医院就诊准备应用开发全解析

React Native鸿蒙适配实践:医院就诊准备应用开发全解析 1. 为什么医院需要一款“就诊准备”应用先想清楚再动手去年我家里长辈生病陪跑了一次三甲医院的门诊流程整个过程让我印象很深。早上七点到医院窗口排队挂号见到医生之后医生问“哪儿不舒服、多久了、以前有没有别的病、最近吃什么药”长辈说不清楚我在旁边不断补充但信息还是零散。医生一边开检查单一边在系统里敲字前后大概用了十几分钟后面排队的病人已经在叹气了。那次体验之后我就一直在想如果患者在候诊之前就能把症状、病史、用药情况这些信息整理好预先提交到系统里医生接诊时直接在屏幕上浏览效率至少能提升一半患者也不用来回补述。这个想法就是我这次做“医院就诊准备应用”的起点。简单来说这个应用的核心价值就三件事让患者用症状描述把“哪里不舒服、怎么不舒服、持续多久”讲清楚用病史填写把“既往疾病、过敏史、用药记录”留下来用医生选择把“想找哪个科室、哪位医生、什么时间方便”提前定住。整个流程做下来相当于把原本要在诊室白热化进行的“首次问诊”往前搬到了候诊前患者在家或者在候诊厅就能完成。从技术实现的角度这个应用选择的是React Native 鸿蒙HarmonyOS NEXT跨平台方案。做这个选择不是拍脑袋后面我会详细展开技术选型的理由。这篇博文会把整个项目的来龙去脉、架构设计、三个核心模块的实现细节、鸿蒙适配过程中踩过的坑以及最终落地后的真实效果都写清楚。适合正在考虑“鸿蒙 跨平台”技术路线的开发者也适合医疗行业里想做类似预问诊产品的产品经理和团队。2. React Native 上鸿蒙的技术选型逻辑为什么没有直接用纯血 ArkTS2.1 选型之前的现实约束在定技术方案之前我其实做了好几轮对比。第一个方案是直接用纯 ArkTS ArkUI 写一个鸿蒙原生应用这是官方最推荐的路线性能最好、体验最稳。但问题是我们团队不是只有鸿蒙一个目标平台团队长期维护的既有 App 还有 Android 和 iOS 版本如果单独为鸿蒙重写一套原生代码意味着未来每一个业务功能都要在四套代码库Android、iOS、鸿蒙、后端之外的客户端里同步维护人力根本扛不住。第二个方案是用 uni-app 这类国内生态做得比较成熟的跨平台框架。uni-app 对多端的支持确实很方便一套代码能发布到 App、H5、微信小程序等等但在原生能力调用和复杂交互场景下还是绕不开一堆条件编译。而且 uni-app 在鸿蒙端的适配其实也是通过桥接机制实现的并非天然第一等公民。第三个方案就是 React Native。React Native 本身就是一个成熟的跨平台框架社区生态大组件库丰富而且它有一个特点——通过桥接层把 JavaScript 逻辑映射到原生组件上。正因为有这个特性它在鸿蒙上的适配是“有天然路径”的把鸿蒙的原生组件实现得像 Android 组件一样暴露给 React Native 层这样上层 JS 代码几乎不用大改。2.2 鸿蒙适配 react-native 的实际成熟度网上关于“react native 支持鸿蒙吗”这个问题其实答案是一路在变化的。以前 HarmonyOS 2/3 时代RN 如果要跑上去需要做大量自定义适配社区里有各种补丁方案但稳定性很差。到了 HarmonyOS NEXT 时代随着 React Native 开源社区发布了 React Native for OpenHarmony 的适配仓库情况好了很多。目前官方提供的适配方案里RN 的 new architecture 也已逐步支持鸿蒙尽管还没有做到和 Android/iOS 完全同版本的同步更新但核心 API 和基础组件已经能正常使用了。从我个人的实践来看现阶段做“RN 鸿蒙化”项目比较务实的姿态是把它当成一次“主平台 次平台”的适配而非“完全一等公民的开发”。什么意思呢就是说假如你有一个已经跑得不错的 RN 应用把它往鸿蒙上搬需要关注的是原生模块的桥接、权限配置、打包工具这几个关键环节而如果你是从零开始写一个新应用打算直接以“RN 鸿蒙”为主那要评估好哪些功能可以用 RN 生态的现成组件哪些必须靠鸿蒙侧自己开发原生能力去补。2.3 这个项目里的折中落法就诊准备应用是一个典型的“表单密集 信息展示 轻交互”场景并没有特别复杂的动画和极致性能要求。这类应用非常适合用 RN 来写因为页面结构清晰、状态管理集中、表单验证逻辑可以复用。在鸿蒙化这一步我采用的是混合工程结构业务代码页面、组件、状态管理全部用 React Native 编写涉及系统能力的模块相机、文件、推送等走鸿蒙原生桥接最终产物通过 DevEco Studio 打包成 hap交付到鸿蒙设备上运行。这样的好处是不必等到 RN 把鸿蒙支持做到百分之百完美再动手核心业务逻辑已经跑起来了遇到鸿蒙特有机制需求时再针对性地补原生代码风险可控迭代速度也快。3. 三个核心功能的拆解与实现症状描述、病史填写、医生选择应用的核心功能围绕“预就诊流程”展开用户在到院前完成三个动作描述症状、填写病史、选择医生。很多人会觉得这三个功能就是几个表单页面没什么难度。但在真实开发过程中每一个功能背后都有不少设计考量。下面把这三个模块逐一拆开讲。3.1 症状描述从自由文本到结构化数据的转换症状描述是患者使用频率最高的入口但也是产品设计上最容易失控的地方。如果只是放一个大的输入框让用户自由输入用户要么不知道写什么要么写一大堆无关信息后端很难处理。如果把选项挖得太死让用户只能从固定下拉列表里选又容易让不熟悉电子产品的患者觉得冷冰冰。我的做法是“结构化引导 自然语言补充”相结合的思路。页面入口是一个自由文本框让用户先用自己喜欢的方式描述“右腹部阵发性疼痛大约三天饭后加重没有发热”。在文本下方提供了几个常用症状的快捷标签比如“发烧”“咳嗽”“头痛”“腹泻”“恶心”等用户点一个标签对应的关键词可以快速插入文本框。这样既保留了个性化描述的空间又方便系统做关键词抽取。关键词抽取是在前端完成的。我写了一个轻量级的规则引擎不依赖后端网络请求用正则匹配 同义词映射来识别症状实体。举个例子患者输入“肚子痛”系统会通过同义词库映射为“腹痛”并把严重程度、病程时间、诱因这类属性抽取出来。这些抽取结果会结构化成 JSON 存储同时保留用户的原始文本方便医生查看完整描述。const symptomPatterns [ { key: fever, keywords: [发热, 发烧, 体温高, 低烧, 高烧], synonyms: { 体温高: 发热, 低烧: 低热 }, extractor: (text) { const match text.match(/(\d(\.\d)?)\s*度/); return match ? { temperature: match[1] } : {}; } }, // ...更多症状规则 ];这段代码的结构很简单但规则库需要根据医院的临床反馈持续迭代。后来我们发现年轻用户倾向于输入英文缩写老年人习惯口语化表达规则库里的同义词就必须不断扩充。如果你也要做类似功能建议规则库的配比权重放在“常见主诉”的覆盖上而不是一上来就追求特别全面的医学词库配合后台可动态下发的配置表会更实用。3.2 病史填写按科室动态切换表单的工程实践病史模块可以说是整个项目里“长得最像普通表单”但实际最复杂的一块。不同科室需要采集的病史信息差别很大心内科关心“是否有高血压、冠心病、支架植入史”消化内科关心“大便性状、消化道出血史、是否做过胃镜”呼吸科关心“吸烟史、肺结核接触史、气喘史”。如果做一个全量的大表单患者填写负担很重医生也看不到重点如果每个科室做一套定制表单开发量又上去了。权衡之后我采用按科室动态渲染表单的方案表单项的 schema 放在后端配置前端只负责解释渲染。{ departmentCode: cardiology, fields: [ { type: radio, name: hypertension, label: 是否有高血压, options: [有, 无, 不确定], required: true }, { type: group, name: familyHistory, label: 家族史, fields: [ { type: checkbox, label: 早发冠心病父亲/兄弟55岁母亲/姐妹65岁, name: earlyCAD } ] }, { type: number, name: systolicBP, label: 最近一次血压收缩压mmHg, condition: { field: hypertension, value: 有 } } ] }这种 schema 驱动的做法一开始会显得比直接写死表单要繁琐但好处是后续科室扩展表单字段时不用再发一次 App 版本后台改配置即可。而且这种渲染逻辑本身非常适合用 React 组件化方式实现不同的字段类型对应不同的组件单选对应 RadioGroup复选对应 CheckboxGroup动态显示隐藏对应 conditional render。病史数据还有个特殊性——它是“高敏感信息”。我在工程上做了几个硬性要求病史数据本地优先存储先写入 SQLite 数据库再异步同步到服务器同步过程使用 HTTPS 加密传输用户可以在隐私设置里选择“不同步敏感字段”此时只有医生端通过安全通道才能读取本机数据。做医疗类的应用隐私安全不能只是嘴上说说合规细节上要舍得投入时间。3.3 医生选择把专家资源结构化展示给患者医生选择模块本质上是一个“精准推荐 排班 号源”的复合业务。在这个项目里我做的是轻量版本先选科室再展示该科室下有出诊计划的医生列表医生卡片上包含职称、擅长方向、简介和未来七天的号源余量。医生数据的来源走的是服务端接口客户端做缓存。医生列表展示时我用了一个比较耗性能的能力——搜索 联动的本地索引。用户在科室列表页滑动选择科室右边的医生列表会联动刷新同时在顶部提供搜索框支持按医生姓名或擅长方向关键字过滤。这个交互看起来简单但在 React Native 里做长列表渲染时如果还按常规的 ScrollView map 去写数据量一大页面就卡。这里我用了 FlatList getItemLayout 动态优先级队列来优化。实际效果是这样的400 多名医生、未来七天排班数据加载到本地后列表滑动流畅度能够保持稳定。这个性能优化经验后面专门用一节来讲。针对号源余量的展示我做了实时状态与轮询缓存的结合。进入医生选择页时先拉一次实时号源页面停留期间每 30 秒轮询一次刷新。这个频率在医院的业务场景下比较合适——太频繁了会给服务器压力太慢了会有患者看到有号但点进去挂不上的问题。预约确认流程我做了两步第一步是预览确认把患者填好的症状摘要、病史要点和选择的医生信息汇总到一张确认卡上让患者核对第二步是提交预约提交成功之后生成一个预约码同时支持一键跳转到“就诊准备单”页面。这个就诊准备单就是本次应用最重要的产出物患者到院后出示给护士站或接诊医生扫码即可。4. 鸿蒙适配实录从白屏定位到 hap 打包链路完整复盘4.1 启动白屏不只是“等一等就好”那么简单React Native 应用在鸿蒙上适配后遇到的最典型、最劝退的问题就是启动白屏。我在网上也看到很多人搜“react native 启动白屏”这个问题说明这不是个例。我刚开始编译完成把 hap 包装到鸿蒙模拟器上一跑确实就是白屏没有报错、没有日志等两三秒才出现页面内容。这其实是 RN 的 JS Bundle 加载时序问题。RN 运行的原理是原生宿主先启动然后在宿主内初始化 JS 引擎加载 bundle 文件并执行 JS 逻辑最后把 JS 里描述的原生视图渲染出来。这个过程中如果 bundle 没有提前打进去而是从网络加载或者从本地文件按需读取那么从“原生起帧”到“JS 首帧渲染”之间就有一段空白期。定位时我建议不要上来就闷头改代码先把日志级别调到最全观察生命周期回调。我最后定位到的根因是鸿蒙适配版 RN 默认从jsbundle/index.harmony.bundle读取本地 bundle但打包时 bundle 的产物路径和工程配置里读的路径不一致导致加载阶段超时进入兜底逻辑后才被动完成渲染。修复方式很明确在 RN 鸿蒙工程的metro.config.js里把output和sourceMap的配置对齐确保生成的 bundle 文件名和宿主工程里BundleName指向的完全一致。同时在MainActivity的初始化配置里把loadFromFile的路径写成固定的绝对路径不要用相对路径兜底。// metro.config.js 关键配置示例 module.exports { transformer: { getTransformOptions: async () ({ transform: { experimentalImportSupport: false, inlineRequires: true, }, }), }, serializer: { // 确保输出到鸿蒙工程能识别的位置 output: { filename: index.harmony.bundle } } };4.2 每次改完 JS 都要重新打包 hap本地调试链路怎么打通鸿蒙应用的打包产物是 hap 包传统流程下每次改了 RN 层的 JS 代码都需要重新执行一次 RN 打包命令生成 bundle再跑到 DevEco Studio 里走一遍构建流程生成新的 hap再安装到模拟器。这个链路太长了一次最少也要两三分钟调试体验非常痛苦。我后来把调试链路改成了这样在开发阶段RN 侧代码以 “Debug Server” 模式运行也就是不把 bundle 打进 hap 里而是让鸿蒙宿主在启动时从开发电脑的 Metro Server 去拉取 bundle。这样任何一次 JS 改动只需要 Metro 自动刷新鸿蒙模拟器上的应用就会从服务端拿到最新 JS然后通过热重载机制刷新界面整个过程两三秒。这里有一个关键点要注意鸿蒙模拟器或者真机要能访问到开发电脑的 Metro Server 端口需要设置正确的 host 和端口。我们现在开发中常用的方案是把 Metro Server 的地址通过鸿蒙工程里的entry/src/main/resources/rawfile下的配置文件指定或者在 build profile 里设置DevServer host。// 设置 Metro Server 调试地址示例开发阶段使用 // entry/src/main/ets/entryability/EntryAbility.ets let devConfig { host: 192.168.1.100, // 换成你的开发电脑IP port: 8081, bundleName: index.harmony.bundle }调试链路打通之后整个项目的开发效率提升非常明显。如果你也在做 RN 鸿蒙适配这个 “DevServer 热加载” 的配置建议优先搞定否则你会被反复打包 hap 这件事消耗大量精力。4.3 热更新和 hap 产物体积权衡鸿蒙应用的分发有几种方式应用市场发版、企业分发、hap 本地安装。在这个就诊准备应用的实际使用场景中很多是医院内部或者医联体内部给患者安装不太可能每次都走应用市场审核。这就引出一个问题RN 的 JS bundle 能不能单独热更新而不重新安装 hap答案是能但有条件。RN 的 JS bundle 本质上就是一个文本文件我们可以把它放到远端服务器在 App 启动时先检查本地 bundle 的版本号如果有新版本就下载覆盖。这种方案在 Android 平台上已经非常成熟鸿蒙平台因为有沙箱机制对应用目录的访问权限控制得更严格但 RN 鸿蒙版仍然开放了 bundle 的动态加载接口允许我们从指定路径读取并执行 bundle。实操下来我建议首次安装的 hap 包内置一个稳定的基础版本 bundle之后每次业务迭代都走远端动态下发。这样做有一个直接的好处——hap 的体积能控制在一个比较小的范围用户下载快、安装时间短。我们最后打出来的 hap 大概在 18MB 左右其中原生代码和资源占了绝大部分bundle 本身只有几百 KB后续更新几乎不占流量。4.4 鸿蒙权限配置与原生模块桥接医院就诊应用涉及拍照上传检查报告、读取本地相册、推送通知等能力。鸿蒙系统对权限管控比较严格应用必须在module.json5里声明对应的权限才能调用系统 API否则运行时会被拒绝甚至直接闪退。我在适配过程中最主要的工作是写了两个原生模块的桥接一个是相册选择模块将鸿蒙的PhotoViewPicker封装成 RN 可以调用的 Promise 方法另一个是通知本地提醒模块用于就诊前一天的提醒推送。桥接的核心代码在鸿蒙侧用 ArkTS 写通过RNInstance向外暴露模块。// 鸿蒙原生模块示例相册选择桥接 import { photoAccessHelper } from kit.MediaLibraryKit; import { rn } from rnoh/react-native-openharmony; export class PhotoPickerModule extends rn.RNOHCorePackage { createModules() { return [{ name: PhotoPickerModule, methods: { pickPhoto: this.pickPhoto.bind(this), }, }]; } async pickPhoto() { try { let picker new photoAccessHelper.PhotoViewPicker(); let result await picker.select({ MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE }); return { uri: result.photoUris[0], }; } catch (error) { return { error: error.message }; } } }RN 侧通过NativeModules.PhotoPickerModule.pickPhoto()就能调用。桥接的坑主要出现在类型转换上鸿蒙侧返回的对象如果包含复杂嵌套字段RN 层解析时可能会丢数据我的经验是尽量在鸿蒙侧把返回值拍平转成一层简单键值对再交出去。5. 优化与踩坑性能调优、网络协同与常见问题规避5.1 长列表性能优化让患者医生列表滑得动医生列表和排班数据展示是本项目最典型的性能瓶颈场景。我在真机上测试时遇到过明显掉帧的现象此时系统的 CPU 占用率并不高反而是 JS 线程长期处于忙碌状态。这个现象的本质是RN 的 JS 线程和 UI 线程是分离的当 FlatList 渲染大量 item 时如果每个 item 的渲染逻辑包含复杂的样式计算或重新创建匿名函数JS 线程的任务处理能力就成了瓶颈。优化的手法比较常规但很有效核心就是减少“浪费的渲染”。第一层优化是给列表项组件包一层React.memo保证父组件数据变化时未变化的子组件不会重新渲染。第二层是使用getItemLayout告诉 FlatList 每个项目的高度这样就不需要动态测量所有项可以跳过大量计算。第三层优化是图片列表的懒加载和降采样。医生头像和科室配图原本是高清图在列表里根本不需要那么高的分辨率我在服务端生成了多套尺寸缩略图列表页用 160x160 的版本点击进入医生详情页再用高清图。缩略图的体积基本上只有原图的十分之一这对长列表来说是非常可观的加载压力减轻。5.2 弱网和断网环境下的表单数据保护医院导航、地下诊区这些场景下患者的手机信号经常很差。预就诊表单本来就是用户花时间填写的如果因为网络抖动导致数据丢失用户的挫败感会非常强。我在历史填写模块做了“本地暂存 定时同步”的机制用户每次输入表单项的状态会通过 debounce防抖机制延迟 1.5 秒后自动写入本地 SQLite。如果用户中途切走应用或者杀掉进程下一次进来时会有恢复草稿的提示把之前未提交的数据找回。只有当用户点了“提交”且网络请求返回成功本地对应数据才被标记为已同步。这个机制在具体实现上要注意一点SQLite 写入不要太频繁。频繁的磁盘写入不仅影响性能还会让低端手机发热。所以要设置一个合理的防抖时间并且在应用进入后台时强制写一次保证数据尽可能在手机关闭前落盘。5.3 鸿蒙系统版本差异和兼容性适配鸿蒙生态目前存在一个事实——不同设备上运行的鸿蒙版本差异很大有基于 AOSP 的旧版本也有 HarmonyOS NEXT 的纯血版本。这意味着同一个 RN 应用在适配时可能要面临两套底层 API 差异。我的建议是团队从最开始就建立一个系统版本检测层封装统一 API。在 JS 层通过调用原生的系统能力封装获取当前设备的鸿蒙主版本号然后根据版本分发到不同的逻辑分支。比如在 HarmonyOS NEXT 上某些图片选择接口的命名和参数格式与其他版本明显不同统一封装后可以简化上层业务代码。5.4 医院信息系统对接时的网络边界这个应用最终要落地到医院场景就避不开对接医院的 HIS 系统医院信息系统里的科室排班、号源、医生职称数据。各家医院的IT系统标准不统一接口设计千差万别。我在项目初期就把和医院信息科对接的工作单独梳理出一条线不直接让 App 请求 HIS 的接口而是通过一个集成网关层把 HIS 数据转换成应用需要的标准格式App 只和网关通信。网关层的好处是可以把 HIS 接口的特殊字段名、响应结构差异全部吸收在服务端客户端只面向自己理想的数据模型。后续更换医院合作方时只需要重新实现网关客户端保持稳定。这个架构思路对医疗场景特别重要因为医院系统改造周期长集成网关是隔离波动性最稳妥的方案。6. 线上运行效果与复盘哪些地方还能做得更好应用上线试运行后的数据反馈比我预期的要好。下面把真实运行情况放出来供大家参考。核心流程转化率方面在到院前完成“症状描述 病史填写”的用户占到挂号预约总人数的 62%这个比例说明大部分患者是愿意提前做一些准备工作的关键是入口要够简单。用了预就诊流程的患者在医生诊室内平均交流时长缩短了约 40%医生反馈最明显的变化是“不需要再花时间问基础病史可以直接进入诊断讨论环节”。用户操作时长方面首次使用的用户在症状描述环节平均耗时 4 分 20 秒病史填写环节平均耗时 3 分 15 秒。症状描述的时间比我预想的要长后来看了用户行为录像发现原因是很多用户在中途停下来回忆症状细节。这个数据启发我在后续版本里考虑加入“智能追问”——比如用户填了“腹痛”系统可以基于规则追问“疼痛位置是上腹还是下腹”“是否伴随恶心”帮用户更快梳理信息。崩溃率和兼容性方面上线后通过 Firebase Crashlytics 和鸿蒙侧的日志上报统计整体崩溃率稳定在 0.2% 以下没有出现大规模兼容性问题。少见的崩溃集中在低内存设备上主要是加载医生头像时图片解码导致的 OOM。后来改成加载前先检查图片尺寸再通过Image.getSize做预缩放这个问题基本消除。实际运营过程中也暴露了几个之前设计时不够周全的地方值得复盘老年用户比例占三成但真正能在手机上自助完成流程的比例不高很多人还是习惯打电话或者到窗口咨询。后续可以考虑加语音输入或者视频客服远程协助填写的功能医生排班取消的情况时有发生用户在就诊当天才发现自己预约的医生停诊了。我们只做了短信通知但没有做 App 内强提醒弹窗体验上有提升空间症状描述的开放文本在后续医生端查看时暂时还只是原样展示。如果要做智能化升级需要引入更细粒度的 NLP 结构化解析这部分目前没有足够资源推进。我个人的体会是医疗类应用最重要的指标不是 DAU日活跃用户数或使用时长而是“能不能真正减少医患沟通中的信息损耗”。就诊准备应用切入的正是这个点。它不试图替代医生的诊断不尝试做远程问诊只是把患者已知的信息在正确的时间、以结构化的方式传递给医生。这个定位一开始就要非常清醒否则很容易做成一个看起来功能很多但实际没人用的“大而全”产品。如果你是做类似方向的开发希望上面这些踩坑记录和实现细节能帮你少走些弯路。医疗信息化一直是个慢赛道需要耐心和敬畏但一旦跑起来每一分钟改进都可能直接体现在患者的就诊体验上。
返回列表