RNOH x HarmonyOS 碰一碰分享:SKU 信息近场流转接入设计
本文是《React Native x HarmonyOS NEXT 创新能力接入方案》系列第 7 篇。
前面我们已经完成了 RNOH 环境搭建、TurboModule 最小实践、TTS、OCR、端侧 AI 商品图处理以及小艺智能体拉起。
本篇继续接入 HarmonyOS 典型系统级协同能力:碰一碰分享。
1. 本篇要实现什么?
本篇我们要实现一个适合跨境电商工具场景的功能:
在 React Native 商品详情页中,点击“碰一碰分享 SKU”,将当前商品的 SKU 信息封装成分享内容,并通过 HarmonyOS 近场分享能力流转到另一台设备。
最终效果可以设计为:
RN 商品详情页
↓
点击“碰一碰分享”
↓
调用 RNOH TurboModule
↓
ArkTS 原生模块组装分享数据
↓
调用 HarmonyOS 碰一碰 / 近场分享能力
↓
接收端打开 SKU 详情或商品资料页面
2. 为什么选择“SKU 信息近场流转”作为场景?
原 UniApp 实践里,“碰一碰”分享更偏通用内容分享。
React Native 版我们可以做得更有业务差异化:
跨境电商场景下,SKU 信息经常需要快速流转
例如:
- 运营把某个商品资料分享给主管;
- 采购把供应商 SKU 信息分享给运营;
- 美工拿到商品图和卖点后继续处理主图;
- 线下会议中快速传递商品链接;
- 多设备协同查看同一个商品资料。
传统方式通常是:
复制链接
↓
打开聊天工具
↓
发送给对方
↓
对方点击进入
而碰一碰分享的体验是:
打开商品详情
↓
两台设备靠近 / 触发近场交互
↓
接收端直接进入对应 SKU 页面
这类能力非常适合体现 HarmonyOS 的系统级协同价值。
3. 本篇技术方案
整体架构如下:
React Native 层
├── TapSharePage.tsx
├── 商品信息展示
├── 分享按钮
└── 分享状态展示
RNOH 桥接层
├── NativeTapShareModule.ts
└── TurboModule 调用声明
HarmonyOS 原生层
├── TapShareModule.ets
├── 分享参数校验
├── 分享内容构造
├── 分享结果
└── 真实碰一碰能力接入位置
调用流程:
TapSharePage.tsx
↓
NativeTapShareModule.shareSku(payload)
↓
TapShareModule.ets
↓
构造 HarmonyOS 分享内容
↓
调用系统分享能力
↓
Promise 返回 RN 页面
4. 运行截图




由于博主没有两台 nova 16,因此无法实际测试
5. 定义 React Native 侧 NativeModule
新建文件:
src/native/NativeTapShareModule.ts
代码如下:
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export type TapShareSkuPayload = {
skuId: string;
spuId?: string;
title: string;
imageUrl?: string;
price?: string;
currency?: string;
stockStatus?: 'normal' | 'low' | 'out_of_stock';
targetRoute: string;
sourceScene: 'sku_detail' | 'product_list' | 'inventory_warning';
};
export type TapShareResult = {
code: number;
message: string;
shareId?: string;
status: 'waiting' | 'success' | 'cancelled' | 'failed';
targetRoute?: string;
};
export interface Spec extends TurboModule {
shareSku(payload: TapShareSkuPayload): Promise<TapShareResult>;
isTapShareAvailable(): Promise<boolean>;
}
export default TurboModuleRegistry.get<Spec>('TapShareModule');
这里我们定义了两个方法:
shareSku(payload)
用于发起 SKU 分享。
isTapShareAvailable()
用于判断当前设备环境是否支持碰一碰分享能力。
6. 设计分享参数结构
不要只传一个字符串。
如果只传:
"SKU123"
后面扩展会很困难。
建议从一开始就设计成结构化对象:
const payload = {
skuId: 'SKU-HM-001',
spuId: 'SPU-HM-2026',
title: '不锈钢厨房置物架',
imageUrl: 'https://example.com/sku-hm-001.png',
price: '19.99',
currency: 'USD',
stockStatus: 'low',
targetRoute: '/sku/detail?skuId=SKU-HM-001',
sourceScene: 'sku_detail',
};
这样接收端拿到数据后,可以决定:
- 直接打开商品详情页;
- 打开库存预警页;
- 打开商品编辑页;
- 打开图片处理页;
- 打开运营分析页。
7. 编写 React Native 页面
新建文件:
src/pages/TapSharePage.tsx
代码如下:
import React, { useEffect, useState } from 'react';
import {
ActivityIndicator,
Alert,
Pressable,
SafeAreaView,
ScrollView,
StyleSheet,
Text,
View,
} from 'react-native';
import TapShareModule, {
TapShareResult,
TapShareSkuPayload,
} from '../native/NativeTapShareModule';
const skuPayload: TapShareSkuPayload = {
skuId: 'SKU-HM-001',
spuId: 'SPU-HM-2026',
title: '不锈钢厨房置物架',
imageUrl: 'https://example.com/sku-hm-001.png',
price: '19.99',
currency: 'USD',
stockStatus: 'low',
targetRoute: '/sku/detail?skuId=SKU-HM-001',
sourceScene: 'sku_detail',
};
export default function TapSharePage() {
const [available, setAvailable] = useState<boolean | null>(null);
const [sharing, setSharing] = useState(false);
const [result, setResult] = useState<TapShareResult | null>(null);
useEffect(() => {
checkAvailable();
}, []);
async function checkAvailable() {
try {
const supported = await TapShareModule.isTapShareAvailable();
setAvailable(supported);
} catch (error) {
console.warn('check tap share available failed:', error);
setAvailable(false);
}
}
async function handleShareSku() {
try {
setSharing(true);
setResult({
code: 1001,
message: '正在等待设备靠近,请使用支持碰一碰能力的设备进行分享',
status: 'waiting',
});
const res = await TapShareModule.shareSku(skuPayload);
setResult(res);
if (res.status === 'success') {
Alert.alert('分享成功', res.message);
} else if (res.status === 'cancelled') {
Alert.alert('已取消', res.message);
} else {
Alert.alert('分享失败', res.message);
}
} catch (error) {
console.error('share sku failed:', error);
setResult({
code: -1,
message: '调用碰一碰分享失败,请查看日志',
status: 'failed',
});
} finally {
setSharing(false);
}
}
const stockText =
skuPayload.stockStatus === 'low'
? '库存偏低'
: skuPayload.stockStatus === 'out_of_stock'
? '已断货'
: '库存正常';
return (
<SafeAreaView style={styles.safeArea}>
<ScrollView contentContainerStyle={styles.container}>
<Text style={styles.title}>SKU 碰一碰分享</Text>
<Text style={styles.subtitle}>
React Native x HarmonyOS 近场分享能力接入方案
</Text>
<View style={styles.card}>
<Text style={styles.label}>商品标题</Text>
<Text style={styles.productTitle}>{skuPayload.title}</Text>
<View style={styles.row}>
<Text style={styles.rowLabel}>SKU</Text>
<Text style={styles.rowValue}>{skuPayload.skuId}</Text>
</View>
<View style={styles.row}>
<Text style={styles.rowLabel}>售价</Text>
<Text style={styles.rowValue}>
{skuPayload.currency} {skuPayload.price}
</Text>
</View>
<View style={styles.row}>
<Text style={styles.rowLabel}>库存状态</Text>
<Text style={styles.warningValue}>{stockText}</Text>
</View>
<View style={styles.row}>
<Text style={styles.rowLabel}>目标路由</Text>
<Text style={styles.routeValue}>{skuPayload.targetRoute}</Text>
</View>
</View>
<View style={styles.statusCard}>
<Text style={styles.statusTitle}>设备能力检测</Text>
<Text style={styles.statusText}>
{available === null
? '检测中...'
: available
? '当前设备支持碰一碰分享'
: '当前环境暂未检测到碰一碰能力,使用 Mock 分享链路'}
</Text>
</View>
<Pressable
style={[styles.button, sharing && styles.buttonDisabled]}
disabled={sharing}
onPress={handleShareSku}
>
{sharing ? (
<ActivityIndicator />
) : (
<Text style={styles.buttonText}>碰一碰分享 SKU</Text>
)}
</Pressable>
{result ? (
<View style={styles.resultCard}>
<Text style={styles.resultTitle}>分享结果</Text>
<Text style={styles.resultText}>状态:{result.status}</Text>
<Text style={styles.resultText}>编码:{result.code}</Text>
<Text style={styles.resultText}>说明:{result.message}</Text>
{result.shareId ? (
<Text style={styles.resultText}>Share ID:{result.shareId}</Text>
) : null}
</View>
) : null}
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#F7F8FA',
},
container: {
padding: 20,
},
title: {
fontSize: 26,
fontWeight: '700',
color: '#111827',
marginTop: 12,
},
subtitle: {
fontSize: 15,
color: '#6B7280',
marginTop: 8,
lineHeight: 22,
},
card: {
backgroundColor: '#FFFFFF',
borderRadius: 18,
padding: 18,
marginTop: 24,
},
label: {
fontSize: 13,
color: '#6B7280',
},
productTitle: {
fontSize: 20,
color: '#111827',
fontWeight: '700',
marginTop: 6,
marginBottom: 16,
},
row: {
paddingVertical: 10,
borderTopWidth: StyleSheet.hairlineWidth,
borderTopColor: '#E5E7EB',
},
rowLabel: {
fontSize: 13,
color: '#6B7280',
marginBottom: 4,
},
rowValue: {
fontSize: 15,
color: '#111827',
},
warningValue: {
fontSize: 15,
color: '#B45309',
fontWeight: '600',
},
routeValue: {
fontSize: 14,
color: '#2563EB',
},
statusCard: {
backgroundColor: '#FFFFFF',
borderRadius: 18,
padding: 18,
marginTop: 16,
},
statusTitle: {
fontSize: 16,
color: '#111827',
fontWeight: '600',
},
statusText: {
fontSize: 14,
color: '#4B5563',
lineHeight: 21,
marginTop: 8,
},
button: {
marginTop: 20,
backgroundColor: '#0A59F7',
borderRadius: 16,
paddingVertical: 14,
alignItems: 'center',
},
buttonDisabled: {
opacity: 0.6,
},
buttonText: {
fontSize: 16,
color: '#FFFFFF',
fontWeight: '600',
},
resultCard: {
backgroundColor: '#FFFFFF',
borderRadius: 18,
padding: 18,
marginTop: 20,
},
resultTitle: {
fontSize: 16,
color: '#111827',
fontWeight: '600',
marginBottom: 10,
},
resultText: {
fontSize: 14,
color: '#374151',
lineHeight: 22,
},
});
8. ArkTS 原生模块
用来验证 RNOH 桥接链路。
新建文件:
harmony/entry/src/main/ets/turbomodule/TapShareModule.ets
示例代码:
import { harmonyShare, systemShare } from '@kit.ShareKit';
import { uniformTypeDescriptor as utd } from '@kit.ArkData';
import { UITurboModule, UITurboModuleContext } from '@rnoh/react-native-openharmony';
interface TapShareSkuPayload {
skuId: string;
spuId?: string;
title: string;
imageUrl?: string;
price?: string;
currency?: string;
stockStatus?: string;
targetRoute: string;
sourceScene: string;
}
interface TapShareResult {
code: number;
message: string;
shareId?: string;
status: string;
targetRoute?: string;
}
export class TapShareModule extends UITurboModule {
static readonly NAME = 'TapShareModule';
private currentPayload: TapShareSkuPayload | null = null;
private isRegistered: boolean = false;
private readonly knockShareCallback = (sharableTarget: harmonyShare.SharableTarget): void => {
this.handleKnockShare(sharableTarget).catch((err: Error) => {
console.error(`[TapShareModule] knockShare failed: ${err.message}`);
});
};
constructor(ctx: UITurboModuleContext) {
super(ctx);
}
async isTapShareAvailable(): Promise<boolean> {
return canIUse('SystemCapability.Collaboration.HarmonyShare');
}
async shareSku(payload: TapShareSkuPayload): Promise<TapShareResult> {
if (!payload || !payload.skuId || !payload.title || !payload.targetRoute) {
return {
code: 4001,
message: '分享参数不完整,请检查 skuId、title、targetRoute',
status: 'failed',
};
}
if (!await this.isTapShareAvailable()) {
return {
code: 3001,
message: '当前系统不支持 Share Kit 碰一碰分享,请确认 HarmonyOS NEXT 版本与设备能力。',
status: 'failed',
};
}
this.currentPayload = payload;
if (!this.isRegistered) {
harmonyShare.on('knockShare', this.knockShareCallback);
this.isRegistered = true;
console.info(`[TapShareModule] knockShare registered: ${JSON.stringify(payload)}`);
return {
code: 1001,
message: '已注册碰一碰分享,请将两台亮屏解锁手机顶部轻碰。',
shareId: `knock_share_${Date.now()}`,
status: 'waiting',
targetRoute: payload.targetRoute,
};
}
console.info(`[TapShareModule] knockShare payload updated: ${JSON.stringify(payload)}`);
return {
code: 1002,
message: '已更新待分享 SKU,保持当前页面等待系统碰一碰回调即可。',
shareId: `knock_share_${Date.now()}`,
status: 'waiting',
targetRoute: payload.targetRoute,
};
}
async stopShare(): Promise<boolean> {
if (!this.isRegistered) {
this.currentPayload = null;
return true;
}
harmonyShare.off('knockShare', this.knockShareCallback);
this.currentPayload = null;
this.isRegistered = false;
console.info('[TapShareModule] knockShare unregistered');
return true;
}
private async handleKnockShare(sharableTarget: harmonyShare.SharableTarget): Promise<void> {
if (!this.currentPayload) {
await sharableTarget.clarifyNonShare({ message: '当前页面暂无可分享 SKU,请返回支持碰一碰分享的界面再试。' });
return;
}
const shareData = this.buildShareData(this.currentPayload);
console.info('[TapShareModule] knockShare triggered');
await sharableTarget.share(shareData);
}
private buildShareData(payload: TapShareSkuPayload): systemShare.SharedData {
const record: systemShare.SharedRecord = {
utd: utd.UniformDataType.HYPERLINK,
content: this.buildShareLink(payload),
title: payload.title,
description: this.buildDescription(payload),
extraData: {
skuId: payload.skuId,
spuId: payload.spuId ?? '',
sourceScene: payload.sourceScene,
},
};
return new systemShare.SharedData(record);
}
private buildShareLink(payload: TapShareSkuPayload): string {
const params: Array<string> = [
`skuId=${encodeURIComponent(payload.skuId)}`,
`title=${encodeURIComponent(payload.title)}`,
`targetRoute=${encodeURIComponent(payload.targetRoute)}`,
`sourceScene=${encodeURIComponent(payload.sourceScene)}`,
];
if (payload.spuId) {
params.push(`spuId=${encodeURIComponent(payload.spuId)}`);
}
if (payload.imageUrl) {
params.push(`imageUrl=${encodeURIComponent(payload.imageUrl)}`);
}
if (payload.price) {
params.push(`price=${encodeURIComponent(payload.price)}`);
}
if (payload.currency) {
params.push(`currency=${encodeURIComponent(payload.currency)}`);
}
if (payload.stockStatus) {
params.push(`stockStatus=${encodeURIComponent(payload.stockStatus)}`);
}
return `skuassistant://demo/sku/detail?${params.join('&')}`;
}
private buildDescription(payload: TapShareSkuPayload): string {
const priceText = payload.price ? `${payload.currency ?? 'USD'} ${payload.price}` : '价格待定';
return `${priceText} · ${payload.stockStatus ?? 'normal'} · ${payload.targetRoute}`;
}
__onDestroy__(): void {
this.stopShare().catch(() => undefined);
super.__onDestroy__();
}
}
9. 注册 TurboModule(RNOH 0.84.1 实装)
第一层在 harmony/entry/src/main/ets/GeneratedPackage.ets 注册 ArkTS UITurboModule:
import { TapShareModule } from './turbomodule/TapShareModule';
return new Map([
[TapShareModule.NAME, (ctx) => new TapShareModule(ctx)],
]);
本工程的自定义 TurboModule 采用 ArkTS 实现 + C++ JSI 元数据 双层注册。只注册 ArkTS 时,编译能够通过,但真机运行会报 Couldn't find Turbo Module on the ArkTs side。
第二层由 TapShareModule.cpp 声明 JS 可调用方法:
methodMap_ = {
ARK_ASYNC_METHOD_METADATA(shareSku, 1),
ARK_ASYNC_METHOD_METADATA(isTapShareAvailable, 0),
};
TapSharePackage.h 创建 C++ TurboModule,最后在 PackageProvider.cpp 加入:
packages.push_back(std::make_shared<TapSharePackage>(ctx));
对应 .cpp 还必须加入 CMakeLists.txt。RN spec、C++ name 和 ArkTS NAME 必须全部保持为 TapShareModule。
10. 真实 HarmonyOS 碰一碰能力接入位置
真实能力替换的位置在 ArkTS 侧:
async shareSku(payload: TapShareSkuPayload): Promise<TapShareResult> {
// 1. 校验 payload
// 2. 构造分享内容
// 3. 调用 HarmonyOS 碰一碰 / 近场分享 / 系统协同能力
// 4. 监听分享状态
// 5. 将结果返回 RN
}
可以把分享内容抽象成:
type HarmonyShareContent = {
title: string
description: string
thumbnailUri?: string
deepLink: string
extraData: Record<string, string>
}
例如:
const content = {
title: payload.title,
description: `SKU:${payload.skuId},库存状态:${payload.stockStatus}`,
thumbnailUri: payload.imageUrl,
deepLink: `rnharmony://sku/detail?skuId=${payload.skuId}`,
extraData: {
skuId: payload.skuId,
spuId: payload.spuId ?? '',
sourceScene: payload.sourceScene
}
}
接收端拿到内容后,可以根据 deepLink 打开对应页面。
11. Deep Link 与接收端页面恢复
碰一碰分享不只是把一段文本发出去,更重要的是接收端能恢复业务页面。
建议设计统一路由:
rnharmony://sku/detail?skuId=SKU-HM-001
接收端处理逻辑:
收到分享内容
↓
解析 deepLink
↓
读取 skuId
↓
请求商品详情
↓
打开 SKU 详情页
React Native 侧可以使用 Linking 处理:
import { Linking } from 'react-native';
useEffect(() => {
const subscription = Linking.addEventListener('url', event => {
console.log('received deep link:', event.url);
// parse url and navigate
});
return () => {
subscription.remove();
};
}, []);
如果你使用 React Navigation,可以在路由配置中加入:
const linking = {
prefixes: ['rnharmony://'],
config: {
screens: {
SkuDetail: 'sku/detail',
},
},
};
12. 错误码设计
建议不要只返回 true / false。
分享类能力容易出现多种失败原因,应该从一开始设计错误码。
| code | status | 含义 |
|---|---|---|
| 0 | success | 分享成功 |
| 1001 | waiting | 等待设备靠近 |
| 2001 | cancelled | 用户取消分享 |
| 3001 | failed | 当前设备不支持 |
| 3002 | failed | 系统分享服务不可用 |
| 4001 | failed | 参数不完整 |
| 5001 | failed | 原生能力调用异常 |
RN 页面可以根据错误码展示不同提示:
if (res.code === 3001) {
Alert.alert('当前设备暂不支持碰一碰分享');
}
13. 常见问题
问题 1:RN 调用后没有反应
优先检查:
1. NativeTapShareModule.ts 中模块名是否为 TapShareModule
2. ArkTS 注册模块名是否一致
3. Codegen 是否重新生成
4. DevEco Studio 是否重新编译
5. Metro 是否加载了最新 JS
问题 2:真实设备没有出现碰一碰入口
优先检查:
1. 当前设备是否支持相关近场分享能力
2. 系统版本是否满足要求
3. 权限是否已申请
4. 是否需要在 AGC 或配置文件中开通对应能力
5. 是否在真机上验证,而不是仅在模拟器上验证
问题 3:接收端无法打开页面
优先检查:
1. deepLink scheme 是否配置正确
2. RN Linking 是否监听成功
3. React Navigation linking 配置是否正确
4. 接收端是否安装了应用
5. 分享内容中的 targetRoute 是否完整
问题 4:分享内容太大
近场分享不建议直接传完整商品详情。
推荐只传:
skuId
spuId
title
thumbnail
deepLink
sourceScene
商品详情由接收端通过 skuId 再请求。
14. 本篇小结
仓库验证结果(2026-06-24)
npm test -- --runInBand
npm run lint
npx tsc --noEmit
cd harmony && /Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw \
--mode module -p module=entry@default -p product=default assembleHap --no-daemon
自动化测试 4/4 通过,HAP 编译、打包、签名和真机安装成功。nova 16 真机验证了页面加载、结构化 payload、实现成功结果与 Share ID。真实碰一碰、接收端设备发现和系统弹窗仍不在本次实装范围内。
本篇完成了 React Native 调用 HarmonyOS 碰一碰分享能力的基础设计:
RN 商品详情页
↓
NativeTapShareModule.shareSku()
↓
RNOH TurboModule
↓
ArkTS TapShareModule
↓
碰一碰能力
↓
结果返回 RN
更重要的是,我们把分享对象从普通文本升级成了业务结构化数据:
SKU 信息
商品标题
库存状态
目标路由
Deep Link
来源场景
这样接收端不是简单“收到一段文字”,而是可以直接恢复业务页面。
15. 下一篇预告
下一篇继续接入 HarmonyOS 交互协同能力:
RNOH x HarmonyOS 智感握姿:原生事件驱动 RN UI 自适应
下一篇会实现:
ArkTS 监听握姿状态
↓
通过事件发送给 RN
↓
RN 根据左右手握持状态调整悬浮按钮位置
更多推荐



所有评论(0)