
1. 项目概述为什么在 OpenHarmony 上要单独聊 Linking先说一下背景。最近在做 React Native for OpenHarmony后面统一叫 RNOH的移植适配项目里有个需求非常典型App 要支持外部链接唤起用户从短信、H5 页面、或者另一个 App 里点击一个myapp://pages/product?skuId12345这样的链接直接跳进我们 App 的商品详情页。在 Android 和 iOS 上这套链路已经跑得很顺了但换到 OpenHarmony 上我们发现 Linking 这一块不是拿来就能用的。先说结论React Native 的 Linking API 本身是一套跨端统一的接口但在 OpenHarmony 上它底层对接的不是 Android 的 Intent 也不是 iOS 的 URL Scheme而是 OpenHarmony 的 Ability 启动参数Want。这就意味着你在 Android 上只要在 Manifest 里配个 intent-filter 就能搞定的事情在 OpenHarmony 上需要去改module.json5的 skills 配置而且获取初始链接的时机、参数解析的方式、以及模拟器上的表现都和传统 RN 开发有不少差别。这篇文章适合谁看两种人正在把现有 RN 工程往 OpenHarmony 上迁移卡在深链唤起、外部跳转这块的开发。刚接触 RNOH想系统了解 Linking 在鸿蒙上怎么配置、怎么调、踩过哪些坑的移动端工程师。我会从 Linking 的核心 API 讲起然后带大家完整走一遍鸿蒙侧的配置、RN 侧的调用链最后把启动白屏、渲染异常、链接收不到这几个高频问题全部过一遍。这些坑我都是实测踩过的不是从文档里抄的。2. Linking 核心 API 与链接数据链拆解2.1 四个核心 API 分别解决什么问题RN 的 Linking 模块说穿了就是三件事主动打开链接、被动接收链接、查询初始链接。对应到 API 上就是openURL、addEventListener(url)、getInitialURL另外还有一个canOpenURL用来判断系统能不能处理某个 scheme。Linking.openURL(url)这个最好理解就是让系统去打开一个外链比如打开浏览器、拨打电话、跳转另一个 App。在 OpenHarmony 上它实际操作的是 HarmonyOS 的want拉起机制系统会找到匹配 scheme 的 Ability 去启动。Linking.getInitialURL()用于 App 被外部链接拉起时获取冷启动状态下携带的那个 URL。这里有个关键点它不是实时获取而是在启动时返回一次。如果 App 是用户点击桌面图标正常启动的这个方法返回 null。Linking.addEventListener(url, callback)则是处理热启动场景也就是 App 已经在前台或后台运行此时又被一个新的链接唤起通过监听事件拿到新 URL。在 OpenHarmony 上这个事件的触发链路跟 Android 有些差异后面我会专门讲。最后一个canOpenURL我用的相对少它主要是在调用 openURL 之前做个可达性检查避免某些 scheme 在系统里没注册导致崩溃。2.2 从外部 URL 到 RN 页面数据是怎么流动的理解 Linking 的核心不是记住这几个方法而是搞清楚一个链接从系统层到 JS 层到底走了怎样一条链路。在 OpenHarmony 上一条外部链接的完整流转路径是这样的外部发起方短信、浏览器、另一个 App通过want携带uri拉起我们的 EntryAbility系统在启动 Ability 时把uri和parameters传进来RNOH 的 C 层和 Java/ArkTS 桥接层拿到这个数据转换成 RN 的 Linking 事件最终在 JS 层通过getInitialURL或url事件回调暴露给业务代码。这里需要特别注意的是鸿蒙的want参数是字符串到对象的映射Recordstring, Object而 RN 的 Linking 通常只处理字符串类型的 URL。所以如果你在 want 里塞的参数类型不对或者希望在 JS 层拿到的是 UC 风格的自定义 scheme 而鸿蒙系统收到的却是ohos.want.action.viewData这类标准 action两边的字段对应关系就需要手动处理。我建议在鸿蒙侧做一个简单的 uri 包装把完整链接拼成一个标准的 scheme URL 字符串再传给 RN这样 JS 侧可以复用 Android/iOS 上已经写好的解析逻辑。这条链路中容易断掉的环节是冷启动App 进程不存在和热启动App 在后台之间getInitialURL和addEventListener的分工。很多刚接触 Linking 的同学会疑惑为什么我在componentDidMount里调了getInitialURL但热启动时拿不到 URL原因就是热启动时RNOH 的判断逻辑并不走 initialURL 分支而是走事件监听分支。如果监听的注册时机晚于事件触发那这单链接就丢了。3. 实战OpenHarmony 侧的链接入口配置3.1 module.json5 里 skills 的正确配置方式在 Android 上配置深链入口是在AndroidManifest.xml里加 intent-filter在 OpenHarmony 上对应的就是entry/src/main/module.json5文件。我们需要在入口 Ability 的 skills 数组里增加一个 uri 匹配规则。看一段我实测可用的配置{ module: { abilities: [ { name: EntryAbility, srcEntry: ./ets/entryability/EntryAbility.ets, skills: [ { actions: [action.system.home], entities: [entity.system.home] }, { actions: [ohos.want.action.viewData], uris: [ { scheme: myapp, host: pages, path: product, pathStartWith: product } ] } ] } ] } }这里有几个点值得展开讲。第一skills 数组里不要只放深链的规则还要保留原来 home 的规则否则 App 图标会从桌面消失或者点击图标无法正常拉起主界面。第二uris里的scheme是必填的host和path按需配置。如果只想接收myapp://开头的所有链接可以只配 scheme 不配 host 和 path。第三也是我在 x86 模拟器上踩到的坑pathStartWith在部分 RNOH 版本的 ArkTS 解析中表现不稳定有时链接带了额外的 path 段就匹配不上了。为了稳妥我最终选择了只配schemehost然后在 JS 层做路由参数解析。这样配置的灵活性最高也最容易排查问题。3.2 在 EntryAbility 里显式处理 uri 并传给 RNmodule.json5 只是让系统知道“这个 App 能处理myapp://的链接”真正把链接传给 RN 的还是要在 EntryAbility 的代码里做一手逻辑。看下面这段 EntryAbility.ets 的核心处理onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { if (want.uri) { AppStorage.setOrCreate(initialLink, want.uri); } if (want.parameters) { // 如果自定义参数不在 uri 里可以在这里取 AppStorage.setOrCreate(linkParams, JSON.stringify(want.parameters)); } }这段代码的目的是把 Ability 收到的链接先存到一个全局存储里等 RN 侧启动完成、getInitialURL被调用时再取出来。为什么要多此一举因为在实际测试中RNOH 的桥接层在冷启动时并不总能保证第一时间把 uri 转换好如果 Activity/Ability 的 uri 只存在于系统回调里而我们在 JS 层调用getInitialURL时取值时机偏晚就可能拿到 null。用一个全局存储做缓冲是成本最低、最稳妥的做法。如果你的 App 需要在页面已经运行的状态下接收新链接记得在onNewWant回调里也做同样的处理onNewWant(want: Want): void { if (want.uri) { AppStorage.setOrCreate(currentLink, want.uri); // 主动通知 RN 侧触发 url 事件 this.emitLinkEvent(want.uri); } }onNewWant在单实例模式下App 已经在后台时会被调用对应到热启动场景。你需要在这里把链接同步给 RNOH 的 Linked 模块否则外部链接会被系统吞掉。3.3 x86 模拟器上 Linking 配置的适配注意点关于热搜词里的openharmony x86我多说一句。RNOH 官方模拟器镜像大多跑在 x86_64 架构上调试起来确实比真机方便但 Linking 这类依赖系统拉起机制的能力在模拟器上的表现和真机有差异。我遇到的一个典型问题是在 x86 模拟器里系统对自定义 scheme 的分发有时不会立即触发 Ability 的onCreate而是延迟几百毫秒甚至更久。如果 App 已经启动用户从浏览器点击链接回来onNewWant的触发是正常的但如果是冷启动想要保证链接不丢就需要轮询或者延迟获取。我的做法是在getInitialURL的 Promise 里做了 500ms 的超时重试代码类似async function fetchInitialLink() { let url await Linking.getInitialURL(); if (!url) { // 模拟器上偶发延迟重试一次 await new Promise(resolve setTimeout(resolve, 500)); url await Linking.getInitialURL(); } return url; }这个重试不是优雅但很管用。它本质上规避了 x86 模拟器的时序问题而在真机上几乎不会走到重试分支。4. RN 侧 Linking 调用实操从取链接到路由跳转4.1 初始化时正确获取 initialURL 并绑定监听RN 侧的代码在不同的工程结构里会有差异但核心套路是固定的。下面是我在一个函数组件里整理的逻辑同时处理冷热启动import { useEffect } from react; import { Linking } from react-native; import { NavigationContainer } from react-navigation/native; function DeepLinkHandler({ children }) { useEffect(() { // 处理冷启动链接 Linking.getInitialURL() .then(url { if (url) { handleDeepLink(url); } }) .catch(err console.warn(getInitialURL error, err)); // 处理热启动链接 const subscription Linking.addEventListener(url, ({ url }) { handleDeepLink(url); }); return () subscription.remove(); }, []); function handleDeepLink(url) { // 解析并跳转 navigateFromUrl(url); } return children; }注意addEventListener注册的这个监听函数在类组件时代是用Linking.addEventListener(url, handler)然后必须记得在componentWillUnmount里移除。如果你用的是 RN 0.65 的新版本remove()方法可以直接调用。很多同学只注册不注销等到页面被重复创建时回调函数被多次触发同一个链接会跳转两次页面这是深链跳转里常见的低级问题。4.2 URL 解析与路由映射的封装拿到 URL 之后下一步就是路由跳转。这里我强烈建议不要在页面组件里写一堆 if-else 去解析 URL而是把解析逻辑单独抽成一个工具模块因为链接格式将来一定会变集中管理才有维护性。以myapp://pages/product?skuId12345为例我的解析逻辑是export function parseDeepLink(url) { if (!url) return null; // 去掉 scheme 前缀 const [scheme, rest] url.split(://); if (!rest) return null; // 分离 hostpath 和 query const [hostAndPath, queryString ] rest.split(?); const segments hostAndPath.split(/).filter(Boolean); // 解析 query 参数 const params {}; queryString.split().forEach(pair { const [key, value] pair.split(); if (key) params[key] decodeURIComponent(value || ); }); return { scheme, segments, params, }; }拿到 segments 之后再映射到具体的路由页面我用的是react-navigation/native的linking配置在 NavigationContainer 里直接声明const linking { prefixes: [myapp://], config: { screens: { Home: home, ProductDetail: product/:skuId, OrderList: order/list, }, }, };配置了linking之后react-navigation 会自动监听 Linking 事件并做路由解析跳转可以省掉手动 handleDeepLink 的很多样板代码。但如果你用的不是 react-navigation那建议按我上面那段parseDeepLink的方式自己在全局路由逻辑里 dispatch 一条跳转动作。4.3 从 RN 主动打开外部链接除了接收链接Linking 还承担主动跳出去的任务。比如帮助中心页面有个“联系客服”按钮需要拉起系统浏览器打开 H5 页面或者拉起拨号盘。这时候代码非常简单import { Linking } from react-native; function openHelpPage() { Linking.canOpenURL(https://example.com/help) .then(supported { if (supported) { return Linking.openURL(https://example.com/help); } }) .catch(err { console.warn(open help page failed, err); }); }在 OpenHarmony 上openURL的效果是通过系统 ability 拉起浏览器一般没有问题。但如果打开的是另一个 RNOH 应用的 scheme比如要拉起同一个厂商的另一款 App需要注意对方应用的 module.json5 里 roles 标签是否声明了ohos.app.ability.role.openserver之类的权限这属于鸿蒙系统侧的参数不是 RN 能控制的。5. 常见问题与排查技巧实录5.1 启动白屏Linking 是“元凶”之一热搜词里有个 “react native 启动白屏”很多人的第一反应是 bundle 加载慢或渲染线程问题。但我在 RNOH 上遇到过的白屏场景里有一个确实是 Linking 造成的根源在于getInitialURL返回了一个包含特殊字符的链接JS 侧在解析时抛了异常导致首屏组件没有正常渲染。具体来说URL 里的参数如果带了%、#或中文编码而解析模块没有做decodeURIComponent字符串拼接就会产出非法的路由参数react-navigation 在匹配路由时会直接报错页面就崩在启动阶段。表面看起来是白屏实际是 JS 运行时报错被吞掉。排查建议在getInitialURL的回调里先打印原始 URL确认链接格式在生成的页面路由里加上 ErrorBoundary把渲染异常显示出来而不是任它白屏。我之前是把链接里所有%先解码再做 JSON parse问题立刻消失。另外还有一种白屏App 在冷启动时已被链接拉起但 RNOH 的初始化还没完成此时handleDeepLink里的跳转执行太早导致路由状态被覆盖成 undefined。我的处理方式是用一个isReady状态在 NavigationContainer 的onReady回调之后才处理 initialURL。const [navReady, setNavReady] useState(false); NavigationContainer onReady{() { setNavReady(true); handlePendingDeepLink(); }} 5.2 画面渲染异常onNewWant 里 setState 的时序陷阱第二个热搜词 “openharmony 画面渲染异常”我在深链测试中也遇到过。场景是这样的App 在前台展示商品列表页此时外部链接拉起同一个商品详情页onNewWant里把新链接抛给 React 层React 层执行navigation.navigate(ProductDetail, { skuId })结果页面偶尔出现商品图片加载异常、列表残留的现象。排查后发现问题不出在 Navigation 本身而是onNewWant的调用线程与 RN OH 应用的主线程之间存在竞态。链接事件到达 JS 层时可能正值某一帧渲染中此时立即 navigation 跳转会打断渲染管线导致界面元素刷新不完整。我们的解决方案是给跳转动作做一个微小的延时把它推入下一个事件循环function safeNavigate(route) { requestAnimationFrame(() { navigationRef.current?.navigate(route); }); }用 rAF 对齐渲染帧之后渲染异常基本消失。这个经验在 Android 上不一定用得上但在 RNOH 上实测很管用我估计跟 OpenHarmony 上 JS 引擎与渲染线程的调度策略有关。5.3 Linking 收不到 url 事件的排查步骤清单链接配了点击了但页面没跳转这是 Linking 问题里最恼人的一种。我在联调阶段就遇到过后来整理了一个排查顺序建议按这个顺序走可以快速定位问题到底出在哪一层。确认链接能被系统识别。在测试环境里用命令或另一个应用手动拉起比如打开系统浏览器地址栏输入myapp://pages/product?skuId1如果毫无反应八成是 module.json5 的 skills 配置问题。确认 EntryAbility 收到了 want。在onCreate和onNewWant里用 hilog 打印want.uri。如果这里没有输出说明链接压根没到我们 App问题在第一步。确认链接已经存入 AppStorage。检查第二步的 setOrCreate 是否正常执行。确认 RN 侧能取到值。在 JS 的getInitialURL回调里打日志看是否能拿到字符串。确认路由解析正常。把 URL 丢到parseDeepLink里单测看返回结果是否和预期一致。按这个链路排查大部分问题会在第 1 步和第 4 步暴露出来。我在真实项目中遇到过最无语的一种情况RN 侧所有代码都正常但getInitialURL一直是 null最后发现是module.json5里的 skills 数组写了两份 home 规则把第二份规则的uris给覆盖了系统以为这个 Ability 只处理 home不处理自定义 scheme。5.4 问题速查表现象可能原因处理建议点击深链无任何反应module.json5 缺少 uris 匹配检查 scheme/host 是否匹配使用真机验证冷启动拿不到 URL时序问题getInitialURL 调用过早加入 500ms 重试或通过 AppStorage 缓存热启动链接丢失onNewWant 未实现或未通知 RN在 onNewWant 里主动触发 RN 的 url 事件拿到链接但跳转失败URL 编码未解码或路由名不匹配统一 decodeURIComponent并检查路由配置页面白屏深链处理异常导致首屏渲染失败加 ErrorBoundary打印 JS 错误渲染残留跳转打断渲染管线的时序问题使用 rAF 对齐跳转时机6. 个人实操体会Linking 调试的经验与扩展建议先分享一个调试技巧。在 OpenHarmony 的模拟器和真机上手动模拟深链拉起非常麻烦因为命令行工具并不像 Android 的adb shell am start那么直观。我做了一个笨但有效的方法在项目里加一个隐藏的调试页面页面上放几个按钮每个按钮发送一个开发用的临时链接事件用来模拟外部链接。这样就不用每次跑到短信或浏览器里去敲 URL 了测试效率提升了一大截。还有一个小细节在撰写module.json5时一定要记得在 git 提交前检查一下别把本地调试用的 scheme 写死到正式环境。我在开发阶段用的是myapp_dev://发布前要统一替换成正式的myapp://。这个替换过程不能只改 module.json5还要同步改 JS 层的prefixes和 parse 逻辑两边不一致的话正式包就会出现“能收到链接但路由解析失败”的诡异问题。关于扩展方向如果你的应用是在 OpenHarmony 上做 IoT 或富设备适配Linking 除了处理 URL scheme还可以配合系统级的公共事件CommonEvent做设备间跳转比如从智能硬件管理应用唤起控制页。RNOH 目前对这类能力的封装还在不断完善中但思路和 URL scheme 是一致的鸿蒙侧把 want 里的数据接住转成 Linking 事件给 JS剩下的就是纯前端开发的事了。最后再说说代码仓库的维护建议。Linking 相关代码建议单独抽成一个模块不要散落在页面组件里。因为你迟早会遇到多个页面都要响应链接、或者路由配置频繁调整的情况集中管理 namespace、link 解析函数、跳转路由映射表后续的维护成本会低很多。我在这个项目里就是这样做的深链逻辑上线之后后续新增一个分享页面只需要在路由映射表里加一行改一个页面入口十分钟就能搞定。